use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use crate::app::{dom, templates};
pub(super) fn save_prompt_pressed() {
let Some(textarea) = dom::textarea_by_id("prompt-input") else { return };
let content = textarea.value();
dom::swap_inner(
"prompt-msg",
"<span style=\"color:var(--muted)\">saving…</span>",
);
wasm_bindgen_futures::spawn_local(async move {
match crate::app::system_prompt::save(&content).await {
Ok(()) => {
let trimmed = content.trim();
let summary = if trimmed.is_empty() {
"✓ saved · using default on next session"
} else {
"✓ saved · takes effect on next session"
};
dom::swap_inner(
"prompt-msg",
&dom::msg_span(dom::Msg::Accent, summary),
);
}
Err(err) => {
dom::swap_inner(
"prompt-msg",
&dom::msg_span(dom::Msg::Error, &err.to_string()),
);
}
}
});
}
pub(super) fn save_tool_allowlist_pressed() {
use crate::types::BuiltinTool;
let mut enabled = Vec::new();
if let Some(doc) = web_sys::window().and_then(|w| w.document()) {
if let Ok(checkboxes) = doc.query_selector_all(".tool-checkbox") {
for i in 0..checkboxes.length() {
if let Some(el) = checkboxes.get(i) {
let input: web_sys::HtmlInputElement = JsCast::unchecked_into(el);
if input.checked() {
if let Some(name) = input.get_attribute("data-tool") {
if let Some(tool) = BuiltinTool::ALL.iter().find(|t| t.wire_name() == name) {
enabled.push(*tool);
}
}
}
}
}
}
}
dom::swap_inner(
"tool-allowlist-msg",
"<span style=\"color:var(--muted)\">saving…</span>",
);
wasm_bindgen_futures::spawn_local(async move {
match crate::app::tool_allowlist::save(&enabled).await {
Ok(()) => {
let summary = crate::app::tool_allowlist::summary(&enabled);
dom::swap_inner(
"tool-allowlist-msg",
&dom::msg_span(dom::Msg::Accent, &format!("✓ saved · {summary} · takes effect on next session")),
);
}
Err(err) => {
dom::swap_inner(
"tool-allowlist-msg",
&dom::msg_span(dom::Msg::Error, &err.to_string()),
);
}
}
});
}
pub(super) fn reset_tool_allowlist_pressed() {
dom::swap_inner(
"tool-allowlist-msg",
"<span style=\"color:var(--muted)\">resetting…</span>",
);
if let Some(doc) = web_sys::window().and_then(|w| w.document()) {
if let Ok(checkboxes) = doc.query_selector_all(".tool-checkbox") {
for i in 0..checkboxes.length() {
if let Some(el) = checkboxes.get(i) {
let input: web_sys::HtmlInputElement = JsCast::unchecked_into(el);
input.set_checked(true);
}
}
}
}
wasm_bindgen_futures::spawn_local(async move {
match crate::app::tool_allowlist::save(&[]).await {
Ok(()) => {
dom::swap_inner(
"tool-allowlist-msg",
"<span style=\"color:var(--accent)\">✓ reset · all tools enabled · takes effect on next session</span>",
);
}
Err(err) => {
dom::swap_inner(
"tool-allowlist-msg",
&dom::msg_span(dom::Msg::Error, &err.to_string()),
);
}
}
});
}
pub(super) fn save_api_key_pressed() {
let Some(input) = dom::input_by_id("api-key-input") else { return };
let value = input.value().trim().to_string();
if value.is_empty() {
return;
}
if let Ok(Some(storage)) = dom::session_storage() {
let _ = storage.set_item("gemini_api_key", &value);
}
dom::swap_inner(
"api-key-msg",
"<span style=\"color:var(--muted)\">checking…</span>",
);
wasm_bindgen_futures::spawn_local(async move {
crate::app::key_store::save(&value).await;
crate::app::opfs::refresh().await;
if let Some(false) = gemini_key_is_valid(&value).await {
dom::swap_inner(
"api-key-msg",
"<span style=\"color:var(--error)\">key rejected — check it</span>",
);
return;
}
if let Some(el) = dom::by_id("api-key-modal") {
if let Some(parent) = el.parent_element() {
let _ = parent.remove_child(&el);
}
}
if let Some(name) = crate::app::tenant::current_name() {
super::key_sync::auto_sync_gemini_key(name, value).await;
}
});
}
async fn gemini_key_is_valid(key: &str) -> Option<bool> {
let url = format!("https://generativelanguage.googleapis.com/v1beta/models?key={key}");
match reqwest::Client::new().get(&url).send().await {
Ok(resp) => Some(resp.status().is_success()),
Err(_) => None,
}
}
pub(super) fn save_x402_price_pressed() {
let Some(input) = dom::input_by_id("x402-price-input") else {
return;
};
let raw = input.value().trim().to_string();
wasm_bindgen_futures::spawn_local(async move {
use crate::filesystem::Filesystem;
let fs = crate::app::shared_opfs();
let result: Result<(), String> = async {
if raw.is_empty() || raw == "0" {
let _ = fs.delete(".lh_x402_price").await;
return Ok(());
}
let whole: u128 = raw.parse().map_err(|_| "bad amount".to_string())?;
let wei = whole
.checked_mul(1_000_000_000_000_000_000u128)
.ok_or_else(|| "overflow".to_string())?;
fs.write_atomic(".lh_x402_price", wei.to_string().as_bytes())
.await
.map_err(|e| e.to_string())
}
.await;
match result {
Ok(()) => dom::swap_inner(
"x402-price-msg",
"<span style=\"color:var(--muted)\">saved</span>",
),
Err(e) => {
web_sys::console::warn_1(&JsValue::from_str(&format!("x402 price: {e}")));
dom::swap_inner(
"x402-price-msg",
"<span style=\"color:var(--error)\">save failed</span>",
);
}
}
});
}
pub(super) fn header_admin_toggle() {
let body = match crate::app::tenant::current() {
crate::app::tenant::Host::Apex => templates::admin_dropdown_apex().into_string(),
crate::app::tenant::Host::Tenant(_) | crate::app::tenant::Host::Other(_) => {
templates::admin_dropdown_tenant().into_string()
}
};
dom::swap_outer("header-admin-panel", &body);
if let Some(card) = crate::app::APP.with(|c| c.borrow().financial_card_html.clone()) {
if dom::by_id("financial-slot").is_some() {
dom::swap_outer("financial-slot", &card);
}
}
wasm_bindgen_futures::spawn_local(async move {
refresh_usage_slot().await;
});
wasm_bindgen_futures::spawn_local(async move {
super::refresh_credits_pill().await;
});
wasm_bindgen_futures::spawn_local(async move {
super::schedule::refresh_jobs_list().await;
});
wasm_bindgen_futures::spawn_local(async move {
super::bounty::refresh_bounty_list().await;
});
wasm_bindgen_futures::spawn_local(async move {
super::guild::refresh_guild_list().await;
});
if matches!(crate::app::tenant::current(), crate::app::tenant::Host::Apex) {
wasm_bindgen_futures::spawn_local(async move {
super::devices::refresh_signer_list().await;
});
}
if matches!(
crate::app::tenant::current(),
crate::app::tenant::Host::Tenant(_) | crate::app::tenant::Host::Other(_)
) {
if let Ok(Some(storage)) = dom::session_storage() {
if let Ok(Some(cached)) = storage.get_item("gemini_api_key") {
if let Some(input) = dom::input_by_id("key") {
input.set_value(&cached);
super::refresh_keymeta();
}
}
}
wasm_bindgen_futures::spawn_local(async move {
if let Some(persisted) = crate::app::key_store::load().await {
if let Some(input) = dom::input_by_id("key") {
input.set_value(&persisted);
super::refresh_keymeta();
}
}
if let Some(prompt) = crate::app::system_prompt::load().await {
if let Some(textarea) = dom::textarea_by_id("prompt-input") {
textarea.set_value(&prompt);
}
}
{
use crate::filesystem::Filesystem;
if let Ok(bytes) = crate::app::shared_opfs().read(".lh_x402_price").await {
if let Some(wei) = String::from_utf8(bytes)
.ok()
.and_then(|s| s.trim().parse::<u128>().ok())
{
if let Some(input) = dom::input_by_id("x402-price-input") {
input.set_value(&(wei / 1_000_000_000_000_000_000u128).to_string());
}
}
}
}
if let Some(allowed) = crate::app::tool_allowlist::load().await {
if let Some(doc) = web_sys::window().and_then(|w| w.document()) {
if let Ok(checkboxes) = doc.query_selector_all(".tool-checkbox") {
for i in 0..checkboxes.length() {
if let Some(el) = checkboxes.get(i) {
let input: web_sys::HtmlInputElement = JsCast::unchecked_into(el);
if let Some(name) = input.get_attribute("data-tool") {
let is_allowed = allowed.iter().any(|t| t.wire_name() == name);
input.set_checked(is_allowed);
}
}
}
}
}
let summary = crate::app::tool_allowlist::summary(&allowed);
dom::swap_inner("tool-allowlist-status", &summary);
} else {
dom::swap_inner("tool-allowlist-status", "all tools enabled");
}
refresh_public_face_status().await;
super::credits::refresh_model_selector().await;
});
}
}
pub(super) async fn refresh_public_face_status() {
let Some(name) = crate::app::tenant::current_name() else { return };
if dom::by_id("public-face-status").is_none() {
return;
}
let face = match crate::app::net::read(crate::app::registry::id_of_name(&name)).await {
Ok(Ok(id)) if id != 0 => crate::app::net::read(crate::app::registry::public_face_of(id))
.await
.ok()
.and_then(Result::ok)
.flatten(),
_ => None,
};
let label = match face.as_deref() {
Some("app") => "currently: app",
Some("html") => "currently: html",
_ => "currently: directory (default)",
};
dom::swap_inner("public-face-status", label);
}
pub(super) fn header_admin_close() {
dom::swap_outer(
"header-admin-panel",
r#"<div id="header-admin-panel" hidden></div>"#,
);
}
pub(super) fn show_admin_tab(name: &str) {
let Some(dialog) = dom::by_id("admin-dialog") else { return };
let mut cls: Vec<String> = dialog
.class_name()
.split_whitespace()
.filter(|c| !c.starts_with("tab-"))
.map(String::from)
.collect();
cls.push(format!("tab-{name}"));
dialog.set_class_name(&cls.join(" "));
for tab in ["agent", "account", "usage", "feedback"] {
let Some(el) = dom::by_id(&format!("admin-tab-btn-{tab}")) else { continue };
let c = el.class_name();
let mut classes: Vec<&str> = c.split_whitespace().filter(|x| *x != "active").collect();
if tab == name {
classes.push("active");
}
el.set_class_name(&classes.join(" "));
}
}
pub(crate) async fn refresh_usage_slot() {
if dom::by_id("usage-tokens").is_some() {
let total = crate::app::APP.with(|c| c.borrow().total_tokens);
dom::swap_inner("usage-tokens", &format!("{total}"));
}
if dom::by_id("usage-subdomains").is_none() {
return;
}
let owner = match crate::app::tenant::current() {
crate::app::tenant::Host::Tenant(name) => {
crate::app::registry::owner_of_name(&name).await.ok().flatten()
}
_ => crate::app::APP.with(|c| c.borrow().wallet.as_ref().map(|w| w.address_hex())),
};
let count = match owner {
Some(owner_hex) => crate::app::registry::list_owned_tokens(&owner_hex)
.await
.map(|t| t.len()),
None => Ok(0),
};
match count {
Ok(n) => dom::swap_inner("usage-subdomains", &format!("{n}")),
Err(_) => dom::swap_inner("usage-subdomains", "—"),
}
}
pub(super) fn show_mobile_tab(name: &str) {
let Some(layout) = dom::by_id("layout") else { return };
let parts: Vec<String> = layout
.class_name()
.split_whitespace()
.filter(|c| !c.starts_with("tab-"))
.map(String::from)
.collect();
let mut new_cls = parts.join(" ");
if !new_cls.is_empty() {
new_cls.push(' ');
}
new_cls.push_str(&format!("tab-{name}"));
layout.set_class_name(&new_cls);
if name == "display" && dom::by_id("display-canvas").is_none() {
dom::swap_inner(
"view-content",
&crate::app::templates::display_surface().into_string(),
);
}
for tab in ["files", "chat", "display", "agent"] {
let id = format!("tab-btn-{tab}");
let Some(el) = dom::by_id(&id) else { continue };
let cls = el.class_name();
let mut classes: Vec<&str> =
cls.split_whitespace().filter(|c| *c != "active").collect();
if tab == name {
classes.push("active");
}
el.set_class_name(&classes.join(" "));
}
}