Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions crates/openfang-api/src/webchat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

use axum::http::header;
use axum::response::IntoResponse;
use std::sync::LazyLock;

/// Nonce placeholder in compile-time HTML, replaced at request time.
const NONCE_PLACEHOLDER: &str = "__NONCE__";
Expand Down Expand Up @@ -120,7 +121,7 @@ pub async fn webchat_page() -> impl IntoResponse {
/// All vendor libraries (Alpine.js, marked.js, highlight.js) are bundled
/// locally — no CDN dependency. Alpine.js is included LAST because it
/// immediately processes x-data directives and fires alpine:init on load.
const WEBCHAT_HTML: &str = concat!(
const WEBCHAT_HTML_BASE: &str = concat!(
include_str!("../static/index_head.html"),
"<style>\n",
include_str!("../static/css/theme.css"),
Expand All @@ -131,7 +132,10 @@ const WEBCHAT_HTML: &str = concat!(
"\n",
include_str!("../static/vendor/github-dark.min.css"),
"\n</style>\n",
include_str!("../static/index_body.html"),
include_str!("../static/index_body.html")
);

const WEBCHAT_SCRIPTS: &str = concat!(
// Vendor libs: marked + highlight first (used by app.js), then Chart.js
"<script nonce=\"__NONCE__\">\n",
include_str!("../static/vendor/marked.min.js"),
Expand All @@ -144,6 +148,8 @@ const WEBCHAT_HTML: &str = concat!(
"\n</script>\n",
// App code
"<script nonce=\"__NONCE__\">\n",
include_str!("../static/js/i18n.js"),
"\n",
include_str!("../static/js/api.js"),
"\n",
include_str!("../static/js/app.js"),
Expand Down Expand Up @@ -187,6 +193,20 @@ const WEBCHAT_HTML: &str = concat!(
// Alpine.js MUST be last — it processes x-data and fires alpine:init
"<script nonce=\"__NONCE__\">\n",
include_str!("../static/vendor/alpine.min.js"),
"\n</script>\n",
"</body></html>"
"\n</script>\n"
);

static WEBCHAT_HTML: LazyLock<String> = LazyLock::new(|| {
if let Some(body_close_idx) = WEBCHAT_HTML_BASE.rfind("</body>") {
let mut assembled = String::with_capacity(WEBCHAT_HTML_BASE.len() + WEBCHAT_SCRIPTS.len());
assembled.push_str(&WEBCHAT_HTML_BASE[..body_close_idx]);
assembled.push_str(WEBCHAT_SCRIPTS);
assembled.push_str(&WEBCHAT_HTML_BASE[body_close_idx..]);
assembled
} else {
let mut assembled = String::with_capacity(WEBCHAT_HTML_BASE.len() + WEBCHAT_SCRIPTS.len());
assembled.push_str(WEBCHAT_HTML_BASE);
assembled.push_str(WEBCHAT_SCRIPTS);
assembled
}
});
10 changes: 9 additions & 1 deletion crates/openfang-api/static/css/components.css
Original file line number Diff line number Diff line change
Expand Up @@ -1256,12 +1256,14 @@ mark.search-highlight {
font-weight: 500;
}

/* Theme switcher — 3-mode pill (Light / System / Dark) */
/* Theme switcher — theme + locale */
.theme-switcher {
display: inline-flex;
border-radius: var(--radius-sm);
border: 1px solid var(--border);
overflow: hidden;
flex: 0 0 auto;
margin-left: auto;
}
.theme-opt {
cursor: pointer;
Expand All @@ -1275,6 +1277,12 @@ mark.search-highlight {
}
.theme-opt:hover { color: var(--text-primary); background: var(--bg-hover); }
.theme-opt.active { color: var(--accent); background: var(--accent-glow); }
.theme-opt.locale-opt {
min-width: 34px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.02em;
}

/* Utility */
.flex { display: flex; }
Expand Down
6 changes: 6 additions & 0 deletions crates/openfang-api/static/css/layout.css
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,15 @@
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 8px;
min-height: 60px;
}

.sidebar-header-text {
min-width: 0;
}

.sidebar-logo {
display: flex;
align-items: center;
Expand Down
39 changes: 21 additions & 18 deletions crates/openfang-api/static/index_body.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ <h1>OPENFANG</h1>
</div>
<div class="theme-switcher">
<button class="theme-opt" :class="{ active: themeMode === 'light' }" @click="setTheme('light')" title="Light">&#9788;</button>
<button class="theme-opt" :class="{ active: themeMode === 'system' }" @click="setTheme('system')" title="System">&#9675;</button>
<button class="theme-opt" :class="{ active: themeMode === 'system' }" @click="setTheme('system')" title="Follow System">&#9675;</button>
<button class="theme-opt" :class="{ active: themeMode === 'dark' }" @click="setTheme('dark')" title="Dark">&#9790;</button>
<button class="theme-opt locale-opt" @click="toggleLocale()" title="Language" x-text="locale === 'zh-CN' ? '中文' : 'EN'">EN</button>
</div>
</div>

Expand Down Expand Up @@ -485,7 +486,7 @@ <h3>Welcome to OpenFang</h3>
<div class="text-xs text-dim truncate" style="margin-top:2px;max-width:300px" x-show="e.detail" x-text="e.detail" :title="e.detail"></div>
</div>
</div>
<span class="text-xs text-dim font-mono" style="white-space:nowrap;flex-shrink:0" x-text="timeAgo(e.timestamp)" :title="new Date(e.timestamp).toLocaleString()"></span>
<span class="text-xs text-dim font-mono" style="white-space:nowrap;flex-shrink:0" x-text="timeAgo(e.timestamp)" :title="formatUiDateTime(e.timestamp, locale)"></span>
</div>
</template>
</div>
Expand Down Expand Up @@ -957,7 +958,7 @@ <h3>
</span>
</template>
</div>
<div class="detail-row"><span class="detail-label">Created</span><span class="detail-value" x-text="detailAgent.created_at ? new Date(detailAgent.created_at).toLocaleString() : '-'"></span></div>
<div class="detail-row"><span class="detail-label">Created</span><span class="detail-value" x-text="formatUiDateTime(detailAgent.created_at, locale)"></span></div>

<!-- Fallback Model Chain -->
<div class="detail-row" style="align-items:flex-start">
Expand Down Expand Up @@ -1241,7 +1242,7 @@ <h3>Create Agent</h3>
<div class="card" style="margin-bottom:16px">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px">
<div style="width:44px;height:44px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:22px" :style="'background:' + spawnIdentity.color + '22; border:2px solid ' + spawnIdentity.color">
<span x-text="spawnIdentity.emoji || '\u{1F916}'"></span>
<span x-text="spawnIdentity.emoji || '\uD83E\uDD16'"></span>
</div>
<div>
<div class="font-bold" style="font-size:15px" x-text="spawnForm.name || 'Unnamed'"></div>
Expand Down Expand Up @@ -1370,7 +1371,7 @@ <h2>Workflows</h2>
<tr>
<td><span class="font-bold" x-text="wf.name"></span><br><span class="text-xs text-dim" x-text="wf.description"></span></td>
<td x-text="Array.isArray(wf.steps) ? wf.steps.length + ' step' + (wf.steps.length !== 1 ? 's' : '') : wf.steps"></td>
<td class="text-xs" x-text="new Date(wf.created_at).toLocaleDateString()"></td>
<td class="text-xs" x-text="formatUiDate(wf.created_at, locale)"></td>
<td>
<button class="btn btn-primary btn-sm" @click="showRunModal(wf)">Run</button>
<button class="btn btn-ghost btn-sm" @click="showEditModal(wf)">Edit</button>
Expand Down Expand Up @@ -1400,7 +1401,7 @@ <h3>No workflows yet</h3>
<div class="form-group"><label>Description</label><input class="form-input" x-model="newWf.description" placeholder="What does this workflow do?"></div>
<div class="mb-4">
<div class="form-group" style="margin:0"><label>Steps</label></div>
<div class="text-xs text-dim mb-2">Each step runs an agent. Use <code style="color:var(--accent)">{{input}}</code> in prompts to pass the previous step's output.</div>
<div class="text-xs text-dim mb-2">Each step runs an agent. Use <code style="color:var(--accent)">&#123;&#123;input&#125;&#125;</code> in prompts to pass the previous step's output.</div>
<template x-for="(step, i) in newWf.steps" :key="i">
<div class="card mt-2" style="padding:10px">
<div class="flex gap-2 items-center">
Expand All @@ -1415,7 +1416,7 @@ <h3>No workflows yet</h3>
</select>
<button class="btn btn-danger btn-sm" @click="newWf.steps.splice(i,1)">&times;</button>
</div>
<input class="form-input mt-2" x-model="step.prompt" placeholder="Prompt template (use {{input}})">
<input class="form-input mt-2" x-model="step.prompt" placeholder="Prompt template (use &#123;&#123;input&#125;&#125;)">
</div>
</template>
<button class="btn btn-ghost btn-sm mt-2" @click="newWf.steps.push({name:'',agent_name:'',mode:'sequential',prompt:'{{input}}'})">+ Add Step</button>
Expand Down Expand Up @@ -1573,7 +1574,7 @@ <h3>No workflows yet</h3>
</div>
<div class="form-group">
<label class="text-xs">Prompt Template</label>
<textarea class="form-textarea" x-model="selectedNode.config.prompt" @input="applyNodeEdit()" style="font-size:11px;min-height:60px" placeholder="{{input}}"></textarea>
<textarea class="form-textarea" x-model="selectedNode.config.prompt" @input="applyNodeEdit()" style="font-size:11px;min-height:60px" placeholder="&#123;&#123;input&#125;&#125;"></textarea>
</div>
<div class="form-group">
<label class="text-xs">Model (optional)</label>
Expand Down Expand Up @@ -1865,7 +1866,7 @@ <h3>Create Scheduled Job</h3>
<td>
<div class="toggle" :class="{ active: t.enabled }" @click="toggleTrigger(t)"></div>
</td>
<td class="text-xs" x-text="new Date(t.created_at).toLocaleDateString()"></td>
<td class="text-xs" x-text="formatUiDate(t.created_at, locale)"></td>
<td>
<button class="btn btn-danger btn-sm" @click="deleteTrigger(t)">Delete</button>
</td>
Expand Down Expand Up @@ -2440,7 +2441,7 @@ <h4>Quick Start Skills</h4>
</div>
<div class="card-meta" x-text="qs.description"></div>
<div class="flex justify-end mt-2">
<button class="btn btn-ghost btn-sm" @click="createDemoSkill(qs)" :disabled="isSkillInstalledByName(qs.name)" x-text="isSkillInstalledByName(qs.name) ? 'Created' : 'Create Skill'"></button>
<button class="btn btn-ghost btn-sm" @click="createDemoSkill(qs)" :disabled="isSkillInstalledByName(qs.name)" x-text="isSkillInstalledByName(qs.name) ? 'Created Skill' : 'Create Skill'"></button>
</div>
</div>
</template>
Expand Down Expand Up @@ -2602,7 +2603,7 @@ <h3>No hands available</h3>
</div>
<span class="badge" :class="{ 'badge-success': inst.status === 'Active', 'badge-dim': inst.status === 'Paused', 'badge-warn': inst.status && inst.status.startsWith('Error'), 'badge-info': inst.status === 'Inactive' }" x-text="inst.status"></span>
</div>
<div class="text-xs text-dim" x-text="'Activated: ' + new Date(inst.activated_at).toLocaleString()"></div>
<div class="text-xs text-dim"><span>Activated:</span> <span x-text="formatUiDateTime(inst.activated_at, locale)"></span></div>
<div class="text-xs text-dim" x-show="inst.agent_id" x-text="'Agent: ' + inst.agent_id"></div>

<!-- Stats (loaded on demand) -->
Expand Down Expand Up @@ -3160,9 +3161,9 @@ <h3><span x-text="detailHand.icon"></span> <span x-text="detailHand.name"></span
<td class="text-dim" x-text="trade.date"></td>
<td style="font-weight:600" x-text="trade.ticker"></td>
<td><span class="trade-side-badge" :class="trade.side === 'BUY' ? 'trade-buy' : 'trade-sell'" x-text="trade.side"></span></td>
<td x-text="'$' + Number(trade.price || 0).toLocaleString(undefined, {minimumFractionDigits:2, maximumFractionDigits:2})"></td>
<td x-text="'$' + formatUiNumber(trade.price || 0, locale, {minimumFractionDigits:2, maximumFractionDigits:2})"></td>
<td x-text="trade.qty"></td>
<td :class="(trade.pnl || 0) >= 0 ? 'heatmap-positive' : 'heatmap-negative'" x-text="(trade.pnl >= 0 ? '+$' : '-$') + Math.abs(trade.pnl || 0).toLocaleString(undefined, {minimumFractionDigits:2, maximumFractionDigits:2})"></td>
<td :class="(trade.pnl || 0) >= 0 ? 'heatmap-positive' : 'heatmap-negative'" x-text="(trade.pnl >= 0 ? '+$' : '-$') + formatUiNumber(Math.abs(trade.pnl || 0), locale, {minimumFractionDigits:2, maximumFractionDigits:2})"></td>
</tr>
</template>
</tbody>
Expand Down Expand Up @@ -3666,7 +3667,7 @@ <h4>Peer Networking (OFP)</h4>
</div>
<div class="empty-state" x-show="!peers.length">
<h4>No peers connected</h4>
<p class="hint">Add a <code style="color:var(--accent-light);background:var(--bg);padding:1px 4px;border-radius:2px">[network]</code> section to config.toml with <code style="color:var(--accent-light);background:var(--bg);padding:1px 4px;border-radius:2px">shared_secret</code> and peer addresses.</p>
<p class="hint"><code style="color:var(--accent-light);background:var(--bg);padding:1px 4px;border-radius:2px">[network]</code> 配置段中设置 <code style="color:var(--accent-light);background:var(--bg);padding:1px 4px;border-radius:2px">shared_secret</code> 和节点地址。</p>
</div>
</div>

Expand Down Expand Up @@ -4002,7 +4003,7 @@ <h4 style="margin-top:16px;margin-bottom:8px">Top Spenders (Today)</h4>
<template x-for="a in byAgent" :key="a.agent_id">
<tr>
<td class="font-bold" x-text="a.name"></td>
<td x-text="a.total_tokens ? a.total_tokens.toLocaleString() : '0'"></td>
<td x-text="formatUiNumber(a.total_tokens || 0, locale)"></td>
<td x-text="a.tool_calls || 0"></td>
</tr>
</template>
Expand Down Expand Up @@ -4211,7 +4212,7 @@ <h2>Sessions</h2>
<td class="text-xs truncate" style="font-family:monospace;max-width:120px" x-text="s.session_id ? s.session_id.substring(0, 8) + '...' : '-'" :title="s.session_id"></td>
<td class="font-bold" x-text="s.agent_name || s.agent_id"></td>
<td x-text="s.message_count"></td>
<td class="text-xs" x-text="s.created_at ? new Date(s.created_at).toLocaleString() : '-'"></td>
<td class="text-xs" x-text="formatUiDateTime(s.created_at, locale)"></td>
<td>
<button class="btn btn-primary btn-sm" @click="openInChat(s)">Chat</button>
<button class="btn btn-danger btn-sm" @click="deleteSession(s.session_id)">Delete</button>
Expand Down Expand Up @@ -4361,7 +4362,7 @@ <h2>Logs</h2>
<div class="card" style="font-family:monospace;max-height:70vh;overflow-y:auto" id="log-container" @mouseenter="hovering = true" @mouseleave="hovering = false">
<template x-for="entry in filteredEntries" :key="entry.seq">
<div class="log-entry">
<span class="log-timestamp" x-text="new Date(entry.timestamp).toLocaleTimeString()"></span>
<span class="log-timestamp" x-text="formatUiTime(entry.timestamp, locale)"></span>
<span class="log-level" :class="'log-level-' + classifyLevel(entry.action)" x-text="classifyLevel(entry.action).toUpperCase()"></span>
<span class="text-xs" style="color:var(--text-dim);margin-right:6px" x-text="'[' + entry.action + ']'"></span>
<span class="text-xs" x-text="entry.detail"></span>
Expand Down Expand Up @@ -4413,7 +4414,7 @@ <h4>No log entries yet</h4>
<template x-for="e in filteredAuditEntries" :key="e.seq">
<tr>
<td x-text="e.seq"></td>
<td class="text-xs" style="white-space:nowrap" x-text="new Date(e.timestamp).toLocaleString()"></td>
<td class="text-xs" style="white-space:nowrap" x-text="formatUiDateTime(e.timestamp, locale)"></td>
<td class="truncate" style="max-width:120px" x-text="auditAgentName(e.agent_id)" :title="e.agent_id"></td>
<td><span class="badge badge-created" x-text="friendlyAction(e.action)"></span></td>
<td class="truncate" style="max-width:200px" x-text="e.detail" :title="e.detail"></td>
Expand Down Expand Up @@ -5061,3 +5062,5 @@ <h3 style="font-size:20px;font-weight:700;margin-bottom:8px;color:var(--accent)"
<div id="toast-container" class="toast-container" aria-live="polite"></div>

<script>if('serviceWorker' in navigator){navigator.serviceWorker.register('/sw.js').catch(function(){});}</script>
</body>
</html>
Loading