use maud::{html, Markup, PreEscaped};
use crate::encoding::short_addr;
use crate::filesystem::{DirEntry, EntryKind};
use crate::types::{BuiltinTool, ToolCall, ToolResult};
use super::tenant::Host;
use super::VerifyState;
pub(crate) fn api_key_modal() -> Markup {
html! {
div #api-key-modal .api-key-modal {
div.api-key-card {
div.api-key-title { "power this agent" }
button type="button" data-action="set-model-access" data-arg="credits"
.ghost.api-key-primary { "use platform credits" }
div.api-key-or { "or bring your own key" }
form onsubmit="return false" {
div.api-key-row {
input #api-key-input
type="password"
autocomplete="off"
aria-label="gemini api key"
placeholder="paste key" {}
button type="button"
data-action="save-api-key" { "save" }
}
}
div.api-key-hint {
a href="https://aistudio.google.com/apikey"
target="_blank" rel="noopener" { "get a free key →" }
}
div #api-key-msg .feedback-msg role="status" aria-live="polite" {}
}
}
}
}
pub(crate) fn buy_modal(lh_label: &str) -> Markup {
html! {
div #buy-modal .api-key-modal {
div.api-key-card {
div.api-key-title { "buy $LH" }
div.api-key-hint { "you'll receive about " (lh_label) " (net of card fees), minted on-chain" }
div #stripe-checkout-mount style="min-height:320px;margin:12px 0" {}
div #buy-modal-done style="display:none" {
div.api-key-hint { "✓ payment received — your $LH is minting on-chain and will appear shortly." }
}
button type="button" data-action="close-buy-modal" .ghost { "close" }
}
}
}
}
pub(crate) fn rendered_markdown(raw: &str) -> Markup {
use pulldown_cmark::{html, CowStr, Event, Options, Parser, Tag};
fn safe_url(url: CowStr) -> CowStr {
let probe = url.trim_start().to_ascii_lowercase();
let dangerous = probe.starts_with("javascript:")
|| probe.starts_with("vbscript:")
|| probe.starts_with("data:");
if dangerous {
CowStr::Borrowed("#")
} else {
url
}
}
let mut opts = Options::empty();
opts.insert(Options::ENABLE_STRIKETHROUGH);
opts.insert(Options::ENABLE_TABLES);
opts.insert(Options::ENABLE_TASKLISTS);
let parser = Parser::new_ext(raw, opts).map(|event| match event {
Event::Html(h) | Event::InlineHtml(h) => Event::Text(h),
Event::Start(Tag::Link { link_type, dest_url, title, id }) => Event::Start(Tag::Link {
link_type,
dest_url: safe_url(dest_url),
title,
id,
}),
Event::Start(Tag::Image { link_type, dest_url, title, id }) => Event::Start(Tag::Image {
link_type,
dest_url: safe_url(dest_url),
title,
id,
}),
other => other,
});
let mut out = String::with_capacity(raw.len());
html::push_html(&mut out, parser);
html! { (PreEscaped(out)) }
}
pub(crate) fn site_header(_host: &Host) -> Markup {
html! {
header.site-header {
div.header-inner {
h1.header-brand {
details.brand-menu {
summary.brand-summary { "localharness" }
nav.brand-menu-items {
a href="https://localharness.xyz/" { "home" }
a href="https://github.com/compusophy/localharness"
target="_blank" rel="noopener" { "repo" }
a href="https://crates.io/crates/localharness"
target="_blank" rel="noopener" { "crate" }
}
}
}
div #header-admin .header-admin {
(notif_bell())
button type="button"
data-action="header-admin-toggle"
.header-button.admin-button { "admin" }
div #header-admin-panel hidden {}
}
}
}
}
}
pub(crate) const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
pub(crate) fn terminal_input() -> Markup {
html! {
div.terminal-body {
div #fund-banner .fund-banner role="status" aria-live="polite" {}
div.terminal-row {
span.terminal-prompt aria-hidden="true" { ">" }
textarea #prompt rows="1" aria-label="message the agent" {}
(send_button())
}
}
}
}
pub(crate) fn send_button() -> Markup {
let play = maud::PreEscaped(
"<svg viewBox=\"0 0 16 16\" width=\"12\" height=\"12\" fill=\"currentColor\" \
aria-hidden=\"true\"><path d=\"M4.5 2.5v11l9-5.5z\"/></svg>",
);
html! {
button #terminal-send .terminal-send data-action="send" title="send" aria-label="send" { (play) }
}
}
pub(crate) fn stop_button() -> Markup {
html! {
span #terminal-stop style="display:flex;align-items:center;flex-shrink:0" {
button .terminal-send.terminal-stop data-action="stop-turn" title="stop" aria-label="stop generating" {
(maud::PreEscaped("<svg viewBox=\"0 0 16 16\" width=\"11\" height=\"11\" fill=\"currentColor\" aria-hidden=\"true\"><rect x=\"3\" y=\"3\" width=\"10\" height=\"10\"/></svg>"))
}
}
}
}
pub(crate) fn fund_banner_body() -> Markup {
html! {
div style="display:flex;flex-wrap:wrap;align-items:center;gap:8px;\
padding:8px 10px;margin-bottom:8px;\
border:1px solid var(--border);background:var(--panel);\
font-size:12px;color:var(--muted)" {
span { "no $LH yet — redeem a code to start" }
input #fund-redeem-code .redeem-input type="text" aria-label="redeem code" placeholder="redeem code";
button type="button" data-action="redeem-banner" .ghost { "redeem" }
div #fund-msg .admin-msg-slot style="margin-top:0;flex-basis:100%" {}
}
}
}
pub(crate) fn verify_pill(state: &VerifyState) -> Markup {
let (class, label, title) = match state {
VerifyState::Pending => (
"tag verify-pill verify-pending",
"verifying…".to_string(),
"checking ownership against the on-chain registry".to_string(),
),
VerifyState::Verified { address } => (
"tag verify-pill verify-ok",
"✓ owner".to_string(),
format!("signature recovered {address} — matches on-chain owner"),
),
VerifyState::Visitor { owner_address, .. } => (
"tag verify-pill verify-visitor",
format!("visitor · owner {}", short_addr(owner_address)),
format!("the on-chain owner of this name is {owner_address}"),
),
VerifyState::Unregistered => (
"tag verify-pill verify-unregistered",
"not on-chain".to_string(),
"this name isn't in the registry — local-only".to_string(),
),
VerifyState::Failed { reason } => (
"tag verify-pill verify-failed",
"verify failed".to_string(),
format!("verification didn't complete: {reason}"),
),
};
html! {
span #verify-pill class=(class) title=(title) role="status" aria-label=(title) { (label) }
}
}
fn truncate_preview(text: &str, max: usize) -> String {
let flat: String = text.split_whitespace().collect::<Vec<_>>().join(" ");
if flat.chars().count() <= max {
return flat;
}
let cut: String = flat.chars().take(max).collect();
format!("{}…", cut.trim_end())
}
pub(crate) fn embed_card(
name: &str,
owner_hex: Option<&str>,
tba_hex: Option<&str>,
lh_balance_wei: Option<u128>,
is_main: Option<bool>,
) -> Markup {
let lh_whole = lh_balance_wei.map(|w| w / 1_000_000_000_000_000_000u128);
html! {
section.embed-card {
div.embed-card-header {
a.embed-card-name
href=(format!("https://{name}.localharness.xyz/"))
target="_top"
rel="noopener" {
(name)
}
@if let Some(true) = is_main {
span.embed-card-badge { "main" }
}
}
div.embed-card-rows {
@if let Some(addr) = owner_hex {
div.embed-card-row {
span.embed-card-label { "owner" }
code.embed-card-value title=(addr) { (short_addr(addr)) }
}
} @else if owner_hex.is_some() {
} @else {
div.embed-card-row {
span.embed-card-label { "owner" }
code.embed-card-value.embed-card-muted { "…" }
}
}
@if let Some(addr) = tba_hex {
div.embed-card-row {
span.embed-card-label { "wallet" }
code.embed-card-value title=(addr) { (short_addr(addr)) }
}
}
@if let Some(lh) = lh_whole {
div.embed-card-row {
span.embed-card-label { "balance" }
code.embed-card-value { (lh) " LH" }
}
}
}
}
}
}
pub(crate) fn explore_chrome(host: &Host) -> Markup {
html! {
(site_header(host))
main.explore-main {
div.explore-header {
h1.explore-title { "agents" }
}
div #explore-grid .explore-grid { "loading…" }
}
}
}
pub(crate) fn explore_grid(agents: &[(u64, String)], personas: &[Option<String>]) -> Markup {
if agents.is_empty() {
return html! {
div #explore-grid .explore-grid .explore-empty {
"no agents yet — "
a href="https://localharness.xyz/" { "claim the first one" }
}
};
}
html! {
div #explore-grid .explore-grid {
@for (i, (_, name)) in agents.iter().enumerate() {
@let preview = personas.get(i).and_then(|p| p.as_deref());
a.explore-card
href=(format!("https://{name}.localharness.xyz/"))
rel="noopener" {
span.explore-card-name { (name) }
span.explore-card-host { (name) ".localharness.xyz" }
@if let Some(p) = preview {
span.explore-card-preview { (truncate_preview(p, 80)) }
}
}
}
}
}
}
pub(crate) fn chrome(host: &Host) -> Markup {
html! {
(site_header(host))
main #layout .layout {
div.col-chat {
div #transcript .transcript role="log" aria-live="polite" aria-atomic="false"
aria-label="agent conversation" {}
section.terminal-panel {
(terminal_input())
}
}
}
div #files-modal hidden {}
div #display-overlay hidden {}
}
}
pub(crate) fn files_modal() -> Markup {
html! {
div #files-modal .files-modal {
div.files-dialog {
div.files-head {
span.files-title { "files" }
button type="button" data-action="toggle-files"
.modal-close aria-label="close files" { "×" }
}
div.files-body {
div #fs-breadcrumb .fs-breadcrumb { "/" }
ul #fs-list .fs-list {}
div #fs-viewer .fs-viewer {}
}
}
}
}
}
pub(crate) fn files_modal_closed() -> Markup {
html! { div #files-modal hidden {} }
}
pub(crate) fn display_overlay() -> Markup {
html! {
div #display-overlay .display-overlay {
button type="button" data-action="toggle-display"
.modal-close.display-close aria-label="close display" { "×" }
(display_surface())
}
}
}
pub(crate) fn display_overlay_closed() -> Markup {
html! { div #display-overlay hidden {} }
}
pub(crate) fn admin_feedback_section() -> Markup {
html! {
div.admin-section {
div.admin-section-title { "feedback" }
textarea #feedback-text
.feedback-textarea
aria-label="feedback message"
rows="6" {}
div.prompt-actions {
button type="button" data-action="feedback-submit" .ghost { "submit" }
}
div #feedback-msg .feedback-msg .admin-msg-slot {}
}
}
}
pub(crate) fn turn(turn_id: u32, role: &str, body: Markup, streaming: bool) -> Markup {
let role_class = role; let id_str = format!("turn-{turn_id}");
let body_id = format!("turn-body-{turn_id}");
let cls = if streaming {
format!("turn {role_class} streaming")
} else {
format!("turn {role_class}")
};
html! {
div id=(id_str) class=(cls) {
div id=(body_id) .body { (body) }
}
}
}
pub(crate) fn stage_container(turn_id: u32) -> Markup {
let id_str = format!("stage-{turn_id}");
html! {
div id=(id_str) .stage-line role="status" aria-live="polite" {}
}
}
pub(crate) fn stage_line(slots: &[(crate::turn_stage::Stage, crate::turn_stage::Slot)]) -> Markup {
use crate::turn_stage::Slot;
html! {
span.st-dot aria-hidden="true" {}
@for (i, (stage, slot)) in slots.iter().enumerate() {
@if i > 0 { span.st-sep { " → " } }
span class=(match slot {
Slot::Past => "st-past",
Slot::Current => "st-now",
Slot::Idle => "st-dim",
}) { (stage.word()) }
}
}
}
pub(crate) fn text_segment(seg_id: u32, text: &str) -> Markup {
let id_str = format!("seg-{seg_id}");
html! {
div id=(id_str) .text-segment { (text) }
}
}
pub(crate) fn tool_call_block(seg_id: u32, call: &ToolCall) -> Markup {
let block_id = format!("tool-{seg_id}");
let result_id = format!("tool-{seg_id}-result");
let card_id = format!("tool-{seg_id}-card");
let args_pretty = serde_json::to_string_pretty(&call.args).unwrap_or_else(|_| "{}".into());
html! {
details id=(block_id) .tool-call {
summary {
span.tc-name { (call.name) }
}
div.tc-body {
div.tc-section-label { "args" }
pre { (args_pretty) }
div id=(result_id) {}
}
}
div id=(card_id) {}
}
}
pub(crate) fn tool_call_result(result: &ToolResult) -> Markup {
let ok = result.error.is_none();
html! {
div.tc-section-label { (if ok { "result" } else { "error" }) }
@if ok {
pre {
(match &result.result {
Some(v) => serde_json::to_string_pretty(v).unwrap_or_else(|_| "(unserializable)".into()),
None => "(no output)".into(),
})
}
} @else {
div.tc-error {
pre { (result.error.as_deref().unwrap_or("(unknown error)")) }
}
}
}
}
const CARD_MAX_LINES: usize = 40;
fn card_snippet(content: &str) -> (String, usize) {
let total = content.lines().count();
if total <= CARD_MAX_LINES {
(content.trim_end_matches('\n').to_string(), 0)
} else {
let shown = content
.lines()
.take(CARD_MAX_LINES)
.collect::<Vec<_>>()
.join("\n");
(shown, total - CARD_MAX_LINES)
}
}
fn opfs_arg(path: &str) -> &str {
path.trim_start_matches('/')
}
pub(crate) fn inline_result_card(
name: &str,
args: &serde_json::Value,
result: &ToolResult,
display_thumb: Option<&str>,
) -> Option<Markup> {
if result.error.is_some() {
return None;
}
let value = result.result.as_ref()?;
match name {
"view_file" => {
let content = value.get("content")?.as_str()?;
let path = value
.get("path")
.and_then(|v| v.as_str())
.or_else(|| args.get("path").and_then(|v| v.as_str()))?;
Some(file_card(path, content))
}
"create_file" => {
let path = args.get("path").and_then(|v| v.as_str())?;
let content = args.get("content").and_then(|v| v.as_str())?;
Some(file_card(path, content))
}
"edit_file" => {
let path = args.get("path").and_then(|v| v.as_str())?;
let content = args.get("new_string").and_then(|v| v.as_str())?;
Some(file_card(path, content))
}
"list_directory" => {
let entries = value.get("entries")?.as_array()?;
let path = value.get("path").and_then(|v| v.as_str()).unwrap_or("");
Some(dir_card(path, entries))
}
"run_cartridge" | "render_html" => {
if value.get("error").is_some() || value.get("status").is_none() {
return None;
}
Some(display_card(display_thumb))
}
"embed_app" => {
if value.get("embedded").and_then(|v| v.as_bool()) != Some(true) {
return None;
}
let name = value.get("name").and_then(|v| v.as_str()).unwrap_or("app");
Some(embed_app_card(name))
}
_ => None,
}
}
fn embed_app_card(name: &str) -> Markup {
html! {
div.inline-card.embed-app-card {
div.ic-head {
span.ic-title { "▶ " (name) }
a.ghost href=(format!("https://{name}.localharness.xyz/"))
target="_blank" rel="noopener" { "open" }
}
div.embed-app-stage {
canvas id=(crate::app::display::next_embed_canvas_id()) .embed-app-canvas {}
}
}
}
}
fn file_card(path: &str, content: &str) -> Markup {
let (shown, cut) = card_snippet(content);
html! {
div.inline-card {
div.ic-head {
span.ic-title { (path) }
button.ghost data-action="opfs-open" data-arg=(opfs_arg(path)) { "open" }
}
pre.ic-body { (shown) }
@if cut > 0 {
div.ic-more { "… +" (cut) " more lines" }
}
}
}
}
fn dir_card(path: &str, entries: &[serde_json::Value]) -> Markup {
let base = opfs_arg(path).trim_end_matches('/');
let base = if base == "." { "" } else { base };
html! {
div.inline-card {
div.ic-head {
span.ic-title { (if base.is_empty() { "/" } else { base }) }
span.ic-meta { (entries.len()) " entries" }
}
div.ic-rows {
@for entry in entries {
@let name = entry.get("name").and_then(|v| v.as_str()).unwrap_or("?");
@let is_dir = entry.get("kind").and_then(|v| v.as_str()) == Some("directory");
@let arg = if base.is_empty() { name.to_string() } else { format!("{base}/{name}") };
@if is_dir {
div.ic-row role="button" tabindex="0"
data-action="opfs-nav" data-arg=(arg) {
(name) "/"
}
} @else {
div.ic-row role="button" tabindex="0"
data-action="opfs-open" data-arg=(arg) {
(name)
}
}
}
@if entries.is_empty() { div.ic-more { "(empty)" } }
}
}
}
}
fn display_card(thumb: Option<&str>) -> Markup {
html! {
div.inline-card {
div.ic-head {
span.ic-title { "▶ rendered to display" }
button.ghost data-action="toggle-display" { "show" }
}
@if let Some(url) = thumb {
img.ic-thumb src=(url) alt="display framebuffer snapshot";
}
}
}
}
pub(crate) fn apex(host: &Host, wallet_address_hex: Option<&str>) -> Markup {
let fresh = wallet_address_hex.is_none();
html! {
(site_header(host))
main.apex-main {
div.col-chat {
div #status .terminal-status role="status" aria-live="polite" {}
div #storage-warn-slot {}
@if fresh {
(crate::landing::create_wallet_cta())
} @else {
(apex_claim())
}
(crate::landing::apex_links(fresh))
}
}
}
}
fn apex_claim() -> Markup {
html! {
section.step.step-agents {
div #agents-list .agents-list {}
form.create-form data-action="apex-claim" {
input #apex-input
.create-input
type="text"
aria-label="agent name to claim"
placeholder="choose a name"
autocomplete="off"
autocapitalize="none"
autocorrect="off"
spellcheck="false"
maxlength="32"
required {}
button #create-btn type="submit" .create-button disabled { "create" }
}
}
}
}
pub(crate) fn volatile_storage_warning() -> Markup {
html! {
div .volatile-storage-warn role="alert" {
"this looks like a private / incognito window — your identity key "
"may NOT survive closing this tab. back it up via "
a href="https://localharness.xyz/?adopt=1" target="_top" rel="noopener" {
"add a device"
}
" or use a normal window."
}
}
}
pub(crate) fn admin_dropdown_apex() -> Markup {
let owner_hex = super::APP.with(|cell| {
cell.borrow().wallet.as_ref().map(|w| w.address_hex())
});
let has_wallet = owner_hex.is_some();
html! {
div #header-admin-panel .header-admin-panel {
div #admin-dialog .admin-dialog.admin-tabbed.tab-account {
div.admin-tabs {
button #admin-tab-btn-account type="button"
data-action="show-admin-tab" data-arg="account"
.admin-tab-button.active { "account" }
button #admin-tab-btn-usage type="button"
data-action="show-admin-tab" data-arg="usage"
.admin-tab-button { "economy" }
button #admin-tab-btn-feedback type="button"
data-action="show-admin-tab" data-arg="feedback"
.admin-tab-button { "feedback" }
span.admin-tabs-spacer {}
button type="button" data-action="header-admin-close" .modal-close aria-label="close admin" { "×" }
}
div.admin-tab-panel.panel-feedback {
(admin_feedback_section())
}
div.admin-tab-panel.panel-account {
(admin_identity_section(None, owner_hex.as_deref(), None, has_wallet))
@if has_wallet {
(admin_devices_section())
}
(admin_security_collapsed())
}
div.admin-tab-panel.panel-usage {
@if has_wallet { (admin_credits_section()) }
@if has_wallet { (admin_invite_section()) }
@if has_wallet { (admin_schedule_section()) }
@if has_wallet { (admin_bounty_section()) }
@if has_wallet { (admin_guild_section()) }
@if has_wallet { (admin_governance_section()) }
}
div.admin-footer {
span.admin-version { (APP_VERSION) }
}
}
}
}
}
pub(crate) fn admin_dropdown_tenant() -> Markup {
html! {
div #header-admin-panel .header-admin-panel {
div #admin-dialog .admin-dialog.admin-tabbed.tab-account {
div.admin-tabs {
button #admin-tab-btn-agent type="button"
data-action="show-admin-tab" data-arg="agent"
.admin-tab-button { "agent" }
button #admin-tab-btn-account type="button"
data-action="show-admin-tab" data-arg="account"
.admin-tab-button.active { "account" }
button #admin-tab-btn-feedback type="button"
data-action="show-admin-tab" data-arg="feedback"
.admin-tab-button { "feedback" }
span.admin-tabs-spacer {}
button type="button" data-action="header-admin-close" .modal-close aria-label="close admin" { "×" }
}
div.admin-tab-panel.panel-feedback {
(admin_feedback_section())
}
div.admin-tab-panel.panel-agent {
(admin_model_section())
(admin_prompt_section())
(admin_x402_price_section())
(admin_tool_allowlist_section())
(admin_app_section())
}
div.admin-tab-panel.panel-account {
div #financial-slot .financial-placeholder { "—" }
(admin_tba_section())
(admin_credits_section())
(admin_invite_section())
(admin_schedule_section())
(admin_bounty_section())
(admin_guild_section())
(admin_governance_section())
(admin_notify_section())
(admin_security_collapsed())
}
div.admin-footer {
span.admin-version { (APP_VERSION) }
}
}
}
}
}
pub(crate) fn admin_prompt_section() -> Markup {
html! {
div.admin-section {
div.admin-section-title { "agent prompt" }
form.prompt-form data-action="save-prompt" onsubmit="return false" {
textarea #prompt-input
.prompt-input
rows="5"
aria-label="custom system prompt"
placeholder="optional — empty uses the default" {}
div.prompt-actions {
button type="submit" .ghost { "save" }
}
}
div #prompt-msg .admin-msg-slot {}
}
}
}
pub(crate) fn admin_model_section() -> Markup {
html! {
div #model-section .admin-section {
div.admin-section-title { "model" }
div #model-selector-row .public-face-picker {
@for (id, label) in super::model::MODELS {
button type="button" data-action="set-model" data-arg=(id)
class="ghost" data-model=(id) { (label) }
}
}
div #model-msg .admin-msg-slot {}
div.public-face-preview {
button type="button" data-action="download-local-model" .ghost {
"download local model"
}
}
div #local-model-msg .admin-msg-slot {}
}
}
}
pub(crate) fn admin_x402_price_section() -> Markup {
html! {
div.admin-section {
div.admin-section-title { "x402 price" }
form.prompt-form data-action="save-x402-price" onsubmit="return false" {
input #x402-price-input .redeem-input type="text" aria-label="x402 price per call in LH" placeholder="price per call (LH)";
div.prompt-actions {
button type="submit" .ghost { "save" }
}
}
div #x402-price-msg .admin-msg-slot {}
}
}
}
pub(crate) fn admin_app_section() -> Markup {
html! {
div.admin-section {
div.admin-section-title { "public face" }
div #public-face-status .admin-msg-slot { "what visitors see at this subdomain" }
div.public-face-picker {
button type="button" data-action="set-public-face" data-arg="directory" .ghost { "directory" }
button type="button" data-action="set-public-face" data-arg="app" .ghost { "publish app" }
button type="button" data-action="set-public-face" data-arg="html" .ghost { "publish html" }
}
div #publish-app-msg .admin-msg-slot {}
div.public-face-preview {
a href="?view=public" { "view public face →" }
}
}
}
}
pub(crate) fn admin_tool_allowlist_section() -> Markup {
html! {
div.admin-section {
div.admin-section-title { "tool allowlist" }
div #tool-allowlist-status .admin-msg-slot { "loading…" }
div.tool-allowlist-grid {
@for tool in BuiltinTool::ALL {
label.tool-checkbox-label {
input.tool-checkbox
type="checkbox"
data-tool=(tool.wire_name())
checked {}
" " (tool.wire_name())
}
}
}
div.prompt-actions {
button type="button"
data-action="save-tool-allowlist"
.ghost { "save" }
button type="button"
data-action="reset-tool-allowlist"
.ghost { "reset (all)" }
}
div #tool-allowlist-msg .admin-msg-slot {}
}
}
}
fn admin_identity_section(
name: Option<&str>,
owner_hex: Option<&str>,
tba_hex: Option<&str>,
has_wallet: bool,
) -> Markup {
html! {
div.admin-section {
@if let Some(n) = name {
div.admin-identity-row {
span.admin-identity-label { "name" }
code.admin-identity-value { (n) }
}
}
@if let Some(addr) = owner_hex {
div.admin-identity-row {
span.admin-identity-label { "owner" }
a.admin-identity-value
href=(format!("https://moderato.tempo.xyz/address/{addr}"))
target="_blank" rel="noopener"
title=(addr) {
(short_addr(addr))
}
}
} @else if has_wallet {
p.admin-blurb { "verifying…" }
} @else {
p.admin-blurb { "no identity on this device" }
div.pair-slot {
button type="button" data-action="create-identity" .ghost {
"create a new identity"
}
}
div.pair-slot {
button type="button" data-action="show-import" .ghost {
"i already have one — import seed"
}
}
div #import-slot {}
div #storage-warn-slot {}
div #identity-msg .admin-msg-slot {}
div #seed-msg .admin-msg-slot {}
p.admin-blurb {
"on mobile? "
a href="https://localharness.xyz/?adopt=1" target="_top" rel="noopener" {
"restore from your seed →"
}
}
}
@if let Some(addr) = tba_hex {
div.admin-identity-row {
span.admin-identity-label { "wallet" }
a.admin-identity-value
href=(format!("https://moderato.tempo.xyz/address/{addr}"))
target="_blank" rel="noopener"
title=(addr) {
(short_addr(addr))
}
}
}
}
}
}
pub(crate) fn admin_credits_section() -> Markup {
html! {
div #credits-section .admin-section {
div.admin-section-title { "model credits" }
div.admin-identity-row {
span.admin-identity-label { "balance" }
code #credits-balance .admin-identity-value { "…" }
}
div.redeem-row {
input #redeem-code .redeem-input type="text" aria-label="redeem code" placeholder="redeem code";
button type="button" data-action="redeem-code" .ghost { "redeem" }
}
div.redeem-row {
input #buy-usd .redeem-input type="text"
inputmode="decimal" aria-label="amount in USD" placeholder="USD amount";
button type="button" data-action="buy-lh" .ghost { "buy $LH" }
}
div #buy-msg .admin-msg-slot {}
div #credits-msg .admin-msg-slot {}
}
}
}
pub(crate) fn admin_invite_section() -> Markup {
html! {
div #invite-section .admin-section {
div.admin-section-title { "invite a friend" }
div.redeem-row {
input #invite-amount .redeem-input type="text"
inputmode="decimal" aria-label="invite amount in $LH" placeholder="$LH amount";
button type="button" data-action="create-invite" .ghost { "create" }
}
div #invite-result .admin-msg-slot {}
}
}
}
pub(crate) fn invite_result_panel(code: &str, link: &str) -> Markup {
html! {
div.invite-result-card {
div.pair-instructions { "share this link with ONE person you trust:" }
@if let Some(svg) = pair_qr_svg(link) {
div.invite-qr { (PreEscaped(svg)) }
}
div.share-line {
a.pair-url href=(link) target="_blank" rel="noopener" { (link) }
button .ghost type="button"
data-action="copy-share-url" data-arg=(link) { "copy" }
}
div.pair-code-row {
span.pair-code-label { "code" }
code.pair-code { (code) }
}
div.pair-instructions {
"the $LH is escrowed; it returns to you if the link goes unclaimed past its expiry."
}
}
}
}
pub(crate) fn admin_tba_section() -> Markup {
html! {
div #tba-section .admin-section {
div.admin-section-title { "agent wallet" }
div.admin-identity-row {
span.admin-identity-label { "address" }
code #tba-act-address .admin-identity-value { "…" }
}
div.admin-identity-row {
span.admin-identity-label { "balance" }
code #tba-act-balance .admin-identity-value { "…" }
}
div.redeem-row {
input #tba-send-recipient .redeem-input type="text"
aria-label="recipient address or agent name" placeholder="recipient (0x… or name)";
}
div.redeem-row {
input #tba-send-amount .redeem-input type="text"
inputmode="decimal" aria-label="amount in $LH" placeholder="$LH amount";
button type="button" data-action="tba-send" .ghost { "send" }
}
div #tba-send-confirm-slot {}
div #tba-send-msg .admin-msg-slot {}
}
}
}
pub(crate) fn tba_send_confirm_panel(label: &str, to_hex: &str, amount_wei: u128) -> Markup {
let amount_display = super::format_wei_as_test_eth(amount_wei);
let arg = format!("{to_hex}:{amount_wei}");
html! {
div #tba-send-confirm-panel .unlink-confirm role="dialog" aria-modal="true"
data-modal-trap data-modal-cancel="tba-send-cancel" {
div {
"send " b { (amount_display) " $LH" } " from the agent wallet to "
code { (label) } "? type the amount to confirm."
}
input #tba-send-confirm-input type="text"
inputmode="decimal" autocomplete="off"
aria-label="type the amount to confirm";
div.pair-confirm-actions {
button type="button" class="ghost" data-action="tba-send-cancel" { "cancel" }
button type="button" class="button-link" data-action="tba-send-confirm"
data-arg=(arg) { "send" }
}
}
}
}
pub(crate) fn admin_schedule_section() -> Markup {
html! {
div #schedule-section .admin-section {
div.admin-section-title { "schedule a job" }
div.redeem-row {
input #schedule-target .redeem-input type="text"
aria-label="target agent name" placeholder="target (agent name)";
}
div.redeem-row {
input #schedule-task .redeem-input type="text"
aria-label="task prompt" placeholder="task";
}
div.redeem-row {
input #schedule-interval .redeem-input type="text"
aria-label="interval" placeholder="every (e.g. 5m, 1h)";
input #schedule-budget .redeem-input type="text"
inputmode="decimal" aria-label="budget in $LH" placeholder="$LH budget";
}
div.redeem-row {
input #schedule-runs .redeem-input type="text"
inputmode="numeric" aria-label="max runs" placeholder="runs (default 100)";
button type="button" data-action="schedule-job" .ghost { "schedule" }
}
div #schedule-result .admin-msg-slot {}
div #schedule-jobs {}
}
}
}
pub(crate) fn schedule_result_panel(job_id: u64) -> Markup {
html! {
div.invite-result-card {
div.pair-instructions { "scheduled — job #" (job_id) }
div.pair-instructions {
"it fires on its cadence with no tab open; the escrowed $LH backs each run \
and the remainder refunds when you cancel or it exhausts."
}
}
}
}
pub(crate) fn admin_bounty_section() -> Markup {
html! {
div #bounty-section .admin-section {
div.admin-section-title { "post a bounty" }
div.redeem-row {
input #bounty-task .redeem-input type="text"
aria-label="bounty task" placeholder="task";
}
div.redeem-row {
input #bounty-reward .redeem-input type="text"
inputmode="decimal" aria-label="reward in $LH" placeholder="$LH reward";
input #bounty-ttl .redeem-input type="text"
inputmode="numeric" aria-label="ttl hours" placeholder="ttl hrs (default 24)";
button type="button" data-action="post-bounty" .ghost { "post" }
}
div #bounty-result .admin-msg-slot {}
div #bounty-list {}
}
}
}
pub(crate) fn bounty_result_panel(bounty_id: u64, reward_lh: &str) -> Markup {
html! {
div.invite-result-card {
div.pair-instructions { "posted — bounty #" (bounty_id) " (" (reward_lh) " $LH escrowed)" }
div.pair-instructions {
"other agents can now discover + claim it; the reward pays out when you \
accept a submitted result, and refunds if it expires unclaimed."
}
}
}
}
pub(crate) fn admin_guild_section() -> Markup {
html! {
div #guild-section .admin-section {
div.admin-section-title { "create a guild" }
div.redeem-row {
input #guild-name .redeem-input type="text"
aria-label="guild name" placeholder="guild name";
button type="button" data-action="create-guild" .ghost { "create" }
}
div #guild-result .admin-msg-slot {}
div #guild-list {}
}
}
}
pub(crate) fn guild_result_panel(guild_id: u64, name: &str) -> Markup {
html! {
div.invite-result-card {
div.pair-instructions { "created — guild #" (guild_id) " (" (name) ")" }
div.pair-instructions {
"you're its founding Admin; fund the shared treasury below and invite \
members — only Admins can spend it."
}
}
}
}
pub(crate) fn admin_governance_section() -> Markup {
html! {
div #governance-section .admin-section {
div.admin-section-title { "govern a treasury" }
div.redeem-row {
input #governance-guild .redeem-input type="text"
inputmode="numeric" aria-label="guild id"
placeholder="guild id";
button type="button" data-action="load-proposals" .ghost { "load" }
}
div.redeem-row {
input #governance-to .redeem-input type="text"
aria-label="spend recipient" placeholder="to (address or name)";
}
div.redeem-row {
input #governance-amount .redeem-input type="text"
inputmode="decimal" aria-label="amount in $LH" placeholder="$LH amount";
input #governance-period .redeem-input type="text"
inputmode="numeric" aria-label="voting period hours"
placeholder="vote hrs (default 48)";
button type="button" data-action="propose-measure" .ghost { "propose" }
}
div #governance-result .admin-msg-slot {}
div #governance-list {}
}
}
}
pub(crate) fn governance_result_panel(proposal_id: u64, amount_lh: &str) -> Markup {
html! {
div.invite-result-card {
div.pair-instructions {
"proposed — measure #" (proposal_id) " (" (amount_lh) " $LH)"
}
div.pair-instructions {
"guild members can now vote for/against; once it passes and the \
voting deadline elapses, it can be executed to pay out the treasury."
}
}
}
}
pub(crate) fn admin_notify_section() -> Markup {
html! {
div.admin-section {
div.admin-section-title { "app" }
div.pair-slot {
button type="button" data-action="install-app" .ghost {
"install app"
}
button type="button" data-action="toggle-files" .ghost {
"files"
}
}
div #install-msg .admin-msg-slot {}
div.admin-section-title { "notifications" }
div.pair-slot {
button type="button" data-action="enable-notifications" .ghost {
"enable notifications"
}
button type="button" data-action="test-notification" .ghost {
"test"
}
}
div #notify-msg .admin-msg-slot {}
}
}
}
pub(crate) fn admin_devices_section() -> Markup {
html! {
div.admin-section {
div.admin-section-title { "devices" }
div #pair-slot .pair-slot {
button #pair-btn type="button" data-action="add-device" .ghost {
"add a device"
}
}
div.pair-slot {
button type="button" data-action="sync-devices" .ghost {
"sync my devices"
}
}
div #pair-msg .admin-msg-slot {}
}
}
}
fn pair_qr_svg(url: &str) -> Option<String> {
crate::qr::qr_svg(url)
}
pub(crate) fn publish_share_fragment(name: &str) -> Markup {
let url = format!("https://{name}.localharness.xyz/");
html! {
div.share-block {
div.share-line {
span { "live at" }
a href=(url) target="_blank" rel="noopener" { (url) }
button #share-copy .ghost type="button"
data-action="copy-share-url" data-arg=(url) { "copy" }
}
@if let Some(svg) = pair_qr_svg(&url) {
div.pair-qr { (PreEscaped(svg)) }
}
}
}
}
pub(crate) fn adopt_panel(code: &str, url: &str) -> Markup {
html! {
div #pair-slot .pair-slot.pair-active {
div.pair-instructions { "scan this on your other device" }
@if let Some(svg) = pair_qr_svg(url) {
div.pair-qr { (PreEscaped(svg)) }
}
div.pair-code-row {
span.pair-code-label { "code" }
code.pair-code { (code) }
}
div.pair-waiting { "type the code on that device to decrypt + import your seed" }
button type="button" data-action="pair-cancel" .ghost { "done" }
}
}
}
pub(crate) fn adopt_join(ct_hex: &str) -> Markup {
html! {
(site_header(&Host::Apex))
main.apex-main {
div.col-chat {
section.step {
div.pair-instructions { "adopt your identity on this device" }
form.create-form data-action="adopt-device" {
input #adopt-code .create-input type="text"
aria-label="one-time adoption code"
placeholder="enter code" autocomplete="off"
autocapitalize="none" autocorrect="off"
spellcheck="false" maxlength="8" required {}
input #adopt-ct type="hidden" value=(ct_hex) {}
button type="submit" .create-button { "adopt" }
}
div #adopt-msg .step-msg {}
}
}
}
}
}
pub(crate) fn identity_choice(name: &str) -> Markup {
html! {
div #agents-list .agents-list {
div.pair-instructions { "no identity on this device yet" }
div.pair-slot {
button type="button" data-action="create-new-claim" data-arg=(name) .ghost {
"create a new identity"
}
}
div.pair-slot {
button type="button" data-action="show-import" .ghost {
"i already have one — import seed"
}
}
div #import-slot {}
div #seed-msg .admin-msg-slot {}
div.pair-waiting { "or open “add a device” on a device you already use" }
}
}
}
pub(crate) fn admin_security_collapsed() -> Markup {
html! {
div #security-slot .admin-section {
div.admin-section-title { "security" }
button type="button" data-action="reveal-security" .ghost {
"seed phrase, import, reset"
}
}
}
}
pub(crate) fn admin_security_expanded() -> Markup {
html! {
div #security-slot .admin-section {
div.admin-section-title { "security" }
div.admin-subsection {
div.admin-subsection-title { "seed phrase" }
div #seed-reveal .seed-reveal {
button type="button" data-action="reveal-seed" .ghost { "reveal" }
}
}
div.admin-subsection {
div.admin-subsection-title { "import a different seed" }
(import_seed_inline())
}
div.admin-subsection {
div.admin-subsection-title { "reset this device" }
div #reset-confirm-slot {
button type="button" data-action="reset-arm" .ghost { "reset…" }
}
}
button type="button" data-action="hide-security" .ghost { "hide" }
}
}
}
pub(crate) fn reset_confirm_inline() -> Markup {
html! {
div #reset-confirm-slot .reset-confirm role="dialog" aria-modal="true"
aria-label="confirm device reset"
data-modal-trap data-modal-cancel="reset-cancel" {
span.reset-confirm-prompt { "type RESET to clear this device — identity + names are kept" }
input #reset-confirm-text .redeem-input type="text" aria-label="type RESET to confirm" placeholder="RESET";
div.reset-confirm-actions {
button type="button" data-action="reset-confirm" .danger { "reset" }
button type="button" data-action="reset-cancel" .ghost { "cancel" }
}
div #reset-confirm-msg .admin-msg-slot {}
}
}
}
pub(crate) fn reset_armed_inline() -> Markup {
html! {
div #reset-confirm-slot {
button type="button" data-action="reset-arm" .ghost { "reset…" }
}
}
}
pub(crate) fn opfs_wipe_armed_inline() -> Markup {
html! {
span #opfs-wipe-slot {
button data-action="opfs-wipe" { "wipe" }
}
}
}
pub(crate) fn opfs_wipe_confirm_inline() -> Markup {
html! {
span #opfs-wipe-slot .opfs-wipe-confirm role="group" aria-label="confirm wipe all files" {
button data-action="opfs-wipe-confirm" .danger { "wipe?" }
button data-action="opfs-wipe-cancel" .ghost { "no" }
}
}
}
#[allow(dead_code)]
pub(crate) fn pricing_card(price_wei: u128) -> Markup {
html! {
section .pricing-card {
div.pricing-header {
div.pricing-title { "pricing" }
}
(pricing_card_body(price_wei, true))
}
}
}
#[allow(dead_code)] pub(crate) fn pricing_readonly_line(price_wei: u128) -> Markup {
let display = if price_wei == 0 {
"free".to_string()
} else {
format!("{} $LH/turn", super::format_wei_as_test_eth(price_wei))
};
html! {
div.financial-line {
span.financial-label { "pricing" }
span.financial-value { (display) }
}
}
}
pub(crate) fn financial_card(
name: &str,
tba_hex: &str,
owner_hex: &str,
lh_balance_wei: u128,
_price_wei: u128,
_is_owner: bool,
) -> Markup {
let tba_url = format!("https://moderato.tempo.xyz/address/{tba_hex}");
let owner_url = format!("https://moderato.tempo.xyz/address/{owner_hex}");
let balance_display = super::format_wei_as_test_eth(lh_balance_wei);
let tool_count = BuiltinTool::ALL.len();
html! {
section #financial-slot .financial-card {
div.financial-line {
span.financial-label { "name" }
span.financial-value { (name) }
}
div.financial-line {
span.financial-label { "owner" }
a.financial-tba href=(owner_url) target="_blank" rel="noopener"
title=(owner_hex) {
(short_addr(owner_hex))
}
}
div.financial-line {
span.financial-label { "wallet" }
a.financial-tba href=(tba_url) target="_blank" rel="noopener"
title=(tba_hex) {
(short_addr(tba_hex))
}
}
div.financial-line {
span.financial-label { "agent $LH" }
span.financial-value.financial-balance { (balance_display) }
}
div.financial-line {
span.financial-label { "tools" }
span #tools-count .financial-value { (tool_count) }
}
}
}
}
pub(crate) fn pricing_card_body(price_wei: u128, is_owner: bool) -> Markup {
let display = if price_wei == 0 {
"free".to_string()
} else {
format!("{} $localharness/turn", super::format_wei_as_test_eth(price_wei))
};
html! {
div #pricing-body .pricing-body {
div.pricing-value { (display) }
@if is_owner {
div.pricing-edit {
input #pricing-input
type="text"
inputmode="decimal"
aria-label="price per turn in $localharness"
placeholder="1.0"
value=(if price_wei == 0 { String::new() } else { super::format_wei_as_test_eth(price_wei) }) {}
span.pricing-unit { "$localharness/turn" }
button.ghost
type="button"
data-action="pricing-save" { "save" }
}
div #pricing-msg .pricing-msg {}
}
}
}
}
pub(crate) fn import_seed_inline() -> Markup {
html! {
div #import-slot .seed-import {
textarea #import-seed
aria-label="12-word recovery phrase"
placeholder="paste 12 words separated by spaces"
rows="3" {}
div.seed-import-actions {
button type="button" data-action="import-seed" { "import" }
button type="button" data-action="cancel-import" .ghost { "cancel" }
}
div #seed-msg .step-msg {}
}
}
}
pub(crate) fn agents_list(
agents: &[crate::app::registry::OwnedToken],
main_token_id: u64,
) -> Markup {
if agents.is_empty() {
return html! {
div #agents-list .agents-list .agents-empty {}
};
}
html! {
div #agents-list .agents-list {
ul.agents-rows {
@for agent in agents {
li.agent-row {
a.agent-row-line
href=(format!("https://{}.localharness.xyz/", agent.name)) {
span.agent-name { (agent.name) }
span.agent-row-spacer {}
@if main_token_id != 0 && agent.token_id == main_token_id {
span.main-badge title="primary identity" { "main" }
} @else {
span.alt-badge title="secondary identity" { "alt" }
}
}
}
}
}
}
}
}
pub(crate) fn seed_phrase(words: &str) -> Markup {
let download_href = format!(
"data:text/plain;charset=utf-8,{}",
words.replace(' ', "%20")
);
html! {
div.seed-words { (words) }
p.seed-warn {
"Save these words now — copy or download them before you switch apps. "
"Backgrounding the browser can refresh this tab and lose them; "
"they are shown once and never leave this device."
}
p.apex-fine {
button #seed-copy type="button" data-action="copy-seed" data-arg=(words)
.link-button { "copy" }
" · "
a.link-button download="localharness-seed.txt" href=(download_href) { "download" }
" · "
button type="button" data-action="hide-seed" .link-button { "hide" }
}
}
}
pub(crate) fn signer_no_identity() -> Markup {
html! {
main.apex-main {
div.col-chat {
section.apex-hero {
h2.apex-headline { "localharness signer" }
p.apex-sub {
"no identity exists on this device yet, so this signer "
"tab can't sign anything. "
a href="https://localharness.xyz/" { "go to apex" }
" to create or import one."
}
}
}
}
}
}
pub(crate) fn signer_chrome(address_hex: &str) -> Markup {
html! {
main.apex-main {
div.col-chat {
section.apex-hero {
h2.apex-headline { "localharness signer" }
p.apex-sub {
"this tab is acting as a signing service for an embedded "
"subdomain. it will sign authentication challenges from "
"any *.localharness.xyz origin using the master wallet:"
}
div.wallet-address-row {
span.wallet-label { "address" }
code .wallet-address { (address_hex) }
}
p.apex-fine {
"if you opened this manually rather than via an iframe, "
a href="https://localharness.xyz/" { "go home" }
"."
}
}
}
}
}
}
pub(crate) fn unclaimed(host: &Host, name: &str) -> Markup {
html! {
(site_header(host))
main.apex-main {
div.col-chat {
section.step.step-unclaimed {
h2.unclaimed-name { (name) ".localharness.xyz" }
p.step-msg {
"this name is open. claim it to make it the home of an agent you own."
}
button type="button" data-action="claim-on-chain" .button-link {
"claim " (name)
}
div #claim-msg .step-msg {}
}
}
}
}
}
pub(crate) fn opfs_breadcrumb(cwd: &[String]) -> Markup {
html! {
a href="#" data-action="opfs-nav" data-arg="" aria-label="root directory" { "/" }
@for i in 0..cwd.len() {
@let arg = cwd[..=i].join("/");
a href="#" data-action="opfs-nav" data-arg=(arg) { (cwd[i]) "/" }
}
}
}
pub(crate) fn opfs_list(cwd: &[String], entries: &[DirEntry]) -> Markup {
html! {
@if entries.is_empty() {
li.empty { "(empty)" }
} @else {
@for entry in entries {
@match entry.kind {
EntryKind::Directory => {
@let arg = if cwd.is_empty() {
entry.name.clone()
} else {
format!("{}/{}", cwd.join("/"), entry.name)
};
li.dir data-action="opfs-nav" data-arg=(arg)
role="button" tabindex="0"
aria-label=(format!("open folder {}", entry.name)) {
span.name { (entry.name) }
}
}
_ => {
@let lname = entry.name.to_ascii_lowercase();
@let opens_display = lname.ends_with(".html")
|| lname.ends_with(".htm")
|| lname.ends_with(".rl");
li.file {
span.name data-action="opfs-open" data-arg=(entry.name)
role="button" tabindex="0"
aria-label=(format!("open {}", entry.name)) {
(entry.name)
}
@if let Some(size) = entry.size {
span.size { (format_bytes(size)) }
}
@if opens_display {
button.file-edit
type="button"
data-action="opfs-edit"
data-arg=(entry.name)
title=(format!("edit {}", entry.name)) { "edit" }
}
button.file-delete
type="button"
data-action="opfs-delete"
data-arg=(entry.name)
aria-label=(format!("delete {}", entry.name))
title=(format!("delete {}", entry.name)) { "×" }
}
}
}
}
}
}
}
pub(crate) fn opfs_error(message: &str) -> Markup {
html! {
li.empty { "error: " (message) }
}
}
pub(crate) fn opfs_editor(display_path: &str, name: &str, text: &str) -> Markup {
html! {
div.editor {
div.editor-header {
span.editor-path { (display_path) }
div.editor-actions {
button.panel-button
type="button"
data-action="opfs-save"
data-arg=(name) { "save" }
button.panel-button
type="button"
data-action="opfs-close-viewer" { "close" }
}
}
textarea #fs-editor .editor-textarea aria-label=(format!("editing {name}")) { (text) }
}
}
}
pub(crate) fn display_surface() -> Markup {
html! {
div.display-wrap {
div.display-stage {
canvas #display-canvas .display-canvas {}
}
(broadcast_composer_closed())
}
}
}
pub(crate) fn broadcast_composer(title: &str, default_body: &str) -> Markup {
html! {
div #broadcast-composer .broadcast-composer {
div.broadcast-composer-panel {
div.broadcast-composer-title { (title) }
input #broadcast-input
type="text"
value=(default_body)
maxlength="200"
autocomplete="off"
aria-label="notification message";
div.prompt-actions {
button #broadcast-send-btn type="button"
data-action="broadcast-send" data-arg=(title) { "send" }
button type="button" data-action="broadcast-cancel"
.ghost { "cancel" }
}
}
}
}
}
pub(crate) fn broadcast_composer_closed() -> Markup {
html! { div #broadcast-composer hidden {} }
}
#[allow(clippy::too_many_arguments)] pub(crate) fn public_landing(
name: &str,
owner: Option<&str>,
tba: Option<&str>,
main_name: Option<&str>,
is_main: bool,
siblings: &[crate::app::registry::OwnedToken],
personas: &[Option<String>],
owner_overlay: bool,
) -> Markup {
html! {
div.public-face {
@if owner_overlay {
a.app-edit href="?edit=1" title="back to your studio" { "studio" }
}
header.public-hero {
h1.public-title { (name) }
p.public-tagline {
"agent on localharness"
@if is_main { " · " span.main-badge title="primary identity" { "main" } }
}
}
div.public-meta {
@if let Some(addr) = owner {
div.public-meta-row {
span.public-meta-label { "owner" }
@if let Some(m) = main_name {
a.public-meta-value
href=(format!("https://{m}.localharness.xyz/"))
title=(addr) { (m) }
} @else {
a.public-meta-value
href=(format!("https://moderato.tempo.xyz/address/{addr}"))
target="_blank" rel="noopener" title=(addr) { (short_addr(addr)) }
}
}
}
@if let Some(t) = tba {
div.public-meta-row {
span.public-meta-label { "wallet" }
a.public-meta-value
href=(format!("https://moderato.tempo.xyz/address/{t}"))
target="_blank" rel="noopener" title=(t) { (short_addr(t)) }
}
}
}
@if !siblings.is_empty() {
section.public-directory {
h2.public-section-title { "more agents by this owner" }
ul.agents-rows {
@for (i, s) in siblings.iter().enumerate() {
@let preview = personas.get(i).and_then(|p| p.as_deref());
li.agent-row {
a.agent-card
href=(format!("https://{}.localharness.xyz/", s.name)) {
span.agent-name { (s.name) }
@if let Some(p) = preview {
span.agent-preview { (truncate_preview(p, 80)) }
}
}
}
}
}
}
}
footer.public-footer {
a href="https://localharness.xyz/" title="localharness" { "localharness" }
}
}
}
}
pub(crate) fn app_fullscreen(owner_overlay: bool) -> Markup {
html! {
(public_face_header(owner_overlay))
div.app-fullscreen {
div.app-stage {
canvas #display-canvas .display-canvas {}
}
(broadcast_composer_closed())
}
}
}
pub(crate) fn public_face_header(owner_overlay: bool) -> Markup {
html! {
header.site-header.public-face-header {
div.header-inner {
h1.header-brand {
details.brand-menu {
summary.brand-summary { "localharness" }
nav.brand-menu-items {
a href="https://localharness.xyz/" { "home" }
a href="https://github.com/compusophy/localharness"
target="_blank" rel="noopener" { "repo" }
a href="https://crates.io/crates/localharness"
target="_blank" rel="noopener" { "crate" }
}
}
}
(notif_bell())
@if owner_overlay {
a.app-edit href="?edit=1" title="back to your studio" { "studio" }
}
}
}
}
}
pub(crate) fn notif_bell() -> Markup {
let bell = maud::PreEscaped(
"<svg viewBox=\"0 0 16 16\" width=\"15\" height=\"15\" fill=\"none\" \
stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\" \
stroke-linejoin=\"round\" aria-hidden=\"true\">\
<path d=\"M8 2.2a3 3 0 0 0-3 3c0 3.2-1.4 4.3-1.4 4.3h8.8S11 8.4 11 5.2a3 3 0 0 0-3-3z\"/>\
<path d=\"M6.6 12.1a1.5 1.5 0 0 0 2.8 0\"/></svg>",
);
html! {
div.notif-bell-wrap {
button #notif-bell type="button" data-action="notif-bell"
title="notifications" aria-label="notifications" .header-button.notif-bell-btn {
(bell)
span #notif-bell-badge .notif-badge hidden {}
}
(notif_list_panel(&[], None, true))
}
}
}
pub(crate) fn notif_list_panel(
items: &[(String, String)],
note: Option<&str>,
hidden: bool,
) -> Markup {
html! {
div #notif-bell-panel .notif-panel hidden[hidden] {
@if let Some(n) = note {
div.notif-panel-empty { (n) }
}
@if items.is_empty() {
@if note.is_none() {
div.notif-panel-empty { "no notifications yet" }
}
} @else {
@for (title, body) in items {
div.notif-item {
div.notif-item-title { (title) }
div.notif-item-body { (body) }
}
}
}
}
}
}
fn format_bytes(n: u64) -> String {
if n < 1024 {
format!("{n} B")
} else if n < 1024 * 1024 {
format!("{:.1} KB", n as f64 / 1024.0)
} else {
format!("{:.1} MB", n as f64 / (1024.0 * 1024.0))
}
}
pub(crate) fn keymeta(key: &str) -> Markup {
let n = key.len();
if n == 0 {
return html! {};
}
let looks_right = (30..=60).contains(&n)
&& key
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-');
let suffix = if looks_right { "" } else { " - check" };
html! {
span style=(if looks_right { "" } else { "color: var(--error)" }) {
"(" (n) " chars" (suffix) ")"
}
}
}