freddie 0.0.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +180 -0
- package/CHANGELOG.md +32 -0
- package/README.md +130 -0
- package/bin/freddie.js +116 -0
- package/package.json +59 -0
- package/skills/creative/README.md +3 -0
- package/skills/creative/architecture-diagram/SKILL.md +52 -0
- package/skills/creative/ascii-video/SKILL.md +60 -0
- package/skills/creative/concept-diagrams/SKILL.md +65 -0
- package/skills/data/README.md +3 -0
- package/skills/data/etl-pipelines/SKILL.md +60 -0
- package/skills/data/sql-explainer/SKILL.md +60 -0
- package/skills/ops/README.md +3 -0
- package/skills/ops/incident-response/SKILL.md +74 -0
- package/skills/ops/log-triage/SKILL.md +79 -0
- package/skills/planning/README.md +3 -0
- package/skills/planning/okr-drafter/SKILL.md +60 -0
- package/skills/planning/weekly-review/SKILL.md +64 -0
- package/skills/software-development/README.md +3 -0
- package/skills/software-development/code-review/SKILL.md +70 -0
- package/skills/software-development/rfc-writer/SKILL.md +68 -0
- package/skills/software-development/systematic-debugging/SKILL.md +80 -0
- package/src/acp/auth.js +21 -0
- package/src/acp/entry.js +2 -0
- package/src/acp/events.js +10 -0
- package/src/acp/main.js +8 -0
- package/src/acp/permissions.js +29 -0
- package/src/acp/server.js +84 -0
- package/src/acp/session.js +26 -0
- package/src/acp/tools.js +17 -0
- package/src/agent/account_usage.js +19 -0
- package/src/agent/acptoapi-bridge.js +80 -0
- package/src/agent/anthropic_adapter.js +10 -0
- package/src/agent/auxiliary_client.js +20 -0
- package/src/agent/bedrock_adapter.js +11 -0
- package/src/agent/codex_responses_adapter.js +10 -0
- package/src/agent/compress/compressor.js +55 -0
- package/src/agent/compress/fallback.js +14 -0
- package/src/agent/compress/index.js +6 -0
- package/src/agent/compress/policy.js +47 -0
- package/src/agent/compress/prompt.js +46 -0
- package/src/agent/compress/prune.js +16 -0
- package/src/agent/compress/tokens.js +31 -0
- package/src/agent/context_references.js +40 -0
- package/src/agent/copilot_acp_client.js +6 -0
- package/src/agent/credential_pool.js +30 -0
- package/src/agent/credential_sources.js +18 -0
- package/src/agent/curator.js +5 -0
- package/src/agent/display.js +23 -0
- package/src/agent/error_classifier.js +15 -0
- package/src/agent/file_safety.js +9 -0
- package/src/agent/gemini_cloudcode_adapter.js +9 -0
- package/src/agent/gemini_native_adapter.js +11 -0
- package/src/agent/gemini_schema.js +19 -0
- package/src/agent/google_code_assist.js +8 -0
- package/src/agent/google_oauth.js +21 -0
- package/src/agent/image_gen_provider.js +8 -0
- package/src/agent/image_gen_registry.js +6 -0
- package/src/agent/image_routing.js +13 -0
- package/src/agent/insights.js +9 -0
- package/src/agent/llm_resolver.js +21 -0
- package/src/agent/lmstudio_reasoning.js +13 -0
- package/src/agent/machine.js +102 -0
- package/src/agent/manual_compression_feedback.js +5 -0
- package/src/agent/memory_manager.js +14 -0
- package/src/agent/memory_provider.js +1 -0
- package/src/agent/model_metadata.js +28 -0
- package/src/agent/models_dev.js +13 -0
- package/src/agent/moonshot_schema.js +11 -0
- package/src/agent/oauth_endpoints.js +79 -0
- package/src/agent/onboarding.js +16 -0
- package/src/agent/pi-bridge.js +37 -0
- package/src/agent/prompt_builder.js +12 -0
- package/src/agent/prompt_caching.js +24 -0
- package/src/agent/rate_limit_tracker.js +12 -0
- package/src/agent/redact.js +25 -0
- package/src/agent/retry_utils.js +17 -0
- package/src/agent/shell_hooks.js +16 -0
- package/src/agent/skill_commands.js +16 -0
- package/src/agent/skill_preprocessing.js +12 -0
- package/src/agent/skill_utils.js +14 -0
- package/src/agent/subdirectory_hints.js +17 -0
- package/src/agent/title_generator.js +13 -0
- package/src/agent/trajectory.js +9 -0
- package/src/agent/usage_pricing.js +16 -0
- package/src/auth.js +84 -0
- package/src/batch.js +32 -0
- package/src/cli/auth_commands.js +17 -0
- package/src/cli/azure_detect.js +9 -0
- package/src/cli/backup.js +17 -0
- package/src/cli/banner.js +13 -0
- package/src/cli/browser_connect.js +11 -0
- package/src/cli/callbacks.js +5 -0
- package/src/cli/claw.js +8 -0
- package/src/cli/cli_output.js +19 -0
- package/src/cli/clipboard.js +24 -0
- package/src/cli/codex_models.js +8 -0
- package/src/cli/colors.js +13 -0
- package/src/cli/completer.js +98 -0
- package/src/cli/completion.js +21 -0
- package/src/cli/copilot_auth.js +9 -0
- package/src/cli/curator_cli.js +5 -0
- package/src/cli/curses.js +15 -0
- package/src/cli/debug.js +6 -0
- package/src/cli/default_soul.js +20 -0
- package/src/cli/dingtalk_auth.js +12 -0
- package/src/cli/doctor.js +15 -0
- package/src/cli/dump.js +11 -0
- package/src/cli/env_loader.js +25 -0
- package/src/cli/fallback_cmd.js +9 -0
- package/src/cli/gateway_cli.js +17 -0
- package/src/cli/hooks.js +9 -0
- package/src/cli/interactive.js +61 -0
- package/src/cli/logs.js +32 -0
- package/src/cli/main.js +7 -0
- package/src/cli/mcp_config.js +9 -0
- package/src/cli/memory_setup.js +12 -0
- package/src/cli/model_catalog.js +23 -0
- package/src/cli/model_normalize.js +12 -0
- package/src/cli/model_switch.js +11 -0
- package/src/cli/models.js +13 -0
- package/src/cli/nous_subscription.js +12 -0
- package/src/cli/oneshot.js +6 -0
- package/src/cli/pairing.js +21 -0
- package/src/cli/platforms.js +14 -0
- package/src/cli/plugins.js +4 -0
- package/src/cli/plugins_cmd.js +21 -0
- package/src/cli/profiles_cli.js +6 -0
- package/src/cli/providers.js +18 -0
- package/src/cli/pty_bridge.js +16 -0
- package/src/cli/relaunch.js +7 -0
- package/src/cli/runtime_provider.js +9 -0
- package/src/cli/setup.js +131 -0
- package/src/cli/skills_config.js +6 -0
- package/src/cli/skills_hub.js +8 -0
- package/src/cli/slack_cli.js +17 -0
- package/src/cli/status.js +10 -0
- package/src/cli/timeouts.js +5 -0
- package/src/cli/tips.js +14 -0
- package/src/cli/tools_config.js +15 -0
- package/src/cli/uninstall.js +8 -0
- package/src/cli/vercel_auth.js +13 -0
- package/src/cli/voice.js +6 -0
- package/src/cli/web_server.js +13 -0
- package/src/cli/webhook.js +12 -0
- package/src/commands/profile.js +72 -0
- package/src/commands/registry.js +94 -0
- package/src/config.js +125 -0
- package/src/context/engine.js +42 -0
- package/src/cron/cron-parse.js +27 -0
- package/src/cron/scheduler.js +63 -0
- package/src/db.js +178 -0
- package/src/gateway/base.js +13 -0
- package/src/gateway/builtin_hooks/boot.js +5 -0
- package/src/gateway/builtin_hooks/broadcast.js +3 -0
- package/src/gateway/builtin_hooks/deny.js +6 -0
- package/src/gateway/builtin_hooks/index.js +17 -0
- package/src/gateway/builtin_hooks/presence.js +4 -0
- package/src/gateway/builtin_hooks/routing.js +7 -0
- package/src/gateway/helpers.js +27 -0
- package/src/gateway/platforms/api_server.js +21 -0
- package/src/gateway/platforms/bluebubbles.js +32 -0
- package/src/gateway/platforms/dingtalk.js +32 -0
- package/src/gateway/platforms/discord.js +24 -0
- package/src/gateway/platforms/email.js +51 -0
- package/src/gateway/platforms/feishu.js +32 -0
- package/src/gateway/platforms/feishu_comment.js +12 -0
- package/src/gateway/platforms/feishu_comment_rules.js +11 -0
- package/src/gateway/platforms/homeassistant.js +32 -0
- package/src/gateway/platforms/matrix.js +40 -0
- package/src/gateway/platforms/mattermost.js +29 -0
- package/src/gateway/platforms/qqbot.js +32 -0
- package/src/gateway/platforms/signal.js +33 -0
- package/src/gateway/platforms/slack.js +34 -0
- package/src/gateway/platforms/sms.js +34 -0
- package/src/gateway/platforms/telegram.js +38 -0
- package/src/gateway/platforms/telegram_network.js +17 -0
- package/src/gateway/platforms/webhook.js +19 -0
- package/src/gateway/platforms/wecom.js +32 -0
- package/src/gateway/platforms/wecom_callback.js +15 -0
- package/src/gateway/platforms/wecom_crypto.js +16 -0
- package/src/gateway/platforms/weixin.js +32 -0
- package/src/gateway/platforms/whatsapp.js +40 -0
- package/src/gateway/platforms/yuanbao.js +9 -0
- package/src/gateway/platforms/yuanbao_media.js +5 -0
- package/src/gateway/platforms/yuanbao_proto.js +9 -0
- package/src/gateway/platforms/yuanbao_sticker.js +6 -0
- package/src/gateway/run.js +42 -0
- package/src/gateway/service.js +143 -0
- package/src/home.js +44 -0
- package/src/index.js +47 -0
- package/src/mcp/server.js +49 -0
- package/src/observability/debug.js +31 -0
- package/src/observability/log.js +38 -0
- package/src/plugins/achievements/index.js +9 -0
- package/src/plugins/cockpit/index.js +8 -0
- package/src/plugins/context_engine/index.js +13 -0
- package/src/plugins/disk_cleanup/index.js +22 -0
- package/src/plugins/google_meet/index.js +19 -0
- package/src/plugins/image_gen/index.js +5 -0
- package/src/plugins/manager.js +66 -0
- package/src/plugins/memory/_index.js +8 -0
- package/src/plugins/memory/byterover.js +25 -0
- package/src/plugins/memory/hindsight.js +25 -0
- package/src/plugins/memory/holographic.js +31 -0
- package/src/plugins/memory/honcho.js +25 -0
- package/src/plugins/memory/mem0.js +25 -0
- package/src/plugins/memory/openviking.js +25 -0
- package/src/plugins/memory/provider.js +35 -0
- package/src/plugins/memory/retaindb.js +25 -0
- package/src/plugins/memory/supermemory.js +25 -0
- package/src/plugins/observability/index.js +18 -0
- package/src/plugins/platforms/index.js +20 -0
- package/src/plugins/spotify/index.js +22 -0
- package/src/rl/atropos.js +22 -0
- package/src/rl/cli.js +18 -0
- package/src/sessions.js +84 -0
- package/src/skills/index.js +49 -0
- package/src/skin/engine.js +81 -0
- package/src/swe/runner.js +26 -0
- package/src/time.js +25 -0
- package/src/tools/ansi_strip.js +8 -0
- package/src/tools/approval.js +15 -0
- package/src/tools/bash.js +35 -0
- package/src/tools/binary_extensions.js +22 -0
- package/src/tools/browser.js +48 -0
- package/src/tools/budget_config.js +13 -0
- package/src/tools/checkpoint.js +29 -0
- package/src/tools/clarify.js +15 -0
- package/src/tools/code_execution.js +27 -0
- package/src/tools/credential_files.js +16 -0
- package/src/tools/cronjob.js +16 -0
- package/src/tools/debug_helpers.js +9 -0
- package/src/tools/delegate.js +28 -0
- package/src/tools/discord_tool.js +13 -0
- package/src/tools/edit.js +31 -0
- package/src/tools/env_passthrough.js +15 -0
- package/src/tools/environments/base.js +26 -0
- package/src/tools/environments/daytona.js +48 -0
- package/src/tools/environments/docker.js +14 -0
- package/src/tools/environments/file_sync.js +60 -0
- package/src/tools/environments/index.js +36 -0
- package/src/tools/environments/local.js +31 -0
- package/src/tools/environments/modal.js +33 -0
- package/src/tools/environments/singularity.js +38 -0
- package/src/tools/environments/ssh.js +14 -0
- package/src/tools/environments/vercel_sandbox.js +47 -0
- package/src/tools/feishu_doc.js +15 -0
- package/src/tools/feishu_drive.js +14 -0
- package/src/tools/file_operations.js +17 -0
- package/src/tools/file_state.js +16 -0
- package/src/tools/file_tools.js +23 -0
- package/src/tools/fuzzy_match.js +8 -0
- package/src/tools/grep.js +51 -0
- package/src/tools/homeassistant_tool.js +15 -0
- package/src/tools/image_gen.js +33 -0
- package/src/tools/interrupt.js +18 -0
- package/src/tools/managed_tool_gateway.js +11 -0
- package/src/tools/mcp_oauth.js +21 -0
- package/src/tools/mcp_oauth_manager.js +20 -0
- package/src/tools/mcp_tool.js +36 -0
- package/src/tools/memory.js +66 -0
- package/src/tools/mixture_of_agents.js +14 -0
- package/src/tools/neutts_synth.js +13 -0
- package/src/tools/openrouter_client.js +13 -0
- package/src/tools/osv_check.js +11 -0
- package/src/tools/patch_parser.js +42 -0
- package/src/tools/path_security.js +16 -0
- package/src/tools/process_registry.js +17 -0
- package/src/tools/read.js +26 -0
- package/src/tools/registry.js +54 -0
- package/src/tools/rl_training.js +13 -0
- package/src/tools/schema_sanitizer.js +18 -0
- package/src/tools/send_message.js +32 -0
- package/src/tools/session_search.js +23 -0
- package/src/tools/skill_manager.js +17 -0
- package/src/tools/skill_usage.js +20 -0
- package/src/tools/skills_guard.js +17 -0
- package/src/tools/skills_hub.js +31 -0
- package/src/tools/skills_index.js +14 -0
- package/src/tools/skills_sync.js +19 -0
- package/src/tools/skills_tool.js +11 -0
- package/src/tools/slash_confirm.js +16 -0
- package/src/tools/terminal.js +29 -0
- package/src/tools/tirith_security.js +25 -0
- package/src/tools/todo.js +54 -0
- package/src/tools/tool_backend_helpers.js +26 -0
- package/src/tools/tool_output_limits.js +15 -0
- package/src/tools/tool_result_storage.js +20 -0
- package/src/tools/transcription.js +19 -0
- package/src/tools/tts.js +19 -0
- package/src/tools/url_safety.js +15 -0
- package/src/tools/vision.js +18 -0
- package/src/tools/voice_mode.js +10 -0
- package/src/tools/web_search.js +37 -0
- package/src/tools/web_tools.js +18 -0
- package/src/tools/website_policy.js +14 -0
- package/src/tools/write.js +25 -0
- package/src/tools/xai_http.js +13 -0
- package/src/tools/yuanbao_tools.js +13 -0
- package/src/toolset_distributions.js +18 -0
- package/src/toolsets.js +26 -0
- package/src/tui/index.js +26 -0
- package/src/utils.js +54 -0
- package/src/web/app.js +547 -0
- package/src/web/index.html +167 -0
- package/src/web/server.js +109 -0
package/src/web/app.js
ADDED
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
import ds, { mount, installStyles, h, components, renderMarkdown, motion } from 'anentrypoint-design'
|
|
2
|
+
const { AppShell, Topbar, Crumb, Side, Status, Panel, Row, Btn, Chip, Chat, ChatComposer, ChatMessage, AICat,
|
|
3
|
+
Brand, EmptyState, RowLink, Receipt, Changelog, Hero, ConfirmDialog, Section, Install } = components
|
|
4
|
+
|
|
5
|
+
await installStyles()
|
|
6
|
+
|
|
7
|
+
if (!window.__debug) { try { window.__debug = {} } catch { Object.defineProperty(window, '__debug', { value: {}, writable: true, configurable: true }) } }
|
|
8
|
+
window.__debug.dashboard = () => ({ booted: true, ts: Date.now(), framework: 'anentrypoint-design+webjsx', route: location.hash || '#/sessions' })
|
|
9
|
+
|
|
10
|
+
const j = async (u, opts) => { try { const r = await fetch(u, opts); if (!r.ok) throw new Error(r.status + ' ' + r.statusText); return await r.json() } catch (e) { return { __error: String(e) } } }
|
|
11
|
+
|
|
12
|
+
const ROUTES = [
|
|
13
|
+
{ path: '#/home', label: 'Home', glyph: '⌂' },
|
|
14
|
+
{ path: '#/chat', label: 'Chat', glyph: '⌨' },
|
|
15
|
+
{ path: '#/sessions', label: 'Sessions', glyph: '✉' },
|
|
16
|
+
{ path: '#/analytics', label: 'Analytics', glyph: '◉' },
|
|
17
|
+
{ path: '#/models', label: 'Models', glyph: '◎' },
|
|
18
|
+
{ path: '#/logs', label: 'Logs', glyph: '☰' },
|
|
19
|
+
{ path: '#/cron', label: 'Cron', glyph: '◷' },
|
|
20
|
+
{ path: '#/skills', label: 'Skills', glyph: '◈' },
|
|
21
|
+
{ path: '#/config', label: 'Config', glyph: '⚙' },
|
|
22
|
+
{ path: '#/env', label: 'Keys', glyph: '⚿' },
|
|
23
|
+
{ path: '#/docs', label: 'Documentation', glyph: '✎' },
|
|
24
|
+
{ path: '#/tools', label: 'Tools', glyph: '⚒' },
|
|
25
|
+
{ path: '#/batch', label: 'Batch', glyph: '⊞' },
|
|
26
|
+
{ path: '#/gateway', label: 'Gateway', glyph: '⇌' },
|
|
27
|
+
]
|
|
28
|
+
window.__debug.routes = () => ROUTES.map(r => r.path)
|
|
29
|
+
|
|
30
|
+
const AppState = {
|
|
31
|
+
hash: location.hash || '#/home',
|
|
32
|
+
body: null,
|
|
33
|
+
ts: new Date().toLocaleTimeString(),
|
|
34
|
+
theme: localStorage.getItem('freddie-theme') || 'dark',
|
|
35
|
+
search: { query: '', results: [] },
|
|
36
|
+
sessionsFilter: '',
|
|
37
|
+
chat: { messages: [], draft: '', streaming: false },
|
|
38
|
+
batch: { results: null, running: false },
|
|
39
|
+
}
|
|
40
|
+
function applyTheme() { document.documentElement.setAttribute('data-theme', AppState.theme) }
|
|
41
|
+
applyTheme()
|
|
42
|
+
window.__debug.state = () => AppState
|
|
43
|
+
|
|
44
|
+
function table(headers, rows, opts = {}) {
|
|
45
|
+
if (!rows || rows.length === 0) return EmptyState({ text: 'no rows' })
|
|
46
|
+
return h('table', {},
|
|
47
|
+
h('thead', {}, h('tr', {}, ...headers.map(hd => h('th', {}, hd)))),
|
|
48
|
+
h('tbody', {}, ...rows.map((row, i) => h('tr', {
|
|
49
|
+
class: opts.onRowClick ? 'clickable' : '',
|
|
50
|
+
onclick: opts.onRowClick ? () => opts.onRowClick(i) : null
|
|
51
|
+
}, ...row.map(c => h('td', {}, c == null ? '' : String(c)))))))
|
|
52
|
+
}
|
|
53
|
+
function kpi(items) {
|
|
54
|
+
return h('div', { class: 'kpi' }, ...items.map(([n, l]) =>
|
|
55
|
+
h('div', { class: 'kpi-card' }, h('div', { class: 'num' }, String(n)), h('div', { class: 'lbl' }, l))))
|
|
56
|
+
}
|
|
57
|
+
function pre(obj) { return h('pre', {}, typeof obj === 'string' ? obj : JSON.stringify(obj, null, 2)) }
|
|
58
|
+
|
|
59
|
+
function timeNow() {
|
|
60
|
+
const d = new Date()
|
|
61
|
+
return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function toChatMsg(m, key) {
|
|
65
|
+
const time = m.time || ''
|
|
66
|
+
if (m.role === 'user') {
|
|
67
|
+
return { who: 'you', avatar: 'u', time, receipt: 'delivered', key,
|
|
68
|
+
parts: [{ kind: 'text', text: typeof m.content === 'string' ? m.content : JSON.stringify(m.content) }] }
|
|
69
|
+
}
|
|
70
|
+
if (m.role === 'tool') {
|
|
71
|
+
const body = typeof m.content === 'string' ? m.content : JSON.stringify(m.content, null, 2)
|
|
72
|
+
return { who: 'them', avatar: '⚒', name: 'tool' + (m.tool_call_id ? ' · ' + String(m.tool_call_id).slice(0, 8) : ''), time, key,
|
|
73
|
+
parts: [{ kind: 'code', lang: 'json', filename: 'tool result', code: body }] }
|
|
74
|
+
}
|
|
75
|
+
const parts = []
|
|
76
|
+
const text = typeof m.content === 'string' ? m.content : ''
|
|
77
|
+
if (text) parts.push({ kind: 'md', text })
|
|
78
|
+
if (Array.isArray(m.tool_calls) && m.tool_calls.length) {
|
|
79
|
+
for (const c of m.tool_calls) {
|
|
80
|
+
parts.push({ kind: 'code', lang: 'json', filename: 'call · ' + (c.name || c.function?.name || '?'),
|
|
81
|
+
code: JSON.stringify(c.arguments || c.function?.arguments || {}, null, 2) })
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (parts.length === 0) parts.push({ kind: 'text', text: '' })
|
|
85
|
+
return { who: 'them', avatar: '◉', name: 'freddie', time, key, parts }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const PAGES = {
|
|
89
|
+
'#/chat': async () => {
|
|
90
|
+
const messages = AppState.chat.messages.map((m, i) => toChatMsg(m, 'm' + i))
|
|
91
|
+
return AICat({
|
|
92
|
+
name: 'freddie',
|
|
93
|
+
status: AppState.chat.streaming ? 'thinking…' : 'online · live runTurn via SSE',
|
|
94
|
+
messages,
|
|
95
|
+
thinking: AppState.chat.streaming,
|
|
96
|
+
composer: ChatComposer({
|
|
97
|
+
value: AppState.chat.draft,
|
|
98
|
+
placeholder: 'Ask freddie — runs through registered tools and the configured LLM…',
|
|
99
|
+
disabled: AppState.chat.streaming,
|
|
100
|
+
onInput: (v) => { AppState.chat.draft = v; rerender() },
|
|
101
|
+
onSend: (text) => { AppState.chat.draft = ''; sendChat(text) },
|
|
102
|
+
}),
|
|
103
|
+
})
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
'#/home': async () => {
|
|
107
|
+
const [sessions, tools, skills] = await Promise.all([j('/api/sessions'), j('/api/tools'), j('/api/skills')])
|
|
108
|
+
const sessionCount = Array.isArray(sessions) ? sessions.length : 0
|
|
109
|
+
const toolCount = Array.isArray(tools) ? tools.length : 0
|
|
110
|
+
const skillCount = ((skills.home || []).length + (skills.bundled || []).length)
|
|
111
|
+
return [
|
|
112
|
+
Hero({ title: 'freddie', body: 'Open JS agent harness built on pi-mono, xstate, floosie, and anentrypoint-design.', accent: 'v0.0.1' }),
|
|
113
|
+
kpi([[sessionCount, 'Sessions'], [toolCount, 'Tools'], [skillCount, 'Skills']]),
|
|
114
|
+
Panel({ title: 'Quick start', children: Receipt({ rows: [
|
|
115
|
+
['Run interactive REPL', 'freddie run'],
|
|
116
|
+
['Start dashboard', 'freddie dashboard --port 3000'],
|
|
117
|
+
['List tools', 'freddie tools'],
|
|
118
|
+
['List skills', 'freddie skills'],
|
|
119
|
+
['Start gateway', 'freddie gateway --port 4000'],
|
|
120
|
+
]}) }),
|
|
121
|
+
]
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
'#/sessions': async () => {
|
|
125
|
+
const sessions = await j('/api/sessions')
|
|
126
|
+
const all = sessions.__error ? [] : sessions
|
|
127
|
+
const q = AppState.sessionsFilter.toLowerCase()
|
|
128
|
+
const filtered = q ? all.filter(s => JSON.stringify(s).toLowerCase().includes(q)) : all
|
|
129
|
+
return [
|
|
130
|
+
kpi([[all.length || 0, 'Total sessions'], [filtered.length, 'After filter']]),
|
|
131
|
+
Panel({ title: 'Filter', children: h('div', { class: 'row-form' },
|
|
132
|
+
h('input', { type: 'text', placeholder: 'filter by platform/title/model/id…', value: AppState.sessionsFilter,
|
|
133
|
+
oninput: (ev) => { AppState.sessionsFilter = ev.target.value; rerender() } })) }),
|
|
134
|
+
Panel({ title: 'Recent sessions (click row → detail)', count: filtered.length,
|
|
135
|
+
children: filtered.length === 0
|
|
136
|
+
? EmptyState({ text: 'no sessions yet — start a chat', glyph: '✉' })
|
|
137
|
+
: h('div', {}, ...filtered.map(s =>
|
|
138
|
+
RowLink({ key: s.id, href: '#/session/' + s.id,
|
|
139
|
+
code: s.id?.slice(0, 8), title: s.title || s.platform || 'untitled',
|
|
140
|
+
sub: s.model || '', meta: new Date(s.updated_at || 0).toLocaleString() }))) }),
|
|
141
|
+
]
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
'#/analytics': async () => {
|
|
145
|
+
const [sessions, tools, debug] = await Promise.all([j('/api/sessions'), j('/api/tools'), j('/api/debug')])
|
|
146
|
+
const all = Array.isArray(sessions) ? sessions : []
|
|
147
|
+
const ts = Array.isArray(tools) ? tools : []
|
|
148
|
+
const byPlatform = all.reduce((acc, s) => { const k = s.platform || 'unknown'; acc[k] = (acc[k] || 0) + 1; return acc }, {})
|
|
149
|
+
const byModel = all.reduce((acc, s) => { const k = s.model || 'unknown'; acc[k] = (acc[k] || 0) + 1; return acc }, {})
|
|
150
|
+
return [
|
|
151
|
+
kpi([
|
|
152
|
+
[all.length || 0, 'Sessions'],
|
|
153
|
+
[ts.length || 0, 'Tools'],
|
|
154
|
+
[Array.isArray(debug) ? debug.length : 0, 'Debug subsystems'],
|
|
155
|
+
]),
|
|
156
|
+
Panel({ title: 'Sessions by platform', children: Object.keys(byPlatform).length === 0
|
|
157
|
+
? EmptyState({ text: 'no sessions yet', glyph: '◉' })
|
|
158
|
+
: table(['platform', 'count'], Object.entries(byPlatform).sort((a,b) => b[1]-a[1])) }),
|
|
159
|
+
Panel({ title: 'Sessions by model', children: Object.keys(byModel).length === 0
|
|
160
|
+
? EmptyState({ text: 'no sessions yet', glyph: '◎' })
|
|
161
|
+
: table(['model', 'count'], Object.entries(byModel).sort((a,b) => b[1]-a[1])) }),
|
|
162
|
+
Panel({ title: 'Tool distribution by toolset', children: table(['toolset', 'count', 'tools'],
|
|
163
|
+
Object.entries(ts.reduce((acc, t) => { acc[t.toolset] = acc[t.toolset] || []; acc[t.toolset].push(t.name); return acc }, {}))
|
|
164
|
+
.map(([k, v]) => [k, v.length, v.slice(0,4).join(', ') + (v.length > 4 ? '…' : '')])) }),
|
|
165
|
+
]
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
'#/models': async () => {
|
|
169
|
+
const config = await j('/api/config')
|
|
170
|
+
const agent = config.agent || {}
|
|
171
|
+
return [
|
|
172
|
+
kpi([[agent.provider || '—', 'Provider'], [agent.model || '—', 'Model']]),
|
|
173
|
+
Panel({ title: 'Active model config', children: Receipt({ rows: [
|
|
174
|
+
['provider', agent.provider || '(not set)'],
|
|
175
|
+
['model', agent.model || '(not set)'],
|
|
176
|
+
['max_iterations', String(agent.max_iterations || '—')],
|
|
177
|
+
['max_tokens', String(agent.max_tokens || '—')],
|
|
178
|
+
['temperature', String(agent.temperature ?? '—')],
|
|
179
|
+
]}) }),
|
|
180
|
+
Panel({ title: 'Change model', children: h('form', { class: 'row-form', onsubmit: async (ev) => {
|
|
181
|
+
ev.preventDefault()
|
|
182
|
+
const f = ev.target.elements
|
|
183
|
+
await j('/api/config', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ key: 'agent.model', value: f.model.value }) })
|
|
184
|
+
await j('/api/config', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ key: 'agent.provider', value: f.provider.value }) })
|
|
185
|
+
rerender()
|
|
186
|
+
} },
|
|
187
|
+
h('input', { name: 'provider', placeholder: 'provider (anthropic / openai / groq)', value: agent.provider || '' }),
|
|
188
|
+
h('input', { name: 'model', placeholder: 'model id (e.g. claude-opus-4-5)', value: agent.model || '' }),
|
|
189
|
+
h('button', { type: 'submit', class: 'primary' }, 'Update')) }),
|
|
190
|
+
]
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
'#/logs': async () => {
|
|
194
|
+
const subs = await j('/api/logs')
|
|
195
|
+
const list = Array.isArray(subs) ? subs : []
|
|
196
|
+
const first = list[0]
|
|
197
|
+
const recent = first ? await j(`/api/logs/${first}?max=50`) : []
|
|
198
|
+
return [
|
|
199
|
+
kpi([[list.length, 'Log subsystems']]),
|
|
200
|
+
Panel({ title: 'Subsystems', children: list.length === 0
|
|
201
|
+
? EmptyState({ text: 'no logs yet — run freddie and observe', glyph: '☰' })
|
|
202
|
+
: h('div', {}, ...list.map(s => Row({ key: s, code: '☰', title: s, meta: '' }))) }),
|
|
203
|
+
first
|
|
204
|
+
? Panel({ title: `Latest entries · ${first}`, children: pre(recent) })
|
|
205
|
+
: null,
|
|
206
|
+
].filter(Boolean)
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
'#/cron': async () => {
|
|
210
|
+
const jobs = await j('/api/cron')
|
|
211
|
+
const list = Array.isArray(jobs) ? jobs : []
|
|
212
|
+
return [
|
|
213
|
+
kpi([[list.length, 'Cron jobs']]),
|
|
214
|
+
Panel({ title: 'Add job', children: h('form', { class: 'row-form', onsubmit: async (ev) => {
|
|
215
|
+
ev.preventDefault()
|
|
216
|
+
const f = ev.target.elements
|
|
217
|
+
await j('/api/cron', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ cron: f.cron.value, prompt: f.prompt.value }) })
|
|
218
|
+
f.cron.value = ''; f.prompt.value = ''; rerender()
|
|
219
|
+
} },
|
|
220
|
+
h('input', { name: 'cron', placeholder: 'cron expr (* * * * *)' }),
|
|
221
|
+
h('input', { name: 'prompt', placeholder: 'prompt' }),
|
|
222
|
+
h('button', { type: 'submit', class: 'primary' }, 'Create')) }),
|
|
223
|
+
Panel({ title: 'Scheduled jobs', count: list.length, children: list.length === 0
|
|
224
|
+
? EmptyState({ text: 'no cron jobs — add one above', glyph: '◷' })
|
|
225
|
+
: h('table', {},
|
|
226
|
+
h('thead', {}, h('tr', {}, ...['id', 'cron', 'prompt', 'enabled', ''].map(c => h('th', {}, c)))),
|
|
227
|
+
h('tbody', {}, ...list.map(job => h('tr', {},
|
|
228
|
+
h('td', {}, String(job.id)),
|
|
229
|
+
h('td', {}, job.cron),
|
|
230
|
+
h('td', {}, (job.prompt || '').slice(0, 60)),
|
|
231
|
+
h('td', {}, job.enabled ? 'yes' : 'no'),
|
|
232
|
+
h('td', {}, h('button', {
|
|
233
|
+
class: 'danger',
|
|
234
|
+
onclick: async () => { await fetch('/api/cron/' + job.id, { method: 'DELETE' }); rerender() }
|
|
235
|
+
}, 'delete')))))) }),
|
|
236
|
+
]
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
'#/skills': async () => {
|
|
240
|
+
const data = await j('/api/skills')
|
|
241
|
+
const home = data.home || []
|
|
242
|
+
const bundled = data.bundled || []
|
|
243
|
+
return [
|
|
244
|
+
kpi([[home.length, 'User skills'], [bundled.length, 'Bundled skills']]),
|
|
245
|
+
Panel({ title: 'User skills (~/.freddie/skills)', count: home.length,
|
|
246
|
+
children: home.length === 0
|
|
247
|
+
? EmptyState({ text: 'drop SKILL.md files in ~/.freddie/skills/ to add', glyph: '◈' })
|
|
248
|
+
: h('div', {}, ...home.map(s => Row({ key: s.name, code: '◈', title: s.name, sub: s.description || '', meta: '' }))) }),
|
|
249
|
+
Panel({ title: 'Bundled skills', count: bundled.length,
|
|
250
|
+
children: h('div', {}, ...bundled.map(s => Row({ key: s.name, code: '◈', title: s.name, sub: s.description || '', meta: '' }))) }),
|
|
251
|
+
]
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
'#/config': async () => {
|
|
255
|
+
const config = await j('/api/config')
|
|
256
|
+
const profiles = await j('/api/profiles')
|
|
257
|
+
const commands = await j('/api/commands')
|
|
258
|
+
return [
|
|
259
|
+
kpi([
|
|
260
|
+
[(profiles || []).length, 'Profiles'],
|
|
261
|
+
[(commands || []).length, 'Commands'],
|
|
262
|
+
[config._config_version || 0, 'Config version'],
|
|
263
|
+
]),
|
|
264
|
+
Panel({ title: 'Set config value', children: h('form', { class: 'row-form', onsubmit: async (ev) => {
|
|
265
|
+
ev.preventDefault()
|
|
266
|
+
const f = ev.target.elements
|
|
267
|
+
await j('/api/config', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ key: f.key.value, value: f.value.value }) })
|
|
268
|
+
f.value.value = ''; rerender()
|
|
269
|
+
} },
|
|
270
|
+
h('input', { name: 'key', placeholder: 'dotted.key (e.g. display.skin)' }),
|
|
271
|
+
h('input', { name: 'value', placeholder: 'value' }),
|
|
272
|
+
h('button', { type: 'submit', class: 'primary' }, 'Save')) }),
|
|
273
|
+
Panel({ title: 'Profiles', count: (profiles || []).length,
|
|
274
|
+
children: (profiles || []).length === 0
|
|
275
|
+
? EmptyState({ text: 'no profiles — using HOME', glyph: '◎' })
|
|
276
|
+
: h('div', {}, ...(profiles || []).map(p => Row({ key: p, code: '◎', title: p, meta: '' }))) }),
|
|
277
|
+
Panel({ title: 'Slash commands', count: (commands || []).length,
|
|
278
|
+
children: table(['name', 'category', 'description'], (commands || []).map(c => [c.name, c.category || '', c.description || ''])) }),
|
|
279
|
+
Panel({ title: 'Active config', children: pre(config) }),
|
|
280
|
+
]
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
'#/env': async () => {
|
|
284
|
+
const keys = await j('/api/env')
|
|
285
|
+
const list = Array.isArray(keys) ? keys : []
|
|
286
|
+
const set = list.filter(k => k.set).length
|
|
287
|
+
return [
|
|
288
|
+
kpi([[set, 'Keys set'], [list.length - set, 'Keys missing'], [list.length, 'Total known']]),
|
|
289
|
+
Panel({ title: 'Environment variables',
|
|
290
|
+
right: h('span', {}, Chip({ tone: 'ok', children: set + ' set' }), ' ', Chip({ tone: 'miss', children: (list.length - set) + ' missing' })),
|
|
291
|
+
children: h('div', { style: 'padding:8px 4px;display:flex;flex-wrap:wrap;gap:6px' },
|
|
292
|
+
...list.map(k => Chip({ tone: k.set ? 'ok' : 'miss', children: k.key + (k.set ? ' ✓' : ' ·') }))) }),
|
|
293
|
+
]
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
'#/tools': async () => {
|
|
297
|
+
const tools = await j('/api/tools')
|
|
298
|
+
const list = Array.isArray(tools) ? tools : []
|
|
299
|
+
const byToolset = list.reduce((acc, t) => { (acc[t.toolset] = acc[t.toolset] || []).push(t); return acc }, {})
|
|
300
|
+
return [
|
|
301
|
+
kpi([[list.length, 'Total tools'], [Object.keys(byToolset).length, 'Toolsets']]),
|
|
302
|
+
...Object.entries(byToolset).map(([ts, ts_tools]) =>
|
|
303
|
+
Panel({ title: 'Toolset · ' + ts, count: ts_tools.length,
|
|
304
|
+
children: h('div', {}, ...ts_tools.map(t =>
|
|
305
|
+
Row({ key: t.name, code: '⚒', title: t.name, sub: (t.schema?.description || '').slice(0, 80), meta: '' }))) }))
|
|
306
|
+
]
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
'#/batch': async () => {
|
|
310
|
+
const results = AppState.batch.results
|
|
311
|
+
const running = AppState.batch.running
|
|
312
|
+
return [
|
|
313
|
+
Section({ title: '// batch runner', children: [
|
|
314
|
+
Panel({ title: 'Run prompts', children: h('div', {},
|
|
315
|
+
h('p', { style: 'margin-bottom:12px;opacity:0.7' }, 'Submit multiple prompts in parallel. Results stream back as JSONL. Each prompt runs a full agent turn.'),
|
|
316
|
+
h('form', { class: 'row-form', style: 'flex-direction:column;gap:8px', onsubmit: async (ev) => {
|
|
317
|
+
ev.preventDefault()
|
|
318
|
+
const f = ev.target.elements
|
|
319
|
+
const prompts = f.prompts.value.split('\n').map(l => l.trim()).filter(Boolean)
|
|
320
|
+
if (!prompts.length) return
|
|
321
|
+
AppState.batch.running = true; AppState.batch.results = null; rerender()
|
|
322
|
+
const res = await j('/api/batch', { method: 'POST', headers: { 'content-type': 'application/json' },
|
|
323
|
+
body: JSON.stringify({ prompts, concurrency: Number(f.concurrency.value) || 4 }) })
|
|
324
|
+
AppState.batch.results = res; AppState.batch.running = false; rerender()
|
|
325
|
+
} },
|
|
326
|
+
h('textarea', { name: 'prompts', rows: 5, placeholder: 'One prompt per line…', style: 'width:100%;font-family:monospace;resize:vertical' }),
|
|
327
|
+
h('div', { style: 'display:flex;gap:8px;align-items:center' },
|
|
328
|
+
h('label', { style: 'font-size:12px;opacity:0.6' }, 'concurrency'),
|
|
329
|
+
h('input', { name: 'concurrency', type: 'number', value: '4', style: 'width:60px' }),
|
|
330
|
+
h('button', { type: 'submit', class: 'primary', disabled: running }, running ? 'running…' : 'Run batch')))) }),
|
|
331
|
+
running ? Panel({ title: 'Running…', children: EmptyState({ text: 'batch in progress', glyph: '⊞' }) }) : null,
|
|
332
|
+
results ? Panel({ title: results.__error ? 'Error' : 'Results · ' + (results.results?.length || 0),
|
|
333
|
+
children: results.__error
|
|
334
|
+
? h('p', { style: 'color:var(--error,red)' }, results.__error)
|
|
335
|
+
: h('div', {}, ...(results.results || []).map((r, i) =>
|
|
336
|
+
Row({ key: i, code: String(i+1), title: (r.prompt || '').slice(0, 60), sub: (r.output || r.error || '').slice(0, 100), meta: r.error ? 'error' : 'ok' }))) }) : null,
|
|
337
|
+
Panel({ title: 'CLI usage', children: Receipt({ rows: [
|
|
338
|
+
['run batch file', 'freddie batch prompts.txt'],
|
|
339
|
+
['set concurrency', 'freddie batch prompts.txt --concurrency 8'],
|
|
340
|
+
['JSONL output', 'freddie batch prompts.txt > results.jsonl'],
|
|
341
|
+
]}) }),
|
|
342
|
+
].filter(Boolean) }),
|
|
343
|
+
]
|
|
344
|
+
},
|
|
345
|
+
|
|
346
|
+
'#/gateway': async () => {
|
|
347
|
+
const data = await j('/api/gateway')
|
|
348
|
+
const platforms = Array.isArray(data?.platforms) ? data.platforms : []
|
|
349
|
+
const active = platforms.filter(p => p.enabled)
|
|
350
|
+
return [
|
|
351
|
+
kpi([[platforms.length, 'Platforms'], [active.length, 'Active']]),
|
|
352
|
+
Panel({ title: 'Platforms', right: active.length > 0 ? Chip({ tone: 'ok', children: active.length + ' active' }) : Chip({ tone: 'miss', children: 'none active' }),
|
|
353
|
+
children: h('div', {}, ...platforms.map(p =>
|
|
354
|
+
Row({ key: p.name, code: p.enabled ? '●' : '○', title: p.name, sub: p.note || '', meta: p.enabled ? 'enabled' : '' }))) }),
|
|
355
|
+
Panel({ title: 'Start gateway', children: Receipt({ rows: [
|
|
356
|
+
['webhook + api_server', 'freddie gateway --port 3000'],
|
|
357
|
+
['specific platform', 'TELEGRAM_BOT_TOKEN=xxx freddie gateway'],
|
|
358
|
+
['all platforms', 'set env vars per platform, then freddie gateway'],
|
|
359
|
+
]}) }),
|
|
360
|
+
]
|
|
361
|
+
},
|
|
362
|
+
|
|
363
|
+
'#/docs': async () => [
|
|
364
|
+
Section({ title: '// documentation', children: [
|
|
365
|
+
Panel({ title: 'freddie — open JS agent harness', children: h('div', {},
|
|
366
|
+
h('p', { style: 'margin-bottom:16px;opacity:0.75;line-height:1.6' },
|
|
367
|
+
'Built on pi-mono, xstate, floosie, and anentrypoint-design. 70+ tools, 18 gateway platforms, 12 bundled skills.'),
|
|
368
|
+
Receipt({ rows: [
|
|
369
|
+
['agent loop', 'xstate + @mariozechner/pi-agent-core'],
|
|
370
|
+
['provider layer', '@mariozechner/pi-ai (Anthropic / OpenAI / Groq)'],
|
|
371
|
+
['gateway', '18 platform adapters (telegram, discord, slack, …)'],
|
|
372
|
+
['tools', '11 built-ins + auto-discovered from src/tools/'],
|
|
373
|
+
['dashboard', 'anentrypoint-design webjsx'],
|
|
374
|
+
] })) }),
|
|
375
|
+
Panel({ title: 'Links', children: h('div', {},
|
|
376
|
+
RowLink({ code: '↗', title: 'GitHub · AnEntrypoint/freddie', href: 'https://github.com/AnEntrypoint/freddie' }),
|
|
377
|
+
RowLink({ code: '↗', title: 'API health', href: '/api/health' }),
|
|
378
|
+
RowLink({ code: '↗', title: 'Debug — all subsystems', href: '/api/debug-all' }),
|
|
379
|
+
RowLink({ code: '↗', title: 'anentrypoint-design', href: 'https://anentrypoint.io/design' }),
|
|
380
|
+
) }),
|
|
381
|
+
Panel({ title: 'Quick reference', children: Receipt({ rows: [
|
|
382
|
+
['interactive REPL', 'freddie run'],
|
|
383
|
+
['dashboard', 'freddie dashboard --port 3000'],
|
|
384
|
+
['gateway', 'freddie gateway --port 4000'],
|
|
385
|
+
['list tools', 'freddie tools'],
|
|
386
|
+
['list skills', 'freddie skills'],
|
|
387
|
+
['manage profiles', 'freddie profile list'],
|
|
388
|
+
['run batch', 'freddie batch prompts.txt'],
|
|
389
|
+
['search sessions', 'freddie search "my query"'],
|
|
390
|
+
]}) }),
|
|
391
|
+
] }),
|
|
392
|
+
Panel({ title: 'Recent changelog', children: Changelog({ entries: [
|
|
393
|
+
{ date: '2026-05-01', ver: 'v0.0.1', msg: 'Initial release — 70 tools, 18 gateway platforms, 12 bundled skills' },
|
|
394
|
+
{ date: '2026-05-01', ver: 'v0.1.0', msg: 'Dashboard routes: #/tools #/batch #/gateway, anentrypoint-design pro-rata upgrade' },
|
|
395
|
+
]}) }),
|
|
396
|
+
],
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function pageSessionDetail(id) {
|
|
400
|
+
const messages = await j('/api/sessions/' + id + '/messages')
|
|
401
|
+
const list = Array.isArray(messages) ? messages : []
|
|
402
|
+
return [
|
|
403
|
+
Panel({ title: 'Session ' + id.slice(0, 8), children: kpi([[list.length, 'messages']]) }),
|
|
404
|
+
list.length === 0
|
|
405
|
+
? Panel({ title: 'Messages', children: EmptyState({ text: 'no messages in this session', glyph: '✉' }) })
|
|
406
|
+
: Chat({ title: 'session ' + id.slice(0, 8), sub: 'replay', messages: list.map((m, i) => toChatMsg(m, 's' + i)) }),
|
|
407
|
+
Panel({ title: 'Back', children: h('a', { href: '#/sessions' }, '← all sessions') }),
|
|
408
|
+
]
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async function sendChat(prompt) {
|
|
412
|
+
if (!prompt || !prompt.trim() || AppState.chat.streaming) return
|
|
413
|
+
AppState.chat.messages.push({ role: 'user', content: prompt, time: timeNow() })
|
|
414
|
+
AppState.chat.streaming = true
|
|
415
|
+
rerender()
|
|
416
|
+
try {
|
|
417
|
+
const r = await fetch('/api/chat', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ prompt }) })
|
|
418
|
+
const reader = r.body.getReader(), dec = new TextDecoder()
|
|
419
|
+
let buf = ''
|
|
420
|
+
while (true) {
|
|
421
|
+
const { value, done } = await reader.read()
|
|
422
|
+
if (done) break
|
|
423
|
+
buf += dec.decode(value, { stream: true })
|
|
424
|
+
let idx
|
|
425
|
+
while ((idx = buf.indexOf('\n\n')) >= 0) {
|
|
426
|
+
const block = buf.slice(0, idx); buf = buf.slice(idx + 2)
|
|
427
|
+
const ev = (block.match(/^event: (.+)$/m) || [, ''])[1]
|
|
428
|
+
const data = (block.match(/^data: (.+)$/m) || [, '{}'])[1]
|
|
429
|
+
let parsed; try { parsed = JSON.parse(data) } catch { parsed = { raw: data } }
|
|
430
|
+
if (ev === 'message') {
|
|
431
|
+
if (parsed.role !== 'user') AppState.chat.messages.push({ ...parsed, time: timeNow() })
|
|
432
|
+
} else if (ev === 'error') {
|
|
433
|
+
AppState.chat.messages.push({ role: 'assistant', content: '**[error]** ' + parsed.error, time: timeNow() })
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
} catch (e) {
|
|
438
|
+
AppState.chat.messages.push({ role: 'assistant', content: '**[network error]** ' + e.message, time: timeNow() })
|
|
439
|
+
}
|
|
440
|
+
AppState.chat.streaming = false
|
|
441
|
+
rerender()
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function doSearch(q) {
|
|
445
|
+
AppState.search.query = q
|
|
446
|
+
if (!q.trim()) { AppState.search.results = []; rerender(); return }
|
|
447
|
+
const r = await j('/api/search?q=' + encodeURIComponent(q))
|
|
448
|
+
AppState.search.results = Array.isArray(r) ? r : []
|
|
449
|
+
rerender()
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function buildSide(state) {
|
|
453
|
+
const sections = [{
|
|
454
|
+
group: 'NAVIGATION',
|
|
455
|
+
items: ROUTES.map(r => ({
|
|
456
|
+
glyph: r.glyph,
|
|
457
|
+
label: r.label,
|
|
458
|
+
href: r.path,
|
|
459
|
+
active: !state.hash.startsWith('#/session/') && r.path === state.hash,
|
|
460
|
+
onClick: (ev) => { ev.preventDefault(); location.hash = r.path },
|
|
461
|
+
})),
|
|
462
|
+
}]
|
|
463
|
+
return Side({ sections })
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function render(state) {
|
|
467
|
+
let route = ROUTES.find(r => r.path === state.hash)
|
|
468
|
+
const isSessionDetail = state.hash.startsWith('#/session/')
|
|
469
|
+
if (!route && !isSessionDetail) route = ROUTES[0]
|
|
470
|
+
const themeLabel = state.theme === 'dark' ? '☀ light' : '☾ dark'
|
|
471
|
+
const themeBtn = h('button', {
|
|
472
|
+
class: 'ghost',
|
|
473
|
+
onclick: () => {
|
|
474
|
+
AppState.theme = AppState.theme === 'dark' ? 'light' : 'dark'
|
|
475
|
+
localStorage.setItem('freddie-theme', AppState.theme)
|
|
476
|
+
applyTheme(); rerender()
|
|
477
|
+
},
|
|
478
|
+
style: 'font-size:12px;padding:4px 12px',
|
|
479
|
+
}, themeLabel)
|
|
480
|
+
const searchInput = h('input', {
|
|
481
|
+
type: 'search',
|
|
482
|
+
placeholder: 'search messages…',
|
|
483
|
+
value: state.search.query,
|
|
484
|
+
onkeydown: (ev) => { if (ev.key === 'Enter') doSearch(ev.target.value) },
|
|
485
|
+
style: 'min-width:240px',
|
|
486
|
+
})
|
|
487
|
+
const topbarWithControls = h('header', { class: 'app-topbar' },
|
|
488
|
+
Brand({ name: 'freddie', leaf: 'dashboard' }),
|
|
489
|
+
h('div', { style: 'flex:1' }),
|
|
490
|
+
searchInput,
|
|
491
|
+
themeBtn,
|
|
492
|
+
)
|
|
493
|
+
const crumbRight = state.search.results.length > 0
|
|
494
|
+
? h('span', { class: 'meta' }, state.search.results.length + ' hits')
|
|
495
|
+
: null
|
|
496
|
+
const crumb = Crumb({ trail: ['freddie'], leaf: isSessionDetail ? state.hash.replace('#/', '') : route.path.replace('#/', ''), right: crumbRight })
|
|
497
|
+
const searchResults = state.search.results.length > 0
|
|
498
|
+
? Panel({ title: `search results · ${state.search.results.length}`, children: state.search.results.slice(0, 8).map((r, i) =>
|
|
499
|
+
Row({ key: i, code: (r.session_id || '?').slice(0, 8), title: (r.content || '').slice(0, 80),
|
|
500
|
+
meta: 'open', onClick: () => { location.hash = '#/session/' + r.session_id } })) })
|
|
501
|
+
: null
|
|
502
|
+
const main = [searchResults, state.body || EmptyState({ text: 'loading…' })].filter(Boolean)
|
|
503
|
+
const status = Status({
|
|
504
|
+
left: ['ds-247420 · webjsx · ' + ROUTES.length + ' routes', 'theme=' + state.theme],
|
|
505
|
+
right: [state.ts],
|
|
506
|
+
})
|
|
507
|
+
return AppShell({ topbar: topbarWithControls, crumb, side: buildSide(state), main, status })
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
let _mount
|
|
511
|
+
|
|
512
|
+
async function go() {
|
|
513
|
+
AppState.hash = location.hash || '#/home'
|
|
514
|
+
AppState.ts = new Date().toLocaleTimeString()
|
|
515
|
+
AppState.body = EmptyState({ text: 'loading…', glyph: '◌' })
|
|
516
|
+
if (_mount) _mount()
|
|
517
|
+
let body
|
|
518
|
+
if (AppState.hash.startsWith('#/session/')) {
|
|
519
|
+
body = await pageSessionDetail(AppState.hash.slice('#/session/'.length))
|
|
520
|
+
} else {
|
|
521
|
+
const page = PAGES[AppState.hash] || PAGES['#/home']
|
|
522
|
+
body = await page()
|
|
523
|
+
}
|
|
524
|
+
AppState.body = body
|
|
525
|
+
AppState.ts = new Date().toLocaleTimeString()
|
|
526
|
+
if (_mount) _mount()
|
|
527
|
+
window.__debug.lastRoute = AppState.hash
|
|
528
|
+
requestAnimationFrame(() => motion.animateSelector('.app-main', 'fadeIn', { duration: 'var(--motion-base)' }))
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function rerender() {
|
|
532
|
+
AppState.ts = new Date().toLocaleTimeString()
|
|
533
|
+
if (AppState.hash === '#/chat') {
|
|
534
|
+
Promise.resolve(PAGES['#/chat']()).then(b => { AppState.body = b; if (_mount) _mount() })
|
|
535
|
+
return
|
|
536
|
+
}
|
|
537
|
+
if (_mount) _mount()
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
window.addEventListener('hashchange', go)
|
|
541
|
+
_mount = mount(document.getElementById('app'), () => render(AppState))
|
|
542
|
+
go()
|
|
543
|
+
|
|
544
|
+
window.__debug.go = go
|
|
545
|
+
window.__debug.sendChat = sendChat
|
|
546
|
+
window.__debug.doSearch = doSearch
|
|
547
|
+
window.__debug.chat = () => ({ messages: AppState.chat.messages.length, streaming: AppState.chat.streaming, draft: AppState.chat.draft })
|