use maud::html;
use crate::router::{
docs_answer, parse_router_cmd, strip_bang, FreeAction, HeuristicClassifier,
IntentClassifier, Route, RouterCmd, UiCommand, FREE_ROUTE_FOOTER,
};
use super::super::{dom, templates, APP};
const ROUTER_FLAG_KEY: &str = "lh_router";
pub(super) enum PreRoute {
Handled,
SendToModel(String),
}
pub(super) async fn pre_route(prompt: &str) -> PreRoute {
if let Some(cmd) = parse_router_cmd(prompt) {
apply_cmd(cmd);
return PreRoute::Handled;
}
if !enabled() {
return PreRoute::SendToModel(prompt.to_string());
}
match HeuristicClassifier.classify(prompt) {
Route::Metered => PreRoute::SendToModel(strip_bang(prompt).to_string()),
Route::Free(action) => {
run_free(prompt, action).await;
PreRoute::Handled
}
}
}
fn enabled() -> bool {
let flag = dom::session_storage()
.ok()
.flatten()
.and_then(|s| s.get_item(ROUTER_FLAG_KEY).ok().flatten());
crate::router::router_enabled(flag.as_deref())
}
fn apply_cmd(cmd: RouterCmd) {
let storage = dom::session_storage().ok().flatten();
match cmd {
RouterCmd::Off => {
if let Some(s) = &storage {
let _ = s.set_item(ROUTER_FLAG_KEY, "0");
}
dom::set_status(
"intent router OFF for this session — every message goes to the model \
(metered). '/router on' reverts to the default (on).",
false,
);
}
RouterCmd::On => {
if let Some(s) = &storage {
let _ = s.remove_item(ROUTER_FLAG_KEY);
}
dom::set_status(
"intent router ON (the default) — obvious balance/files/display/docs \
messages are answered free. '!' prefix forces the model; '/router off' \
opts this session out.",
false,
);
}
RouterCmd::Status => {
let state = if enabled() { "ON (the default)" } else { "OFF (opted out this session)" };
dom::set_status(
&format!(
"intent router: {state}. Free tiers when on: balance/credits, open \
files/display/terminal, light/dark mode, inline admin cards \
(settings · identity · model · public face · funds · devices), \
a small docs FAQ. '!' prefix forces the model; '/router on|off' \
toggles."
),
false,
);
}
}
}
async fn run_free(prompt: &str, action: FreeAction) {
let (user_turn_id, assistant_turn_id) = APP.with(|cell| {
let mut app = cell.borrow_mut();
(app.alloc_id(), app.alloc_id())
});
dom::append_html(
"transcript",
&templates::turn(user_turn_id, "user", html! { (prompt) }, false).into_string(),
);
dom::scroll_to_bottom("transcript");
let body = match action {
FreeAction::BalanceQuery => rendered_with_footer(&balance_answer().await),
FreeAction::DocsAnswer(topic) => rendered_with_footer(docs_answer(topic)),
FreeAction::UiCommand(cmd) => rendered_with_footer(&run_ui_command(cmd).await),
FreeAction::AdminCard(topic) => {
crate::app::events::admin_card_will_mount();
html! {
(templates::admin_chat_card(topic))
p.admin-blurb { (FREE_ROUTE_FOOTER) }
}
}
};
dom::append_html(
"transcript",
&templates::turn(assistant_turn_id, "assistant", body, false).into_string(),
);
dom::scroll_to_bottom("transcript");
if let FreeAction::AdminCard(topic) = action {
crate::app::events::admin_card_refresh(topic).await;
}
}
fn rendered_with_footer(answer: &str) -> maud::Markup {
templates::rendered_markdown(&format!("{answer}\n\n{FREE_ROUTE_FOOTER}"))
}
async fn balance_answer() -> String {
let Some(addr) = super::credit_address_existing().await else {
return "No credit identity on this device yet — create or import an \
identity first (the apex page, or the admin panel)."
.to_string();
};
let wallet = crate::app::net::read(crate::registry::token_balance_of(&addr))
.await
.ok()
.and_then(Result::ok);
let meter = crate::app::net::read(crate::registry::credit_balance_of(&addr))
.await
.ok()
.and_then(Result::ok);
match (wallet, meter) {
(Some(wallet), Some(meter)) => {
let total = wallet + meter;
let whole = total / 1_000_000_000_000_000_000u128;
let cents = (total % 1_000_000_000_000_000_000u128) / 10_000_000_000_000_000u128;
format!(
"Balance: **{whole}.{cents:02} $LH** spendable (wallet + meter) for \
`{addr}`."
)
}
_ => "Couldn't read the balance right now (RPC timeout) — check the admin \
panel, or retry."
.to_string(),
}
}
async fn run_ui_command(cmd: UiCommand) -> String {
match cmd {
UiCommand::OpenFiles => {
crate::app::opfs::toggle_files_modal().await;
"Toggled the files modal (say it again — or tap its ×/outside — to close)."
.to_string()
}
UiCommand::OpenDisplay => {
let was_open = crate::app::dom::by_id("display-canvas").is_some();
crate::app::opfs::toggle_display();
if was_open {
"Closed the display overlay.".to_string()
} else {
"Opened the display overlay (× or ESC closes it).".to_string()
}
}
UiCommand::ThemeLight => {
if crate::app::events::set_theme_light(true) {
"Light mode on.".to_string()
} else {
"Already in light mode.".to_string()
}
}
UiCommand::ThemeDark => {
if crate::app::events::set_theme_light(false) {
"Dark mode on.".to_string()
} else {
"Already in dark mode.".to_string()
}
}
UiCommand::ViewDesktop => {
if crate::app::events::set_view_desktop(true) {
"Desktop view on.".to_string()
} else {
"Already in desktop view.".to_string()
}
}
UiCommand::ViewMobile => {
if crate::app::events::set_view_desktop(false) {
"Mobile view on.".to_string()
} else {
"Already in mobile view.".to_string()
}
}
UiCommand::OpenTerminal => {
let was_open = crate::app::cli::terminal_open();
crate::app::cli::toggle_terminal();
if crate::app::cli::terminal_open() {
"Opened the terminal overlay (× or ESC closes it).".to_string()
} else if was_open {
"Closed the terminal overlay.".to_string()
} else {
"No terminal run to show yet — the overlay replays the last CLI run; \
ask the model to run a command first."
.to_string()
}
}
}
}