myaiforone 1.0.0
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/README.md +113 -0
- package/agents/_template/CLAUDE.md +18 -0
- package/agents/_template/agent.json +7 -0
- package/agents/platform/agentcreator/CLAUDE.md +300 -0
- package/agents/platform/appcreator/CLAUDE.md +158 -0
- package/agents/platform/gym/CLAUDE.md +486 -0
- package/agents/platform/gym/agent.json +40 -0
- package/agents/platform/gym/programs/agent-building/program.json +160 -0
- package/agents/platform/gym/programs/automations-mastery/program.json +129 -0
- package/agents/platform/gym/programs/getting-started/program.json +124 -0
- package/agents/platform/gym/programs/mcp-integrations/program.json +116 -0
- package/agents/platform/gym/programs/multi-model-strategy/program.json +115 -0
- package/agents/platform/gym/programs/prompt-engineering/program.json +136 -0
- package/agents/platform/gym/souls/alex.md +12 -0
- package/agents/platform/gym/souls/jordan.md +12 -0
- package/agents/platform/gym/souls/morgan.md +12 -0
- package/agents/platform/gym/souls/riley.md +12 -0
- package/agents/platform/gym/souls/sam.md +12 -0
- package/agents/platform/hub/CLAUDE.md +372 -0
- package/agents/platform/promptcreator/CLAUDE.md +130 -0
- package/agents/platform/skillcreator/CLAUDE.md +163 -0
- package/bin/cli.js +566 -0
- package/config.example.json +310 -0
- package/dist/agent-registry.d.ts +32 -0
- package/dist/agent-registry.d.ts.map +1 -0
- package/dist/agent-registry.js +144 -0
- package/dist/agent-registry.js.map +1 -0
- package/dist/channels/discord.d.ts +17 -0
- package/dist/channels/discord.d.ts.map +1 -0
- package/dist/channels/discord.js +114 -0
- package/dist/channels/discord.js.map +1 -0
- package/dist/channels/imessage.d.ts +23 -0
- package/dist/channels/imessage.d.ts.map +1 -0
- package/dist/channels/imessage.js +214 -0
- package/dist/channels/imessage.js.map +1 -0
- package/dist/channels/slack.d.ts +19 -0
- package/dist/channels/slack.d.ts.map +1 -0
- package/dist/channels/slack.js +167 -0
- package/dist/channels/slack.js.map +1 -0
- package/dist/channels/telegram.d.ts +19 -0
- package/dist/channels/telegram.d.ts.map +1 -0
- package/dist/channels/telegram.js +274 -0
- package/dist/channels/telegram.js.map +1 -0
- package/dist/channels/types.d.ts +44 -0
- package/dist/channels/types.d.ts.map +1 -0
- package/dist/channels/types.js +18 -0
- package/dist/channels/types.js.map +1 -0
- package/dist/channels/whatsapp.d.ts +23 -0
- package/dist/channels/whatsapp.d.ts.map +1 -0
- package/dist/channels/whatsapp.js +189 -0
- package/dist/channels/whatsapp.js.map +1 -0
- package/dist/config.d.ts +134 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +127 -0
- package/dist/config.js.map +1 -0
- package/dist/cron.d.ts +8 -0
- package/dist/cron.d.ts.map +1 -0
- package/dist/cron.js +35 -0
- package/dist/cron.js.map +1 -0
- package/dist/decrypt-keys.d.ts +7 -0
- package/dist/decrypt-keys.d.ts.map +1 -0
- package/dist/decrypt-keys.js +53 -0
- package/dist/decrypt-keys.js.map +1 -0
- package/dist/encrypt-keys.d.ts +8 -0
- package/dist/encrypt-keys.d.ts.map +1 -0
- package/dist/encrypt-keys.js +62 -0
- package/dist/encrypt-keys.js.map +1 -0
- package/dist/executor.d.ts +31 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +2009 -0
- package/dist/executor.js.map +1 -0
- package/dist/gemini-executor.d.ts +27 -0
- package/dist/gemini-executor.d.ts.map +1 -0
- package/dist/gemini-executor.js +160 -0
- package/dist/gemini-executor.js.map +1 -0
- package/dist/goals.d.ts +24 -0
- package/dist/goals.d.ts.map +1 -0
- package/dist/goals.js +189 -0
- package/dist/goals.js.map +1 -0
- package/dist/gym/activity-digest.d.ts +30 -0
- package/dist/gym/activity-digest.d.ts.map +1 -0
- package/dist/gym/activity-digest.js +506 -0
- package/dist/gym/activity-digest.js.map +1 -0
- package/dist/gym/dimension-scorer.d.ts +76 -0
- package/dist/gym/dimension-scorer.d.ts.map +1 -0
- package/dist/gym/dimension-scorer.js +236 -0
- package/dist/gym/dimension-scorer.js.map +1 -0
- package/dist/gym/gym-router.d.ts +7 -0
- package/dist/gym/gym-router.d.ts.map +1 -0
- package/dist/gym/gym-router.js +718 -0
- package/dist/gym/gym-router.js.map +1 -0
- package/dist/gym/index.d.ts +11 -0
- package/dist/gym/index.d.ts.map +1 -0
- package/dist/gym/index.js +11 -0
- package/dist/gym/index.js.map +1 -0
- package/dist/heartbeat.d.ts +21 -0
- package/dist/heartbeat.d.ts.map +1 -0
- package/dist/heartbeat.js +163 -0
- package/dist/heartbeat.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +254 -0
- package/dist/index.js.map +1 -0
- package/dist/keystore.d.ts +22 -0
- package/dist/keystore.d.ts.map +1 -0
- package/dist/keystore.js +178 -0
- package/dist/keystore.js.map +1 -0
- package/dist/logger.d.ts +9 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +45 -0
- package/dist/logger.js.map +1 -0
- package/dist/memory/daily.d.ts +22 -0
- package/dist/memory/daily.d.ts.map +1 -0
- package/dist/memory/daily.js +82 -0
- package/dist/memory/daily.js.map +1 -0
- package/dist/memory/embeddings.d.ts +15 -0
- package/dist/memory/embeddings.d.ts.map +1 -0
- package/dist/memory/embeddings.js +154 -0
- package/dist/memory/embeddings.js.map +1 -0
- package/dist/memory/index.d.ts +32 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +159 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/memory/search.d.ts +21 -0
- package/dist/memory/search.d.ts.map +1 -0
- package/dist/memory/search.js +77 -0
- package/dist/memory/search.js.map +1 -0
- package/dist/memory/store.d.ts +23 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/memory/store.js +144 -0
- package/dist/memory/store.js.map +1 -0
- package/dist/ollama-executor.d.ts +17 -0
- package/dist/ollama-executor.d.ts.map +1 -0
- package/dist/ollama-executor.js +112 -0
- package/dist/ollama-executor.js.map +1 -0
- package/dist/openai-executor.d.ts +38 -0
- package/dist/openai-executor.d.ts.map +1 -0
- package/dist/openai-executor.js +197 -0
- package/dist/openai-executor.js.map +1 -0
- package/dist/router.d.ts +11 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +185 -0
- package/dist/router.js.map +1 -0
- package/dist/test-message.d.ts +2 -0
- package/dist/test-message.d.ts.map +1 -0
- package/dist/test-message.js +60 -0
- package/dist/test-message.js.map +1 -0
- package/dist/utils/imsg-db-reader.d.ts +24 -0
- package/dist/utils/imsg-db-reader.d.ts.map +1 -0
- package/dist/utils/imsg-db-reader.js +92 -0
- package/dist/utils/imsg-db-reader.js.map +1 -0
- package/dist/utils/imsg-rpc.d.ts +25 -0
- package/dist/utils/imsg-rpc.d.ts.map +1 -0
- package/dist/utils/imsg-rpc.js +149 -0
- package/dist/utils/imsg-rpc.js.map +1 -0
- package/dist/utils/message-formatter.d.ts +3 -0
- package/dist/utils/message-formatter.d.ts.map +1 -0
- package/dist/utils/message-formatter.js +69 -0
- package/dist/utils/message-formatter.js.map +1 -0
- package/dist/web-ui.d.ts +12 -0
- package/dist/web-ui.d.ts.map +1 -0
- package/dist/web-ui.js +5784 -0
- package/dist/web-ui.js.map +1 -0
- package/dist/whatsapp-chats.d.ts +2 -0
- package/dist/whatsapp-chats.d.ts.map +1 -0
- package/dist/whatsapp-chats.js +76 -0
- package/dist/whatsapp-chats.js.map +1 -0
- package/dist/whatsapp-login.d.ts +2 -0
- package/dist/whatsapp-login.d.ts.map +1 -0
- package/dist/whatsapp-login.js +90 -0
- package/dist/whatsapp-login.js.map +1 -0
- package/dist/wiki-sync.d.ts +21 -0
- package/dist/wiki-sync.d.ts.map +1 -0
- package/dist/wiki-sync.js +147 -0
- package/dist/wiki-sync.js.map +1 -0
- package/docs/AddNewAgentGuide.md +100 -0
- package/docs/AddNewMcpGuide.md +72 -0
- package/docs/Architecture.md +795 -0
- package/docs/CLAUDE-AI-SETUP.md +166 -0
- package/docs/Setup.md +297 -0
- package/docs/ai-gym-architecture.md +1040 -0
- package/docs/ai-gym-build-plan.md +343 -0
- package/docs/ai-gym-onboarding.md +122 -0
- package/docs/appcreator_plan.md +348 -0
- package/docs/platform-mcp-audit.md +320 -0
- package/docs/server-deployment-plan.md +503 -0
- package/docs/superpowers/plans/2026-03-25-marketplace.md +1281 -0
- package/docs/superpowers/specs/2026-03-25-marketplace-design.md +287 -0
- package/docs/user-guide.md +2016 -0
- package/mcp-catalog.json +628 -0
- package/package.json +63 -0
- package/public/MyAIforOne-logomark-512.svg +16 -0
- package/public/MyAIforOne-logomark-transparent.svg +15 -0
- package/public/activity.html +314 -0
- package/public/admin.html +1674 -0
- package/public/agent-dashboard.html +670 -0
- package/public/api-docs.html +1106 -0
- package/public/automations.html +722 -0
- package/public/canvas.css +223 -0
- package/public/canvas.js +588 -0
- package/public/changelog.html +231 -0
- package/public/gym.html +2766 -0
- package/public/home.html +1930 -0
- package/public/index.html +2809 -0
- package/public/lab.html +1643 -0
- package/public/library.html +1442 -0
- package/public/marketplace.html +1101 -0
- package/public/mcp-docs.html +441 -0
- package/public/mini.html +390 -0
- package/public/monitor.html +584 -0
- package/public/org.html +4304 -0
- package/public/projects.html +734 -0
- package/public/settings.html +645 -0
- package/public/tasks.html +932 -0
- package/public/trainers/alex.svg +12 -0
- package/public/trainers/jordan.svg +12 -0
- package/public/trainers/morgan.svg +12 -0
- package/public/trainers/riley.svg +12 -0
- package/public/trainers/sam.svg +12 -0
- package/public/user-guide.html +218 -0
- package/registry/agents.json +3 -0
- package/registry/apps.json +20 -0
- package/registry/installed-drafts.json +3 -0
- package/registry/mcps.json +1084 -0
- package/registry/prompts/personal/mcp-test-prompt.md +6 -0
- package/registry/prompts/personal/memory-recall.md +6 -0
- package/registry/prompts/platform/brainstorm.md +15 -0
- package/registry/prompts/platform/code-review.md +16 -0
- package/registry/prompts/platform/explain.md +16 -0
- package/registry/prompts.json +58 -0
- package/registry/skills/external/brainstorming.md +5 -0
- package/registry/skills/external/code-review.md +40 -0
- package/registry/skills/external/frontend-patterns.md +642 -0
- package/registry/skills/external/frontend-slides.md +184 -0
- package/registry/skills/external/systematic-debugging.md +5 -0
- package/registry/skills/external/tdd.md +328 -0
- package/registry/skills/external/verification-before-completion.md +5 -0
- package/registry/skills/external/writing-plans.md +5 -0
- package/registry/skills/platform/ai41_app_build.md +930 -0
- package/registry/skills/platform/ai41_app_deploy.md +168 -0
- package/registry/skills/platform/ai41_app_orchestrator.md +239 -0
- package/registry/skills/platform/ai41_app_patterns.md +359 -0
- package/registry/skills/platform/ai41_app_register.md +85 -0
- package/registry/skills/platform/ai41_app_scaffold.md +421 -0
- package/registry/skills/platform/ai41_app_verify.md +107 -0
- package/registry/skills/platform/opProjectCreate.md +239 -0
- package/registry/skills/platform/op_devbrowser.md +136 -0
- package/registry/skills/platform/sop_brandguidelines.md +103 -0
- package/registry/skills/platform/sop_docx.md +117 -0
- package/registry/skills/platform/sop_frontenddesign.md +44 -0
- package/registry/skills/platform/sop_frontenddesign_v2.md +659 -0
- package/registry/skills/platform/sop_mcpbuilder.md +133 -0
- package/registry/skills/platform/sop_pdf.md +172 -0
- package/registry/skills/platform/sop_pptx.md +133 -0
- package/registry/skills/platform/sop_skillcreator.md +104 -0
- package/registry/skills/platform/sop_themefactory.md +128 -0
- package/registry/skills/platform/sop_webapptesting.md +75 -0
- package/registry/skills/platform/sop_webartifactsbuilder.md +97 -0
- package/registry/skills/platform/sop_xlsx.md +134 -0
- package/registry/skills.json +1055 -0
- package/scripts/discover-chats.sh +11 -0
- package/scripts/install-service-windows.ps1 +87 -0
- package/scripts/install-service.sh +52 -0
- package/scripts/seed-registry.ts +195 -0
- package/scripts/test-send.sh +5 -0
- package/scripts/tray-indicator.ps1 +35 -0
- package/scripts/uninstall-service-windows.ps1 +23 -0
- package/scripts/uninstall-service.sh +15 -0
- package/scripts/xbar-myagent.5s.sh +32 -0
- package/server/mcp-server/dist/index.d.ts +11 -0
- package/server/mcp-server/dist/index.js +1332 -0
- package/server/mcp-server/dist/lib/api-client.d.ts +165 -0
- package/server/mcp-server/dist/lib/api-client.js +241 -0
- package/server/mcp-server/index.ts +1545 -0
- package/server/mcp-server/lib/api-client.ts +366 -0
- package/server/mcp-server/tsconfig.json +14 -0
- package/src/agent-registry.ts +180 -0
- package/src/channels/discord.ts +129 -0
- package/src/channels/imessage.ts +261 -0
- package/src/channels/slack.ts +208 -0
- package/src/channels/telegram.ts +307 -0
- package/src/channels/types.ts +62 -0
- package/src/channels/whatsapp.ts +227 -0
- package/src/config.ts +281 -0
- package/src/cron.ts +43 -0
- package/src/decrypt-keys.ts +60 -0
- package/src/encrypt-keys.ts +70 -0
- package/src/executor.ts +2190 -0
- package/src/gemini-executor.ts +212 -0
- package/src/goals.ts +240 -0
- package/src/gym/activity-digest.ts +546 -0
- package/src/gym/dimension-scorer.ts +297 -0
- package/src/gym/gym-router.ts +801 -0
- package/src/gym/index.ts +19 -0
- package/src/heartbeat.ts +220 -0
- package/src/index.ts +275 -0
- package/src/keystore.ts +190 -0
- package/src/logger.ts +51 -0
- package/src/memory/daily.ts +101 -0
- package/src/memory/embeddings.ts +185 -0
- package/src/memory/index.ts +218 -0
- package/src/memory/search.ts +124 -0
- package/src/memory/store.ts +189 -0
- package/src/ollama-executor.ts +126 -0
- package/src/openai-executor.ts +259 -0
- package/src/router.ts +230 -0
- package/src/test-message.ts +72 -0
- package/src/utils/imsg-db-reader.ts +109 -0
- package/src/utils/imsg-rpc.ts +178 -0
- package/src/utils/message-formatter.ts +90 -0
- package/src/web-ui.ts +5778 -0
- package/src/whatsapp-chats.ts +91 -0
- package/src/whatsapp-login.ts +110 -0
- package/src/wiki-sync.ts +199 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,2809 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>MyAIforOne</title>
|
|
7
|
+
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&family=Syne:wght@600;700;800&display=swap" rel="stylesheet">
|
|
8
|
+
<script src="https://cdn.jsdelivr.net/npm/mammoth@1.8.0/mammoth.browser.min.js"></script>
|
|
9
|
+
<script src="https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js"></script>
|
|
10
|
+
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
|
|
11
|
+
<link rel="stylesheet" href="/canvas.css">
|
|
12
|
+
<script src="/canvas.js"></script>
|
|
13
|
+
<style>
|
|
14
|
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
15
|
+
|
|
16
|
+
/* ─── Theme Variables ─────────────────────────────────────────── */
|
|
17
|
+
:root{
|
|
18
|
+
--bg-deep:#060a13;
|
|
19
|
+
--bg-surface:rgba(12,18,33,0.92);
|
|
20
|
+
--bg-card:rgba(16,22,40,0.85);
|
|
21
|
+
--bg-input:rgba(0,0,0,0.35);
|
|
22
|
+
--border-dim:rgba(56,189,248,0.08);
|
|
23
|
+
--border-glow:rgba(56,189,248,0.18);
|
|
24
|
+
--border-active:rgba(56,189,248,0.45);
|
|
25
|
+
--text-primary:rgba(255,255,255,0.92);
|
|
26
|
+
--text-secondary:rgba(255,255,255,0.68);
|
|
27
|
+
--text-muted:rgba(148,163,184,0.55);
|
|
28
|
+
--accent:#22d3ee;
|
|
29
|
+
--accent-soft:#38bdf8;
|
|
30
|
+
--accent-bg:rgba(6,182,212,0.15);
|
|
31
|
+
--accent-glow:rgba(34,211,238,0.12);
|
|
32
|
+
--purple:rgba(139,92,246,0.7);
|
|
33
|
+
--purple-bg:rgba(139,92,246,0.12);
|
|
34
|
+
--green:#4ade80;
|
|
35
|
+
--green-bg:rgba(74,222,128,0.1);
|
|
36
|
+
--amber:#fbbf24;
|
|
37
|
+
--amber-bg:rgba(251,191,36,0.1);
|
|
38
|
+
--msg-user:rgba(56,189,248,0.08);
|
|
39
|
+
--msg-agent:rgba(139,92,246,0.06);
|
|
40
|
+
--shadow:0 2px 12px rgba(0,0,0,0.3);
|
|
41
|
+
--shadow-glow:0 0 20px rgba(34,211,238,0.08);
|
|
42
|
+
--radius:12px;
|
|
43
|
+
--font-sans:'DM Sans',system-ui,-apple-system,sans-serif;
|
|
44
|
+
--font-mono:'IBM Plex Mono',monospace;
|
|
45
|
+
--font-display:'Syne',sans-serif;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
[data-theme="light"]{
|
|
49
|
+
--bg-deep:#f4f6f9;
|
|
50
|
+
--bg-surface:rgba(255,255,255,0.95);
|
|
51
|
+
--bg-card:rgba(255,255,255,0.9);
|
|
52
|
+
--bg-input:rgba(0,0,0,0.04);
|
|
53
|
+
--border-dim:rgba(0,0,0,0.08);
|
|
54
|
+
--border-glow:rgba(14,116,144,0.18);
|
|
55
|
+
--border-active:rgba(14,116,144,0.45);
|
|
56
|
+
--text-primary:rgba(15,23,42,0.92);
|
|
57
|
+
--text-secondary:rgba(51,65,85,0.8);
|
|
58
|
+
--text-muted:rgba(100,116,139,0.6);
|
|
59
|
+
--accent:#0891b2;
|
|
60
|
+
--accent-soft:#0e7490;
|
|
61
|
+
--accent-bg:rgba(14,116,144,0.08);
|
|
62
|
+
--accent-glow:rgba(14,116,144,0.06);
|
|
63
|
+
--purple:rgba(109,40,217,0.75);
|
|
64
|
+
--purple-bg:rgba(139,92,246,0.08);
|
|
65
|
+
--green:#16a34a;
|
|
66
|
+
--green-bg:rgba(22,163,74,0.08);
|
|
67
|
+
--amber:#d97706;
|
|
68
|
+
--amber-bg:rgba(217,119,6,0.08);
|
|
69
|
+
--msg-user:rgba(14,116,144,0.06);
|
|
70
|
+
--msg-agent:rgba(139,92,246,0.04);
|
|
71
|
+
--shadow:0 1px 8px rgba(0,0,0,0.06);
|
|
72
|
+
--shadow-glow:none;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
html,body{
|
|
76
|
+
width:100%;height:100%;overflow:hidden;max-width:100vw;
|
|
77
|
+
background:var(--bg-deep);
|
|
78
|
+
font-family:var(--font-sans);
|
|
79
|
+
color:var(--text-primary);
|
|
80
|
+
transition:background .3s,color .3s;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* ─── Topbar ──────────────────────────────────────────────────── */
|
|
84
|
+
.topbar{height:48px;display:flex;align-items:center;padding:0 20px;background:var(--bg-surface);border-bottom:1px solid var(--border-dim);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);position:fixed;top:0;left:0;right:0;z-index:200;flex-shrink:0;overflow:hidden;max-width:100vw}
|
|
85
|
+
.topbar-logo{display:flex;align-items:center;gap:10px;margin-right:28px;flex-shrink:0}
|
|
86
|
+
.tab-group{display:flex;gap:0;flex-shrink:1;min-width:0}
|
|
87
|
+
.tab-btn{font-family:var(--font-sans);font-size:13px;font-weight:600;color:var(--text-muted);background:none;border:none;padding:14px 12px;cursor:pointer;position:relative;transition:color .2s;text-decoration:none;display:block;white-space:nowrap;flex-shrink:0}
|
|
88
|
+
.tab-btn:hover{color:var(--text-secondary)}
|
|
89
|
+
.tab-btn.active{color:var(--accent)}
|
|
90
|
+
.tab-btn.active::after{content:'';position:absolute;bottom:0;left:10px;right:10px;height:2px;background:var(--accent);border-radius:1px}
|
|
91
|
+
.topbar-right{margin-left:auto;display:flex;align-items:center;gap:8px;flex-shrink:0}
|
|
92
|
+
.gym-nav-btn{font-family:var(--font-sans);font-size:12px;font-weight:700;color:#fff;background:var(--accent);border:none;border-radius:8px;padding:6px 14px;cursor:pointer;text-decoration:none;letter-spacing:.02em;transition:opacity .2s,transform .2s;white-space:nowrap}
|
|
93
|
+
.gym-nav-btn:hover{opacity:.88;transform:translateY(-1px)}
|
|
94
|
+
|
|
95
|
+
/* ─── Layout ──────────────────────────────────────────────────── */
|
|
96
|
+
.app{display:flex;height:100vh;overflow:hidden;padding-top:48px;max-width:100vw}
|
|
97
|
+
|
|
98
|
+
.sidebar{
|
|
99
|
+
width:320px;min-width:320px;
|
|
100
|
+
background:var(--bg-surface);
|
|
101
|
+
border-right:1px solid var(--border-dim);
|
|
102
|
+
display:flex;flex-direction:column;
|
|
103
|
+
backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
|
|
104
|
+
transition:all .3s ease;overflow:hidden;
|
|
105
|
+
}
|
|
106
|
+
.sidebar.collapsed{width:0;min-width:0;border-right:none;padding:0}
|
|
107
|
+
.sidebar-expand{
|
|
108
|
+
position:fixed;top:12px;left:8px;z-index:50;
|
|
109
|
+
width:32px;height:32px;border-radius:8px;border:1px solid var(--border-dim);
|
|
110
|
+
background:var(--bg-surface);color:var(--text-muted);cursor:pointer;
|
|
111
|
+
display:none;align-items:center;justify-content:center;font-size:16px;
|
|
112
|
+
transition:all .2s;
|
|
113
|
+
}
|
|
114
|
+
.sidebar-expand:hover{border-color:var(--border-glow);color:var(--text-secondary)}
|
|
115
|
+
.sidebar-expand.visible{display:flex}
|
|
116
|
+
.sidebar-collapse{
|
|
117
|
+
padding:8px;border-top:1px solid var(--border-dim);
|
|
118
|
+
display:flex;align-items:center;justify-content:center;
|
|
119
|
+
}
|
|
120
|
+
.sidebar-collapse button{
|
|
121
|
+
width:100%;padding:6px;border-radius:8px;border:1px solid var(--border-dim);
|
|
122
|
+
background:transparent;color:var(--text-muted);cursor:pointer;
|
|
123
|
+
font-size:11px;font-family:var(--font-sans);transition:all .2s;
|
|
124
|
+
}
|
|
125
|
+
.sidebar-collapse button:hover{border-color:var(--border-glow);color:var(--text-secondary)}
|
|
126
|
+
|
|
127
|
+
.main{flex:1;display:flex;flex-direction:column;position:relative;min-width:0;overflow:hidden}
|
|
128
|
+
|
|
129
|
+
/* ─── Sidebar Header ─────────────────────────────────────────── */
|
|
130
|
+
.sidebar-header{
|
|
131
|
+
padding:20px 20px 16px;
|
|
132
|
+
border-bottom:1px solid var(--border-dim);
|
|
133
|
+
display:flex;align-items:center;justify-content:space-between;
|
|
134
|
+
}
|
|
135
|
+
.logo-group{display:flex;align-items:center;gap:10px}
|
|
136
|
+
.logo-mark{width:28px;height:28px;border-radius:8px;object-fit:contain}
|
|
137
|
+
.logo-text{font-family:var(--font-display);font-size:15px;font-weight:700;color:var(--accent)}
|
|
138
|
+
/* Docs dropdown */
|
|
139
|
+
.docs-dropdown{position:relative;display:inline-block}
|
|
140
|
+
.docs-btn{display:flex;align-items:center;justify-content:center;width:34px;height:34px;border-radius:8px;border:1px solid var(--border-dim);background:transparent;color:var(--text-muted);cursor:pointer;font-size:14px;transition:all .2s}
|
|
141
|
+
.docs-btn:hover{border-color:var(--border-glow);color:var(--text-secondary)}
|
|
142
|
+
.docs-menu{display:none;position:absolute;top:40px;right:0;min-width:140px;background:var(--bg-surface);border:1px solid var(--border-glow);border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,0.4);z-index:200;padding:4px;backdrop-filter:blur(20px)}
|
|
143
|
+
.docs-menu.open{display:block}
|
|
144
|
+
.docs-menu a{display:flex;align-items:center;gap:8px;padding:7px 12px;font-size:12px;font-weight:500;color:var(--text-secondary);text-decoration:none;border-radius:6px;transition:all .15s}
|
|
145
|
+
.docs-menu a:hover{background:var(--accent-bg);color:var(--accent)}
|
|
146
|
+
.docs-menu a .dm-icon{font-size:14px;width:18px;text-align:center}
|
|
147
|
+
.theme-toggle,.gear-btn{
|
|
148
|
+
width:34px;height:34px;border-radius:8px;border:1px solid var(--border-dim);
|
|
149
|
+
background:transparent;color:var(--text-muted);cursor:pointer;
|
|
150
|
+
display:flex;align-items:center;justify-content:center;font-size:16px;
|
|
151
|
+
transition:all .2s;
|
|
152
|
+
text-decoration:none;}
|
|
153
|
+
.theme-toggle:hover,.gear-btn:hover{border-color:var(--border-glow);color:var(--text-secondary)}
|
|
154
|
+
|
|
155
|
+
/* ─── Status Bar ──────────────────────────────────────────────── */
|
|
156
|
+
.status-bar{
|
|
157
|
+
padding:12px 20px;border-bottom:1px solid var(--border-dim);
|
|
158
|
+
display:flex;align-items:center;gap:12px;
|
|
159
|
+
}
|
|
160
|
+
.status-dot{
|
|
161
|
+
width:8px;height:8px;border-radius:50%;background:var(--green);
|
|
162
|
+
box-shadow:0 0 8px rgba(74,222,128,0.4);
|
|
163
|
+
animation:pulse 2s ease-in-out infinite;
|
|
164
|
+
}
|
|
165
|
+
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
|
|
166
|
+
.status-text{font-family:var(--font-mono);font-size:11px;color:var(--text-muted)}
|
|
167
|
+
.status-channels{margin-left:auto;display:flex;gap:4px}
|
|
168
|
+
.channel-pill{
|
|
169
|
+
font-family:var(--font-mono);font-size:9px;font-weight:500;
|
|
170
|
+
padding:2px 7px;border-radius:4px;
|
|
171
|
+
background:var(--accent-bg);color:var(--accent);
|
|
172
|
+
text-transform:uppercase;letter-spacing:.04em;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* ─── Filter Bar ──────────────────────────────────────────────── */
|
|
176
|
+
.filter-bar{
|
|
177
|
+
padding:8px 12px;border-bottom:1px solid var(--border-dim);
|
|
178
|
+
display:flex;gap:6px;
|
|
179
|
+
}
|
|
180
|
+
.filter-select{
|
|
181
|
+
flex:1;min-width:0;font-family:var(--font-mono);font-size:10px;
|
|
182
|
+
padding:5px 8px;border-radius:6px;
|
|
183
|
+
border:1px solid var(--border-dim);background:var(--bg-input);
|
|
184
|
+
color:var(--text-primary);outline:none;cursor:pointer;
|
|
185
|
+
transition:border-color .2s;overflow:hidden;text-overflow:ellipsis;
|
|
186
|
+
}
|
|
187
|
+
.filter-select:focus{border-color:var(--accent)}
|
|
188
|
+
|
|
189
|
+
/* ─── Agent List ──────────────────────────────────────────────── */
|
|
190
|
+
.agent-list{flex:1;overflow-y:auto;padding:8px 12px}
|
|
191
|
+
.agent-list::-webkit-scrollbar{width:4px}
|
|
192
|
+
.agent-list::-webkit-scrollbar-thumb{background:var(--border-glow);border-radius:2px}
|
|
193
|
+
|
|
194
|
+
/* Sidebar sections */
|
|
195
|
+
.sidebar-section{margin-bottom:12px}
|
|
196
|
+
.sidebar-section-header{
|
|
197
|
+
display:flex;align-items:center;gap:6px;padding:4px 4px;
|
|
198
|
+
font-family:var(--font-mono);font-size:9px;font-weight:600;
|
|
199
|
+
letter-spacing:.1em;text-transform:uppercase;color:var(--text-muted);
|
|
200
|
+
cursor:default;user-select:none;
|
|
201
|
+
}
|
|
202
|
+
.sidebar-section-header.collapsible{cursor:pointer}
|
|
203
|
+
.sidebar-section-header .section-arrow{font-size:8px;transition:transform .2s}
|
|
204
|
+
.sidebar-section.open .section-arrow{transform:rotate(90deg)}
|
|
205
|
+
.sidebar-section:not(.open) .section-body{display:none}
|
|
206
|
+
.sidebar-section-header .section-count{font-size:9px;color:var(--text-muted);opacity:.6;margin-left:auto}
|
|
207
|
+
.agent-card-compact{
|
|
208
|
+
padding:8px 10px;margin-bottom:4px;
|
|
209
|
+
border-radius:8px;cursor:pointer;transition:all .15s;
|
|
210
|
+
display:flex;align-items:center;gap:8px;
|
|
211
|
+
}
|
|
212
|
+
.agent-card-compact:hover{background:var(--bg-input)}
|
|
213
|
+
.agent-card-compact.selected{background:var(--accent-bg);border-left:2px solid var(--accent)}
|
|
214
|
+
.agent-card-compact .compact-avatar{
|
|
215
|
+
width:28px;height:28px;border-radius:7px;flex-shrink:0;
|
|
216
|
+
background:var(--accent-bg);border:1px solid var(--border-dim);
|
|
217
|
+
display:flex;align-items:center;justify-content:center;
|
|
218
|
+
font-family:var(--font-mono);font-size:10px;font-weight:600;color:var(--accent);
|
|
219
|
+
}
|
|
220
|
+
.agent-card-compact .compact-info{flex:1;min-width:0}
|
|
221
|
+
.agent-card-compact .compact-name{font-size:12px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
222
|
+
.agent-card-compact .compact-time{font-family:var(--font-mono);font-size:9px;color:var(--text-muted)}
|
|
223
|
+
.thinking-pulse{
|
|
224
|
+
width:8px;height:8px;border-radius:50%;background:var(--amber);flex-shrink:0;
|
|
225
|
+
animation:think-pulse 1.2s ease-in-out infinite;
|
|
226
|
+
}
|
|
227
|
+
@keyframes think-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.7)}}
|
|
228
|
+
|
|
229
|
+
.agent-card{
|
|
230
|
+
padding:14px 16px;margin-bottom:8px;
|
|
231
|
+
background:var(--bg-card);border:1px solid var(--border-dim);
|
|
232
|
+
border-radius:var(--radius);cursor:pointer;
|
|
233
|
+
backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);
|
|
234
|
+
box-shadow:var(--shadow);transition:all .25s;position:relative;
|
|
235
|
+
}
|
|
236
|
+
.agent-card:hover{
|
|
237
|
+
border-color:var(--border-glow);
|
|
238
|
+
box-shadow:var(--shadow),var(--shadow-glow);
|
|
239
|
+
transform:translateY(-1px);
|
|
240
|
+
}
|
|
241
|
+
.agent-card.selected{
|
|
242
|
+
border-color:var(--border-active);
|
|
243
|
+
box-shadow:0 0 20px rgba(34,211,238,0.12),0 0 40px rgba(34,211,238,0.06);
|
|
244
|
+
}
|
|
245
|
+
.agent-card-top{display:flex;align-items:center;gap:10px;margin-bottom:6px}
|
|
246
|
+
.agent-avatar{
|
|
247
|
+
width:36px;height:36px;border-radius:10px;
|
|
248
|
+
background:var(--accent-bg);border:1px solid var(--accent);
|
|
249
|
+
display:flex;align-items:center;justify-content:center;
|
|
250
|
+
font-family:var(--font-mono);font-size:12px;font-weight:600;color:var(--accent);
|
|
251
|
+
flex-shrink:0;
|
|
252
|
+
}
|
|
253
|
+
.agent-name{font-family:var(--font-display);font-size:14px;font-weight:700;flex:1}
|
|
254
|
+
.agent-session-dot{
|
|
255
|
+
width:8px;height:8px;border-radius:50%;flex-shrink:0;
|
|
256
|
+
}
|
|
257
|
+
.agent-session-dot.active{background:var(--green);box-shadow:0 0 6px rgba(74,222,128,0.4)}
|
|
258
|
+
.agent-session-dot.inactive{background:var(--text-muted)}
|
|
259
|
+
.agent-alias{
|
|
260
|
+
font-family:var(--font-mono);font-size:11px;color:var(--accent);
|
|
261
|
+
margin-bottom:4px;display:flex;justify-content:space-between;align-items:center;
|
|
262
|
+
}
|
|
263
|
+
.agent-last-msg{
|
|
264
|
+
font-family:var(--font-sans);font-size:10px;color:var(--text-muted);font-weight:400;
|
|
265
|
+
}
|
|
266
|
+
.agent-desc{font-size:12px;color:var(--text-muted);line-height:1.5;margin-bottom:8px}
|
|
267
|
+
.agent-meta{display:flex;gap:6px;flex-wrap:wrap}
|
|
268
|
+
.agent-tag{
|
|
269
|
+
font-family:var(--font-mono);font-size:9px;font-weight:500;
|
|
270
|
+
padding:2px 7px;border-radius:4px;
|
|
271
|
+
border:1px solid;text-transform:uppercase;letter-spacing:.04em;
|
|
272
|
+
}
|
|
273
|
+
.tag-persistent{border-color:rgba(74,222,128,0.2);background:var(--green-bg);color:var(--green)}
|
|
274
|
+
.tag-msgs{border-color:var(--border-dim);background:transparent;color:var(--text-muted)}
|
|
275
|
+
.tag-mcp{border-color:rgba(139,92,246,0.2);background:var(--purple-bg);color:var(--purple)}
|
|
276
|
+
|
|
277
|
+
/* ─── Add Agent Button ────────────────────────────────────────── */
|
|
278
|
+
.add-agent-btn{
|
|
279
|
+
margin:8px 12px 16px;padding:12px;
|
|
280
|
+
border:1px dashed var(--border-glow);border-radius:var(--radius);
|
|
281
|
+
background:transparent;color:var(--text-muted);cursor:pointer;
|
|
282
|
+
font-family:var(--font-sans);font-size:13px;font-weight:500;
|
|
283
|
+
display:flex;align-items:center;justify-content:center;gap:8px;
|
|
284
|
+
transition:all .2s;
|
|
285
|
+
}
|
|
286
|
+
.add-agent-btn:hover{border-color:var(--accent);color:var(--accent);background:var(--accent-bg)}
|
|
287
|
+
|
|
288
|
+
/* ─── Main: Empty State ───────────────────────────────────────── */
|
|
289
|
+
.empty-state{
|
|
290
|
+
flex:1;display:flex;flex-direction:column;
|
|
291
|
+
align-items:center;justify-content:center;gap:12px;
|
|
292
|
+
color:var(--text-muted);
|
|
293
|
+
}
|
|
294
|
+
.empty-icon{font-size:48px;opacity:.3}
|
|
295
|
+
.empty-text{font-size:14px;font-weight:500}
|
|
296
|
+
.empty-sub{font-size:12px;opacity:.6}
|
|
297
|
+
|
|
298
|
+
/* ─── Main: Chat View ─────────────────────────────────────────── */
|
|
299
|
+
.chat-header{
|
|
300
|
+
padding:16px 24px;
|
|
301
|
+
border-bottom:1px solid var(--border-dim);
|
|
302
|
+
background:var(--bg-surface);
|
|
303
|
+
backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
|
|
304
|
+
display:flex;align-items:center;gap:12px;
|
|
305
|
+
}
|
|
306
|
+
.chat-header-info{flex:1}
|
|
307
|
+
.chat-header-name{font-family:var(--font-display);font-size:16px;font-weight:700}
|
|
308
|
+
.chat-header-alias{font-family:var(--font-mono);font-size:11px;color:var(--accent)}
|
|
309
|
+
.chat-header-actions{display:flex;gap:8px}
|
|
310
|
+
.header-btn{
|
|
311
|
+
font-family:var(--font-mono);font-size:10px;font-weight:500;
|
|
312
|
+
padding:6px 12px;border-radius:8px;
|
|
313
|
+
border:1px solid var(--border-dim);background:transparent;
|
|
314
|
+
color:var(--text-muted);cursor:pointer;transition:all .2s;
|
|
315
|
+
}
|
|
316
|
+
.header-btn:hover{border-color:var(--border-glow);color:var(--text-secondary)}
|
|
317
|
+
.header-btn.danger:hover{border-color:rgba(239,68,68,.3);color:#ef4444}
|
|
318
|
+
|
|
319
|
+
/* ─── Session Tabs ──────────────────────────────────────────── */
|
|
320
|
+
.session-tabs{
|
|
321
|
+
display:flex;align-items:center;gap:0;
|
|
322
|
+
padding:0 16px;
|
|
323
|
+
border-bottom:1px solid var(--border-dim);
|
|
324
|
+
background:var(--bg-surface);
|
|
325
|
+
min-height:32px;
|
|
326
|
+
overflow-x:auto;
|
|
327
|
+
scrollbar-width:none;
|
|
328
|
+
}
|
|
329
|
+
.session-tabs::-webkit-scrollbar{display:none}
|
|
330
|
+
.session-tab{
|
|
331
|
+
display:flex;align-items:center;gap:4px;
|
|
332
|
+
padding:6px 10px;
|
|
333
|
+
font-family:var(--font-mono);font-size:11px;
|
|
334
|
+
color:var(--text-muted);cursor:pointer;
|
|
335
|
+
border-bottom:2px solid transparent;
|
|
336
|
+
white-space:nowrap;transition:all .15s;
|
|
337
|
+
position:relative;
|
|
338
|
+
}
|
|
339
|
+
.session-tab:hover{color:var(--text-secondary);background:rgba(255,255,255,.03)}
|
|
340
|
+
.session-tab.active{
|
|
341
|
+
color:var(--accent);border-bottom-color:var(--accent);
|
|
342
|
+
background:rgba(var(--amber-rgb,245,158,11),.05);
|
|
343
|
+
}
|
|
344
|
+
.session-tab .tab-label{max-width:120px;overflow:hidden;text-overflow:ellipsis}
|
|
345
|
+
.session-tab .tab-close{
|
|
346
|
+
display:none;font-size:13px;line-height:1;
|
|
347
|
+
padding:0 2px;border-radius:3px;color:var(--text-muted);
|
|
348
|
+
cursor:pointer;margin-left:2px;
|
|
349
|
+
}
|
|
350
|
+
.session-tab:hover .tab-close{display:inline}
|
|
351
|
+
.session-tab .tab-close:hover{color:#ef4444;background:rgba(239,68,68,.15)}
|
|
352
|
+
.session-tab .tab-dot{
|
|
353
|
+
width:5px;height:5px;border-radius:50%;
|
|
354
|
+
background:var(--accent);display:none;
|
|
355
|
+
}
|
|
356
|
+
.session-tab.has-activity .tab-dot{display:inline-block}
|
|
357
|
+
.session-tab-add{
|
|
358
|
+
padding:4px 8px;font-size:14px;line-height:1;
|
|
359
|
+
color:var(--text-muted);cursor:pointer;
|
|
360
|
+
border-radius:4px;transition:all .15s;
|
|
361
|
+
flex-shrink:0;
|
|
362
|
+
}
|
|
363
|
+
.session-tab-add:hover{color:var(--accent);background:rgba(255,255,255,.05)}
|
|
364
|
+
.tab-rename-input{
|
|
365
|
+
font-family:var(--font-mono);font-size:11px;
|
|
366
|
+
background:transparent;border:1px solid var(--accent);
|
|
367
|
+
color:var(--text-primary);padding:1px 4px;border-radius:3px;
|
|
368
|
+
outline:none;width:90px;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/* ─── Session Picker Modal ───────────────────────────────── */
|
|
372
|
+
.picker-session{
|
|
373
|
+
padding:12px 20px;cursor:pointer;border-bottom:1px solid var(--border-dim);
|
|
374
|
+
transition:background .15s;
|
|
375
|
+
}
|
|
376
|
+
.picker-session:last-child{border-bottom:none}
|
|
377
|
+
.picker-session:hover{background:rgba(255,255,255,.04)}
|
|
378
|
+
.picker-session.picker-active{background:rgba(var(--amber-rgb,245,158,11),.06)}
|
|
379
|
+
.picker-session-name{font-size:13px;font-weight:600;color:var(--text-primary);display:flex;align-items:center;gap:8px}
|
|
380
|
+
.picker-session-meta{font-size:11px;color:var(--text-muted);margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
381
|
+
.picker-badge{font-size:9px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;padding:1px 5px;border-radius:3px;background:rgba(var(--amber-rgb,245,158,11),.15);color:var(--accent);}
|
|
382
|
+
#sessionPickerModal .modal{max-height:70vh;display:flex;flex-direction:column}
|
|
383
|
+
#sessionPickerList{overflow-y:auto;flex:1}
|
|
384
|
+
.picker-footer{padding:12px 20px;border-top:1px solid var(--border-dim)}
|
|
385
|
+
.picker-new-btn{
|
|
386
|
+
width:100%;padding:8px 16px;border-radius:8px;
|
|
387
|
+
border:1px dashed var(--border-glow);background:transparent;
|
|
388
|
+
color:var(--text-muted);font-size:12px;cursor:pointer;transition:all .15s;
|
|
389
|
+
}
|
|
390
|
+
.picker-new-btn:hover{border-color:var(--accent);color:var(--accent);background:rgba(var(--amber-rgb,245,158,11),.05)}
|
|
391
|
+
|
|
392
|
+
.chat-messages{
|
|
393
|
+
flex:1;overflow-y:auto;padding:20px 24px;
|
|
394
|
+
display:flex;flex-direction:column;gap:12px;
|
|
395
|
+
}
|
|
396
|
+
.chat-messages::-webkit-scrollbar{width:4px}
|
|
397
|
+
.chat-messages::-webkit-scrollbar-thumb{background:var(--border-glow);border-radius:2px}
|
|
398
|
+
|
|
399
|
+
.msg{
|
|
400
|
+
max-width:80%;padding:12px 16px;border-radius:14px;
|
|
401
|
+
font-size:13px;line-height:1.6;
|
|
402
|
+
animation:msgIn .3s ease-out;
|
|
403
|
+
}
|
|
404
|
+
@keyframes msgIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
|
|
405
|
+
.msg-user{
|
|
406
|
+
align-self:flex-end;
|
|
407
|
+
background:var(--msg-user);border:1px solid var(--border-glow);
|
|
408
|
+
border-bottom-right-radius:4px;
|
|
409
|
+
}
|
|
410
|
+
.msg-agent{
|
|
411
|
+
align-self:flex-start;
|
|
412
|
+
background:var(--msg-agent);border:1px solid rgba(139,92,246,0.1);
|
|
413
|
+
border-bottom-left-radius:4px;
|
|
414
|
+
}
|
|
415
|
+
.msg-agent pre{
|
|
416
|
+
background:var(--bg-input);border:1px solid var(--border-dim);
|
|
417
|
+
border-radius:8px;padding:10px 12px;margin:8px 0;
|
|
418
|
+
overflow-x:auto;font-family:var(--font-mono);font-size:12px;line-height:1.5;
|
|
419
|
+
}
|
|
420
|
+
.msg-agent code{font-family:var(--font-mono);font-size:12px;background:var(--bg-input);padding:1px 5px;border-radius:4px}
|
|
421
|
+
.msg-agent p{margin:0 0 .6em 0}.msg-agent p:last-child{margin-bottom:0}
|
|
422
|
+
.channel-badge{
|
|
423
|
+
display:inline-block;background:rgba(139,92,246,0.15);color:var(--accent);
|
|
424
|
+
padding:1px 5px;border-radius:3px;font-size:8px;font-weight:600;
|
|
425
|
+
text-transform:uppercase;letter-spacing:.05em;margin-right:4px;
|
|
426
|
+
}
|
|
427
|
+
.msg-time{
|
|
428
|
+
font-family:var(--font-mono);font-size:9px;color:var(--text-muted);
|
|
429
|
+
margin-top:4px;
|
|
430
|
+
}
|
|
431
|
+
.msg-system{
|
|
432
|
+
align-self:center;
|
|
433
|
+
font-family:var(--font-mono);font-size:11px;color:var(--text-muted);
|
|
434
|
+
padding:6px 14px;background:var(--bg-input);border-radius:20px;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.thinking{
|
|
438
|
+
align-self:flex-start;display:flex;align-items:center;gap:8px;
|
|
439
|
+
padding:12px 18px;background:var(--msg-agent);border:1px solid rgba(139,92,246,0.1);
|
|
440
|
+
border-radius:14px;border-bottom-left-radius:4px;
|
|
441
|
+
}
|
|
442
|
+
.thinking-dots{display:flex;gap:4px}
|
|
443
|
+
.thinking-dots span{
|
|
444
|
+
width:6px;height:6px;border-radius:50%;background:var(--purple);
|
|
445
|
+
animation:bounce 1.4s ease-in-out infinite;
|
|
446
|
+
}
|
|
447
|
+
.thinking-dots span:nth-child(2){animation-delay:.2s}
|
|
448
|
+
.thinking-dots span:nth-child(3){animation-delay:.4s}
|
|
449
|
+
@keyframes bounce{0%,80%,100%{transform:scale(0.6);opacity:.4}40%{transform:scale(1);opacity:1}}
|
|
450
|
+
.thinking-text{font-size:12px;color:var(--text-muted)}
|
|
451
|
+
|
|
452
|
+
/* ─── Streaming Activity Feed ─────────────────────────────────── */
|
|
453
|
+
.activity-feed{
|
|
454
|
+
align-self:flex-start;max-width:85%;
|
|
455
|
+
background:var(--bg-input);border:1px solid var(--border-dim);
|
|
456
|
+
border-radius:12px;padding:10px 14px;margin-bottom:4px;
|
|
457
|
+
animation:msgIn .2s ease-out;
|
|
458
|
+
}
|
|
459
|
+
.activity-line{
|
|
460
|
+
display:flex;align-items:center;gap:6px;
|
|
461
|
+
font-family:var(--font-mono);font-size:11px;
|
|
462
|
+
padding:3px 0;color:var(--text-muted);line-height:1.4;
|
|
463
|
+
}
|
|
464
|
+
.activity-line:last-child{color:var(--text-secondary)}
|
|
465
|
+
.activity-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}
|
|
466
|
+
.activity-dot.done{background:var(--green)}
|
|
467
|
+
.activity-dot.active{background:var(--accent);animation:pulse 1s ease-in-out infinite}
|
|
468
|
+
.activity-dot.pending{background:var(--text-muted);opacity:.3}
|
|
469
|
+
.activity-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
470
|
+
.activity-feed.collapsed .activity-lines{display:none}
|
|
471
|
+
.activity-feed.collapsed{padding:6px 12px}
|
|
472
|
+
.activity-header{
|
|
473
|
+
font-family:var(--font-mono);font-size:9px;font-weight:600;
|
|
474
|
+
text-transform:uppercase;letter-spacing:.08em;color:var(--accent);user-select:none;
|
|
475
|
+
margin-bottom:4px;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/* ─── Chat Input ──────────────────────────────────────────────── */
|
|
479
|
+
.chat-input-wrap{
|
|
480
|
+
padding:16px 24px 20px;
|
|
481
|
+
border-top:1px solid var(--border-dim);
|
|
482
|
+
background:var(--bg-surface);
|
|
483
|
+
backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
|
|
484
|
+
}
|
|
485
|
+
.chat-input-box{
|
|
486
|
+
display:flex;gap:10px;align-items:flex-end;
|
|
487
|
+
}
|
|
488
|
+
.chat-textarea{
|
|
489
|
+
flex:1;resize:none;
|
|
490
|
+
font-family:var(--font-sans);font-size:14px;
|
|
491
|
+
padding:12px 16px;border-radius:12px;
|
|
492
|
+
border:1px solid var(--border-dim);
|
|
493
|
+
background:var(--bg-input);color:var(--text-primary);
|
|
494
|
+
outline:none;min-height:44px;max-height:140px;
|
|
495
|
+
transition:border-color .2s;line-height:1.5;
|
|
496
|
+
}
|
|
497
|
+
.chat-textarea::placeholder{color:var(--text-muted)}
|
|
498
|
+
.chat-textarea:focus{border-color:var(--accent)}
|
|
499
|
+
.send-btn{
|
|
500
|
+
width:44px;height:44px;border-radius:10px;border:none;
|
|
501
|
+
background:var(--accent);color:#fff;cursor:pointer;
|
|
502
|
+
display:flex;align-items:center;justify-content:center;
|
|
503
|
+
font-size:18px;transition:all .2s;flex-shrink:0;
|
|
504
|
+
}
|
|
505
|
+
.send-btn:hover{filter:brightness(1.1);transform:scale(1.05)}
|
|
506
|
+
.send-btn:disabled{opacity:.4;cursor:not-allowed;transform:none}
|
|
507
|
+
.mic-btn{
|
|
508
|
+
width:44px;height:44px;border-radius:10px;border:1px solid var(--border-dim);
|
|
509
|
+
background:transparent;color:var(--text-muted);cursor:pointer;
|
|
510
|
+
display:flex;align-items:center;justify-content:center;
|
|
511
|
+
font-size:18px;transition:all .2s;flex-shrink:0;
|
|
512
|
+
}
|
|
513
|
+
.mic-btn:hover{border-color:var(--border-glow);color:var(--text-secondary)}
|
|
514
|
+
.mic-btn.recording{
|
|
515
|
+
border-color:rgba(239,68,68,.5);background:rgba(239,68,68,.1);color:#ef4444;
|
|
516
|
+
animation:micPulse 1s ease-in-out infinite;
|
|
517
|
+
}
|
|
518
|
+
@keyframes micPulse{0%,100%{opacity:1}50%{opacity:.6}}
|
|
519
|
+
.clip-btn{
|
|
520
|
+
width:44px;height:44px;border-radius:10px;border:1px solid var(--border-dim);
|
|
521
|
+
background:transparent;color:var(--text-muted);cursor:pointer;
|
|
522
|
+
display:flex;align-items:center;justify-content:center;
|
|
523
|
+
font-size:18px;transition:all .2s;flex-shrink:0;
|
|
524
|
+
}
|
|
525
|
+
.clip-btn:hover{border-color:var(--border-glow);color:var(--text-secondary)}
|
|
526
|
+
.clip-btn.has-files{border-color:var(--accent);color:var(--accent)}
|
|
527
|
+
.download-btn{
|
|
528
|
+
display:inline-flex;align-items:center;gap:4px;
|
|
529
|
+
padding:3px 10px;margin:2px 0;border-radius:6px;
|
|
530
|
+
border:1px solid var(--border-glow);
|
|
531
|
+
background:var(--accent-bg);color:var(--accent);
|
|
532
|
+
font-family:var(--font-sans);font-size:11px;font-weight:500;
|
|
533
|
+
cursor:pointer;transition:all .2s;vertical-align:middle;
|
|
534
|
+
}
|
|
535
|
+
.download-btn:hover{background:var(--accent);color:#000;border-color:var(--accent)}
|
|
536
|
+
/* ─── Recent Files Bar ────────────────────────────────────────── */
|
|
537
|
+
.recent-files-bar{
|
|
538
|
+
display:flex;align-items:center;gap:6px;padding:4px 12px;
|
|
539
|
+
border-top:1px solid var(--border-dim);background:var(--bg-surface);
|
|
540
|
+
overflow-x:auto;flex-shrink:0;
|
|
541
|
+
}
|
|
542
|
+
.recent-files-bar:empty{display:none}
|
|
543
|
+
.recent-files-label{
|
|
544
|
+
font-family:var(--font-mono);font-size:9px;font-weight:600;
|
|
545
|
+
color:var(--text-muted);text-transform:uppercase;letter-spacing:.04em;
|
|
546
|
+
white-space:nowrap;flex-shrink:0;
|
|
547
|
+
}
|
|
548
|
+
.recent-file-chip{
|
|
549
|
+
display:inline-flex;align-items:center;gap:4px;
|
|
550
|
+
padding:2px 8px;border-radius:5px;
|
|
551
|
+
border:1px solid var(--border-dim);background:var(--bg-input);
|
|
552
|
+
font-family:var(--font-mono);font-size:10px;color:var(--text-secondary);
|
|
553
|
+
cursor:pointer;transition:all .15s;white-space:nowrap;flex-shrink:0;
|
|
554
|
+
}
|
|
555
|
+
.recent-file-chip:hover{border-color:var(--border-glow);color:var(--text-primary);background:var(--accent-bg)}
|
|
556
|
+
.recent-file-chip .chip-icon{font-size:12px}
|
|
557
|
+
.recent-file-chip .chip-folder{font-size:10px;opacity:.5;margin-left:2px;cursor:pointer}
|
|
558
|
+
.recent-file-chip .chip-folder:hover{opacity:1}
|
|
559
|
+
|
|
560
|
+
.tool-cards{margin-bottom:8px;display:flex;flex-direction:column;gap:4px}
|
|
561
|
+
.tool-card{
|
|
562
|
+
display:flex;align-items:center;gap:6px;
|
|
563
|
+
padding:4px 10px;border-radius:6px;
|
|
564
|
+
background:var(--purple-bg);border:1px solid rgba(139,92,246,0.15);
|
|
565
|
+
font-size:11px;color:var(--text-muted);
|
|
566
|
+
font-family:var(--font-mono);overflow:hidden;
|
|
567
|
+
}
|
|
568
|
+
.tool-name{
|
|
569
|
+
color:var(--purple);font-weight:600;white-space:nowrap;flex-shrink:0;
|
|
570
|
+
}
|
|
571
|
+
.tool-cmd{
|
|
572
|
+
background:rgba(0,0,0,0.2);padding:1px 5px;border-radius:3px;
|
|
573
|
+
color:var(--text-secondary);font-size:10px;
|
|
574
|
+
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:400px;
|
|
575
|
+
}
|
|
576
|
+
.tool-detail{
|
|
577
|
+
color:var(--text-muted);font-size:10px;
|
|
578
|
+
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:400px;
|
|
579
|
+
}
|
|
580
|
+
.file-drop-zone{
|
|
581
|
+
display:none;padding:12px 16px;margin-bottom:10px;
|
|
582
|
+
border:2px dashed var(--border-glow);border-radius:12px;
|
|
583
|
+
background:var(--bg-input);text-align:center;
|
|
584
|
+
font-size:12px;color:var(--text-muted);cursor:pointer;
|
|
585
|
+
transition:all .2s;min-height:60px;
|
|
586
|
+
display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;
|
|
587
|
+
}
|
|
588
|
+
.file-drop-zone.hidden{display:none}
|
|
589
|
+
.file-drop-zone.dragover{border-color:var(--accent);background:var(--accent-bg);color:var(--accent)}
|
|
590
|
+
.file-drop-zone input[type=file]{display:none}
|
|
591
|
+
.file-list{display:flex;flex-wrap:wrap;gap:6px;width:100%;justify-content:flex-start}
|
|
592
|
+
.file-pill{
|
|
593
|
+
display:inline-flex;align-items:center;gap:6px;
|
|
594
|
+
padding:5px 10px;border-radius:8px;
|
|
595
|
+
background:var(--bg-surface);border:1px solid var(--border-dim);
|
|
596
|
+
font-family:var(--font-mono);font-size:10px;color:var(--text-primary);
|
|
597
|
+
max-width:200px;
|
|
598
|
+
}
|
|
599
|
+
.file-pill-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
|
|
600
|
+
.file-pill-size{color:var(--text-muted);flex-shrink:0}
|
|
601
|
+
.file-pill-remove{
|
|
602
|
+
cursor:pointer;color:var(--text-muted);font-size:14px;flex-shrink:0;
|
|
603
|
+
transition:color .2s;
|
|
604
|
+
}
|
|
605
|
+
.file-pill-remove:hover{color:#ef4444}
|
|
606
|
+
.file-pill-store{
|
|
607
|
+
cursor:pointer;font-size:12px;flex-shrink:0;opacity:.5;
|
|
608
|
+
transition:opacity .2s;
|
|
609
|
+
}
|
|
610
|
+
.file-pill-store.active{opacity:1}
|
|
611
|
+
.file-pill-store:hover{opacity:1}
|
|
612
|
+
|
|
613
|
+
/* ─── Modal ───────────────────────────────────────────────────── */
|
|
614
|
+
.modal-overlay{
|
|
615
|
+
position:fixed;inset:0;z-index:1000;
|
|
616
|
+
background:rgba(0,0,0,0.6);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);
|
|
617
|
+
display:none;align-items:center;justify-content:center;
|
|
618
|
+
animation:fadeIn .2s ease-out;
|
|
619
|
+
}
|
|
620
|
+
.modal-overlay.show{display:flex}
|
|
621
|
+
@keyframes fadeIn{from{opacity:0}to{opacity:1}}
|
|
622
|
+
|
|
623
|
+
.modal{
|
|
624
|
+
width:520px;max-width:92vw;max-height:85vh;overflow-y:auto;
|
|
625
|
+
background:var(--bg-surface);border:1px solid var(--border-glow);
|
|
626
|
+
border-radius:16px;padding:0;
|
|
627
|
+
box-shadow:0 8px 40px rgba(0,0,0,0.4),0 0 60px rgba(34,211,238,0.06);
|
|
628
|
+
animation:modalIn .25s ease-out;
|
|
629
|
+
}
|
|
630
|
+
@keyframes modalIn{from{opacity:0;transform:scale(.96) translateY(8px)}to{opacity:1;transform:none}}
|
|
631
|
+
|
|
632
|
+
.modal-header{
|
|
633
|
+
display:flex;align-items:center;justify-content:space-between;
|
|
634
|
+
padding:20px 24px 16px;border-bottom:1px solid var(--border-dim);
|
|
635
|
+
}
|
|
636
|
+
.modal-title{font-family:var(--font-display);font-size:18px;font-weight:700}
|
|
637
|
+
.modal-close{
|
|
638
|
+
width:32px;height:32px;border-radius:8px;border:1px solid var(--border-dim);
|
|
639
|
+
background:transparent;color:var(--text-muted);cursor:pointer;
|
|
640
|
+
display:flex;align-items:center;justify-content:center;font-size:16px;
|
|
641
|
+
transition:all .2s;
|
|
642
|
+
}
|
|
643
|
+
.modal-close:hover{border-color:var(--border-glow);color:var(--text-secondary)}
|
|
644
|
+
|
|
645
|
+
.modal-body{padding:20px 24px}
|
|
646
|
+
|
|
647
|
+
.form-group{margin-bottom:16px}
|
|
648
|
+
.form-label{
|
|
649
|
+
display:block;font-family:var(--font-mono);font-size:10px;font-weight:600;
|
|
650
|
+
text-transform:uppercase;letter-spacing:.08em;color:var(--text-muted);
|
|
651
|
+
margin-bottom:6px;
|
|
652
|
+
}
|
|
653
|
+
.form-input{
|
|
654
|
+
width:100%;padding:10px 14px;border-radius:10px;
|
|
655
|
+
border:1px solid var(--border-dim);background:var(--bg-input);
|
|
656
|
+
color:var(--text-primary);font-family:var(--font-sans);font-size:13px;
|
|
657
|
+
outline:none;transition:border-color .2s;
|
|
658
|
+
}
|
|
659
|
+
.form-input:focus{border-color:var(--accent)}
|
|
660
|
+
.form-input::placeholder{color:var(--text-muted)}
|
|
661
|
+
.form-hint{font-size:11px;color:var(--text-muted);margin-top:4px}
|
|
662
|
+
|
|
663
|
+
.form-row{display:flex;gap:12px}
|
|
664
|
+
.form-row .form-group{flex:1}
|
|
665
|
+
|
|
666
|
+
.form-section{
|
|
667
|
+
font-family:var(--font-mono);font-size:10px;font-weight:600;
|
|
668
|
+
text-transform:uppercase;letter-spacing:.12em;color:var(--accent);
|
|
669
|
+
margin:20px 0 12px;padding-bottom:6px;
|
|
670
|
+
border-bottom:1px solid var(--border-dim);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
.checkbox-group{display:flex;flex-wrap:wrap;gap:6px}
|
|
674
|
+
.checkbox-pill{
|
|
675
|
+
font-family:var(--font-mono);font-size:10px;font-weight:500;
|
|
676
|
+
padding:5px 10px;border-radius:6px;cursor:pointer;
|
|
677
|
+
border:1px solid var(--border-dim);background:transparent;
|
|
678
|
+
color:var(--text-muted);transition:all .2s;user-select:none;
|
|
679
|
+
}
|
|
680
|
+
.checkbox-pill:hover{border-color:var(--border-glow);color:var(--text-secondary)}
|
|
681
|
+
.checkbox-pill.checked{
|
|
682
|
+
border-color:var(--accent);background:var(--accent-bg);color:var(--accent);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
.modal-footer{
|
|
686
|
+
display:flex;justify-content:space-between;align-items:center;
|
|
687
|
+
padding:16px 24px 20px;border-top:1px solid var(--border-dim);
|
|
688
|
+
}
|
|
689
|
+
.modal-footer-hint{font-size:11px;color:var(--text-muted)}
|
|
690
|
+
.btn-create{
|
|
691
|
+
padding:10px 24px;border-radius:10px;border:none;
|
|
692
|
+
background:var(--accent);color:#fff;cursor:pointer;
|
|
693
|
+
font-family:var(--font-sans);font-size:13px;font-weight:600;
|
|
694
|
+
transition:all .2s;
|
|
695
|
+
}
|
|
696
|
+
.btn-create:hover{filter:brightness(1.1);transform:scale(1.02)}
|
|
697
|
+
.btn-create:disabled{opacity:.5;cursor:not-allowed;transform:none}
|
|
698
|
+
|
|
699
|
+
.route-entry{
|
|
700
|
+
display:flex;gap:8px;align-items:center;margin-bottom:8px;
|
|
701
|
+
}
|
|
702
|
+
.route-entry select,.route-entry input{
|
|
703
|
+
padding:8px 10px;border-radius:8px;
|
|
704
|
+
border:1px solid var(--border-dim);background:var(--bg-input);
|
|
705
|
+
color:var(--text-primary);font-family:var(--font-mono);font-size:12px;
|
|
706
|
+
outline:none;
|
|
707
|
+
}
|
|
708
|
+
.route-entry select{width:110px}
|
|
709
|
+
.route-entry input{flex:1}
|
|
710
|
+
.route-remove{
|
|
711
|
+
width:28px;height:28px;border-radius:6px;border:1px solid var(--border-dim);
|
|
712
|
+
background:transparent;color:var(--text-muted);cursor:pointer;
|
|
713
|
+
display:flex;align-items:center;justify-content:center;font-size:14px;
|
|
714
|
+
}
|
|
715
|
+
.route-remove:hover{border-color:rgba(239,68,68,.3);color:#ef4444}
|
|
716
|
+
.add-route-btn{
|
|
717
|
+
font-family:var(--font-mono);font-size:11px;color:var(--accent);
|
|
718
|
+
background:none;border:none;cursor:pointer;padding:4px 0;
|
|
719
|
+
}
|
|
720
|
+
.add-route-btn:hover{text-decoration:underline}
|
|
721
|
+
|
|
722
|
+
/* ─── Scrollbar ───────────────────────────────────────────────── */
|
|
723
|
+
::-webkit-scrollbar{width:6px}
|
|
724
|
+
::-webkit-scrollbar-track{background:transparent}
|
|
725
|
+
::-webkit-scrollbar-thumb{background:var(--border-glow);border-radius:3px}
|
|
726
|
+
|
|
727
|
+
/* ─── Hide Names (presentation mode) ────────────────────────── */
|
|
728
|
+
.name-blurred{filter:blur(8px);-webkit-filter:blur(8px);transition:filter .2s}
|
|
729
|
+
|
|
730
|
+
/* ─── Stop Button ────────────────────────────────────────────── */
|
|
731
|
+
.stop-btn{
|
|
732
|
+
width:44px;height:44px;border-radius:10px;border:none;
|
|
733
|
+
background:rgba(239,68,68,0.85);color:#fff;cursor:pointer;
|
|
734
|
+
display:flex;align-items:center;justify-content:center;
|
|
735
|
+
font-size:16px;transition:all .2s;flex-shrink:0;
|
|
736
|
+
}
|
|
737
|
+
.stop-btn:hover{filter:brightness(1.1);transform:scale(1.05);background:#ef4444}
|
|
738
|
+
|
|
739
|
+
/* ─── Raw Logs Drawer ────────────────────────────────────────── */
|
|
740
|
+
.raw-logs-btn{
|
|
741
|
+
width:44px;height:44px;border-radius:10px;border:1px solid var(--border-dim);
|
|
742
|
+
background:transparent;color:var(--text-muted);cursor:pointer;
|
|
743
|
+
display:flex;align-items:center;justify-content:center;font-size:18px;
|
|
744
|
+
transition:all .2s;flex-shrink:0;
|
|
745
|
+
}
|
|
746
|
+
.raw-logs-btn:hover{border-color:var(--border-glow);color:var(--text-secondary)}
|
|
747
|
+
.raw-logs-btn.active{border-color:var(--accent);color:var(--accent);background:rgba(139,92,246,0.1)}
|
|
748
|
+
.raw-logs-drawer{
|
|
749
|
+
display:none;height:200px;background:#0a0a0a;border-top:1px solid var(--border-dim);
|
|
750
|
+
overflow:auto;font-family:var(--font-mono);font-size:11px;line-height:1.5;
|
|
751
|
+
color:#8b8b8b;padding:8px 12px;
|
|
752
|
+
}
|
|
753
|
+
.raw-logs-drawer.open{display:block}
|
|
754
|
+
.raw-logs-header{display:flex;justify-content:space-between;align-items:center;padding:4px 0;border-bottom:1px solid rgba(255,255,255,0.06);margin-bottom:4px}
|
|
755
|
+
.raw-logs-mode-btn{background:none;border:1px solid var(--border-dim);color:var(--text-muted);font-size:10px;padding:2px 8px;border-radius:4px;cursor:pointer;font-family:var(--font-mono)}
|
|
756
|
+
.raw-logs-mode-btn:hover{border-color:var(--accent);color:var(--accent)}
|
|
757
|
+
.raw-logs-drawer pre{margin:0;white-space:pre-wrap;word-break:break-all}
|
|
758
|
+
.raw-logs-drawer .log-line{opacity:0.7}
|
|
759
|
+
.raw-logs-drawer .log-line:last-child{opacity:1}
|
|
760
|
+
.raw-logs-drawer .log-stderr{color:#ef4444}
|
|
761
|
+
.raw-logs-drawer .log-dim{opacity:0.4}
|
|
762
|
+
|
|
763
|
+
/* ─── Queue Button variant ───────────────────────────────────── */
|
|
764
|
+
.send-btn.queue-mode{
|
|
765
|
+
background:var(--amber);
|
|
766
|
+
font-size:11px;font-weight:700;font-family:var(--font-mono);
|
|
767
|
+
width:auto;padding:0 14px;letter-spacing:.02em;
|
|
768
|
+
}
|
|
769
|
+
.send-btn.queue-mode:hover{filter:brightness(1.1);transform:scale(1.05)}
|
|
770
|
+
|
|
771
|
+
/* ─── Message Queue ──────────────────────────────────────────── */
|
|
772
|
+
.msg-queue{
|
|
773
|
+
display:flex;flex-direction:column;gap:6px;
|
|
774
|
+
padding:8px 0 0;
|
|
775
|
+
}
|
|
776
|
+
.msg-queue-item{
|
|
777
|
+
display:flex;align-items:center;gap:8px;
|
|
778
|
+
padding:6px 12px;border-radius:8px;
|
|
779
|
+
background:var(--bg-input);border:1px solid var(--border-dim);
|
|
780
|
+
animation:msgIn .2s ease-out;
|
|
781
|
+
}
|
|
782
|
+
.msg-queue-text{
|
|
783
|
+
flex:1;font-size:12px;color:var(--text-secondary);
|
|
784
|
+
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
|
|
785
|
+
font-family:var(--font-sans);
|
|
786
|
+
}
|
|
787
|
+
.msg-queue-pos{
|
|
788
|
+
font-family:var(--font-mono);font-size:9px;font-weight:600;
|
|
789
|
+
color:var(--text-muted);flex-shrink:0;min-width:16px;text-align:center;
|
|
790
|
+
}
|
|
791
|
+
.msg-queue-actions{display:flex;gap:4px;flex-shrink:0}
|
|
792
|
+
.msg-queue-btn{
|
|
793
|
+
width:24px;height:24px;border-radius:6px;border:1px solid var(--border-dim);
|
|
794
|
+
background:transparent;color:var(--text-muted);cursor:pointer;
|
|
795
|
+
display:flex;align-items:center;justify-content:center;font-size:12px;
|
|
796
|
+
transition:all .2s;
|
|
797
|
+
}
|
|
798
|
+
.msg-queue-btn:hover{border-color:var(--border-glow);color:var(--text-secondary)}
|
|
799
|
+
.msg-queue-btn.danger:hover{border-color:rgba(239,68,68,.3);color:#ef4444}
|
|
800
|
+
</style>
|
|
801
|
+
</head>
|
|
802
|
+
<body>
|
|
803
|
+
|
|
804
|
+
<div class="topbar">
|
|
805
|
+
<a href="/" style="display:flex;align-items:center;gap:10px;margin-right:28px;text-decoration:none">
|
|
806
|
+
<img class="logo-mark" src="/MyAIforOne-logomark-transparent.svg" alt="MyAIforOne">
|
|
807
|
+
<span class="logo-text">MyAIforOne</span>
|
|
808
|
+
</a>
|
|
809
|
+
<nav class="tab-group">
|
|
810
|
+
<a class="tab-btn" href="/">Home</a>
|
|
811
|
+
<a class="tab-btn" href="/org">Agents</a>
|
|
812
|
+
<a class="tab-btn active" href="/ui">Chat</a>
|
|
813
|
+
<a class="tab-btn" href="/library">Library</a>
|
|
814
|
+
<a class="tab-btn" href="/lab">Lab</a>
|
|
815
|
+
</nav>
|
|
816
|
+
<div class="topbar-right">
|
|
817
|
+
<a class="gym-nav-btn gym-tab-link" href="/gym" style="display:none">Gym</a>
|
|
818
|
+
<a href="/marketplace" class="gear-btn" title="Marketplace" style="font-size:11px;font-weight:700;letter-spacing:.03em;padding:0 10px;width:auto">Marketplace</a>
|
|
819
|
+
<a href="/monitor" class="gear-btn" title="Monitor" style="font-size:11px;font-weight:700;letter-spacing:.03em;padding:0 10px;width:auto">Monitor</a>
|
|
820
|
+
<a href="/admin" class="gear-btn" title="Admin" style="font-size:11px;font-weight:700;letter-spacing:.03em;padding:0 10px;width:auto">Admin</a>
|
|
821
|
+
<a href="/user-guide" class="gear-btn" title="User Guide" style="font-size:11px;font-weight:700;letter-spacing:.03em;padding:0 10px;width:auto">User Guide</a>
|
|
822
|
+
<button onclick="window.open('/mini','minibar','width=440,height=460,resizable=yes,scrollbars=no')" title="Open Mini Bar" style="display:flex;align-items:center;justify-content:center;width:34px;height:34px;border-radius:8px;border:1px solid var(--border-dim);background:transparent;color:var(--text-muted);cursor:pointer;font-size:15px;transition:all .2s" onmouseover="this.style.borderColor='var(--border-glow)'" onmouseout="this.style.borderColor='var(--border-dim)'">⊡</button>
|
|
823
|
+
<button class="theme-toggle" onclick="toggleTheme()" title="Toggle theme">☀</button>
|
|
824
|
+
</div>
|
|
825
|
+
</div>
|
|
826
|
+
|
|
827
|
+
<div class="app">
|
|
828
|
+
<!-- Sidebar -->
|
|
829
|
+
<div class="sidebar">
|
|
830
|
+
<div class="status-bar">
|
|
831
|
+
<div class="status-dot"></div>
|
|
832
|
+
<span class="status-text" id="statusText">Loading...</span>
|
|
833
|
+
<div class="status-channels" id="channelPills"></div>
|
|
834
|
+
</div>
|
|
835
|
+
<div class="sidebar-filters" style="padding:8px 12px;display:flex;gap:6px;border-bottom:1px solid var(--border-dim)">
|
|
836
|
+
<input type="text" id="sidebarSearch" placeholder="Search..." oninput="renderAgentList()" style="flex:1;min-width:0;font-family:var(--font-sans);font-size:11px;padding:5px 8px;border-radius:6px;border:1px solid var(--border-dim);background:var(--bg-input);color:var(--text-primary);outline:none">
|
|
837
|
+
<select id="sidebarOrgFilter" onchange="renderAgentList()" style="min-width:0;font-family:var(--font-sans);font-size:11px;padding:5px 8px;border-radius:6px;border:1px solid var(--border-dim);background:var(--bg-input);color:var(--text-primary);outline:none;cursor:pointer;max-width:120px">
|
|
838
|
+
<option value="">All Orgs</option>
|
|
839
|
+
</select>
|
|
840
|
+
</div>
|
|
841
|
+
<div class="agent-list" id="agentList"></div>
|
|
842
|
+
<div class="sidebar-collapse">
|
|
843
|
+
<button onclick="toggleSidebar()">◀ Collapse</button>
|
|
844
|
+
</div>
|
|
845
|
+
</div>
|
|
846
|
+
|
|
847
|
+
<!-- Sidebar expand button (outside main so it survives re-renders) -->
|
|
848
|
+
<button class="sidebar-expand" id="sidebarExpandBtn" onclick="toggleSidebar()" title="Show sidebar">☰</button>
|
|
849
|
+
|
|
850
|
+
<!-- Main -->
|
|
851
|
+
<div class="main" id="mainArea">
|
|
852
|
+
<div class="empty-state" id="emptyState">
|
|
853
|
+
<div class="empty-icon">◇</div>
|
|
854
|
+
<div class="empty-text">Select an agent to start chatting</div>
|
|
855
|
+
<div class="empty-sub">Or click "+ New Agent" to create one</div>
|
|
856
|
+
</div>
|
|
857
|
+
</div>
|
|
858
|
+
</div>
|
|
859
|
+
|
|
860
|
+
<!-- New Agent Modal -->
|
|
861
|
+
<div class="modal-overlay" id="newAgentModal">
|
|
862
|
+
<div class="modal">
|
|
863
|
+
<div class="modal-header">
|
|
864
|
+
<div class="modal-title">Create New Agent</div>
|
|
865
|
+
<button class="modal-close" onclick="closeModal()">✕</button>
|
|
866
|
+
</div>
|
|
867
|
+
<div class="modal-body">
|
|
868
|
+
<div class="form-row">
|
|
869
|
+
<div class="form-group">
|
|
870
|
+
<label class="form-label">Agent ID</label>
|
|
871
|
+
<input class="form-input" id="f-agentId" placeholder="my-agent" oninput="autoAlias()">
|
|
872
|
+
<div class="form-hint">Lowercase, hyphens only. Used as folder name.</div>
|
|
873
|
+
</div>
|
|
874
|
+
<div class="form-group">
|
|
875
|
+
<label class="form-label">Mention Alias</label>
|
|
876
|
+
<input class="form-input" id="f-alias" placeholder="@myagent">
|
|
877
|
+
<div class="form-hint">How you trigger this agent in chat.</div>
|
|
878
|
+
</div>
|
|
879
|
+
</div>
|
|
880
|
+
|
|
881
|
+
<div class="form-group">
|
|
882
|
+
<label class="form-label">Name</label>
|
|
883
|
+
<input class="form-input" id="f-name" placeholder="My Agent">
|
|
884
|
+
</div>
|
|
885
|
+
|
|
886
|
+
<div class="form-group">
|
|
887
|
+
<label class="form-label">Description</label>
|
|
888
|
+
<input class="form-input" id="f-desc" placeholder="What does this agent do?">
|
|
889
|
+
</div>
|
|
890
|
+
|
|
891
|
+
<div class="form-group">
|
|
892
|
+
<label class="form-label">Workspace</label>
|
|
893
|
+
<input class="form-input" id="f-workspace" placeholder="~" value="~">
|
|
894
|
+
<div class="form-hint">Directory Claude can access. ~ = home directory.</div>
|
|
895
|
+
</div>
|
|
896
|
+
|
|
897
|
+
<div class="form-section">Session</div>
|
|
898
|
+
<div class="form-group">
|
|
899
|
+
<div class="checkbox-group">
|
|
900
|
+
<div class="checkbox-pill checked" id="f-persistent" onclick="togglePill(this)">Persistent Session</div>
|
|
901
|
+
</div>
|
|
902
|
+
</div>
|
|
903
|
+
|
|
904
|
+
<div class="form-section">Tools</div>
|
|
905
|
+
<div class="form-group">
|
|
906
|
+
<div class="checkbox-group" id="f-tools">
|
|
907
|
+
<div class="checkbox-pill checked" data-val="Read" onclick="togglePill(this)">Read</div>
|
|
908
|
+
<div class="checkbox-pill checked" data-val="Edit" onclick="togglePill(this)">Edit</div>
|
|
909
|
+
<div class="checkbox-pill checked" data-val="Write" onclick="togglePill(this)">Write</div>
|
|
910
|
+
<div class="checkbox-pill checked" data-val="Glob" onclick="togglePill(this)">Glob</div>
|
|
911
|
+
<div class="checkbox-pill checked" data-val="Grep" onclick="togglePill(this)">Grep</div>
|
|
912
|
+
<div class="checkbox-pill checked" data-val="Bash" onclick="togglePill(this)">Bash</div>
|
|
913
|
+
<div class="checkbox-pill checked" data-val="WebFetch" onclick="togglePill(this)">WebFetch</div>
|
|
914
|
+
<div class="checkbox-pill checked" data-val="WebSearch" onclick="togglePill(this)">WebSearch</div>
|
|
915
|
+
</div>
|
|
916
|
+
</div>
|
|
917
|
+
|
|
918
|
+
<div class="form-section">MCPs</div>
|
|
919
|
+
<div class="form-group">
|
|
920
|
+
<div class="checkbox-group" id="f-mcps"></div>
|
|
921
|
+
<div class="form-hint" id="f-mcps-hint">Loading available MCPs...</div>
|
|
922
|
+
</div>
|
|
923
|
+
|
|
924
|
+
<div class="form-section">Routes</div>
|
|
925
|
+
<div id="routeEntries"></div>
|
|
926
|
+
<button class="add-route-btn" onclick="addRouteEntry()">+ Add Route</button>
|
|
927
|
+
</div>
|
|
928
|
+
<div class="modal-footer">
|
|
929
|
+
<div class="modal-footer-hint">Agent files created in ~/Desktop/MyAIforOne Drive/PersonalAgents/</div>
|
|
930
|
+
<button class="btn-create" id="createBtn" onclick="createAgent()">Create Agent</button>
|
|
931
|
+
</div>
|
|
932
|
+
</div>
|
|
933
|
+
</div>
|
|
934
|
+
|
|
935
|
+
<script>
|
|
936
|
+
// ─── Hidden Orgs (presentation mode) ────────────────────────────
|
|
937
|
+
function getHiddenOrgs() {
|
|
938
|
+
try { return JSON.parse(localStorage.getItem('hiddenOrgs') || '[]'); } catch { return []; }
|
|
939
|
+
}
|
|
940
|
+
function isAgentOrgHidden(agent) {
|
|
941
|
+
const hidden = getHiddenOrgs();
|
|
942
|
+
if (!hidden.length) return false;
|
|
943
|
+
return (agent.org || []).some(o => hidden.includes(o.organization));
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// ─── State ─────────────────────────────────────────────────────
|
|
947
|
+
let agents = [];
|
|
948
|
+
let selectedAgent = null;
|
|
949
|
+
let chatHistory = {}; // agentId -> [{role,text,time}]
|
|
950
|
+
// Per-agent streaming state
|
|
951
|
+
const streamState = {}; // agentId -> {isThinking, text, status, tools, jobId, queue}
|
|
952
|
+
function getStream(id) {
|
|
953
|
+
if (!streamState[id]) streamState[id] = { isThinking: false, text: '', status: '', tools: [], jobId: null, queue: [], renderDirty: false, renderRaf: null };
|
|
954
|
+
return streamState[id];
|
|
955
|
+
}
|
|
956
|
+
// Legacy globals — read from selected agent's state (used by renderChat)
|
|
957
|
+
let isThinking = false; // kept for non-streaming code paths
|
|
958
|
+
let streamingText = '';
|
|
959
|
+
let streamingStatus = '';
|
|
960
|
+
let streamingTools = [];
|
|
961
|
+
|
|
962
|
+
// ─── Voice read-back mode ─────────────────────────────
|
|
963
|
+
let voiceModeOn = false;
|
|
964
|
+
function toggleVoiceMode(){
|
|
965
|
+
voiceModeOn = !voiceModeOn;
|
|
966
|
+
const btn = document.getElementById('voiceModeBtn');
|
|
967
|
+
if(btn){ btn.textContent = voiceModeOn ? '🔊' : '🔇'; btn.style.color = voiceModeOn ? 'var(--accent)' : ''; }
|
|
968
|
+
if(!voiceModeOn) window.speechSynthesis?.cancel();
|
|
969
|
+
}
|
|
970
|
+
function speakText(text){
|
|
971
|
+
if(!voiceModeOn || !window.speechSynthesis) return;
|
|
972
|
+
const clean = text.replace(/\[via:[^\]]+\]/g,'').replace(/[*_`#>~]/g,'').replace(/\n+/g,' ').trim();
|
|
973
|
+
if(!clean) return;
|
|
974
|
+
window.speechSynthesis.cancel();
|
|
975
|
+
const u = new SpeechSynthesisUtterance(clean);
|
|
976
|
+
u.rate = 1.05;
|
|
977
|
+
window.speechSynthesis.speak(u);
|
|
978
|
+
}
|
|
979
|
+
let availableAccounts = [];
|
|
980
|
+
let accountOverrides = {}; // agentId -> account name (in-memory only)
|
|
981
|
+
|
|
982
|
+
// ─── Session Tabs ──────────────────────────────────────────────
|
|
983
|
+
// Open tabs stored in localStorage; full session list lives server-side.
|
|
984
|
+
// chatHistory and streamState are keyed by "agentId::tabId".
|
|
985
|
+
let sessionTabs = {}; // agentId -> [{id, label, createdAt}]
|
|
986
|
+
let activeTab = {}; // agentId -> tabId
|
|
987
|
+
|
|
988
|
+
function tabKey(agentId, tabId) { return agentId + '::' + tabId; }
|
|
989
|
+
function activeTabKey(agentId) { return tabKey(agentId, activeTab[agentId] || getAgentTabs(agentId)[0]?.id); }
|
|
990
|
+
|
|
991
|
+
function getAgentTabs(agentId) {
|
|
992
|
+
if (!sessionTabs[agentId] || !sessionTabs[agentId].length) {
|
|
993
|
+
const id = 'tab-' + Date.now().toString(36);
|
|
994
|
+
sessionTabs[agentId] = [{ id, label: 'Session 1', createdAt: Date.now() }];
|
|
995
|
+
activeTab[agentId] = id;
|
|
996
|
+
syncTabCreate(agentId, id, 'Session 1');
|
|
997
|
+
}
|
|
998
|
+
return sessionTabs[agentId];
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function saveTabs() {
|
|
1002
|
+
try { localStorage.setItem('sessionTabs', JSON.stringify(sessionTabs)); localStorage.setItem('activeTabs', JSON.stringify(activeTab)); } catch {}
|
|
1003
|
+
}
|
|
1004
|
+
function loadTabs() {
|
|
1005
|
+
try {
|
|
1006
|
+
const t = localStorage.getItem('sessionTabs');
|
|
1007
|
+
const a = localStorage.getItem('activeTabs');
|
|
1008
|
+
if (t) sessionTabs = JSON.parse(t);
|
|
1009
|
+
if (a) activeTab = JSON.parse(a);
|
|
1010
|
+
} catch {}
|
|
1011
|
+
}
|
|
1012
|
+
loadTabs();
|
|
1013
|
+
|
|
1014
|
+
// ── Server sync helpers ──────────────────────────────────────
|
|
1015
|
+
async function syncTabCreate(agentId, tabId, label) {
|
|
1016
|
+
try { await fetch(`/api/agents/${agentId}/session-tabs`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tabId, label }) }); } catch {}
|
|
1017
|
+
}
|
|
1018
|
+
async function syncTabRename(agentId, tabId, label) {
|
|
1019
|
+
try { await fetch(`/api/agents/${agentId}/session-tabs/${tabId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ label }) }); } catch {}
|
|
1020
|
+
}
|
|
1021
|
+
async function syncTabDelete(agentId, tabId) {
|
|
1022
|
+
try { await fetch(`/api/agents/${agentId}/session-tabs/${tabId}`, { method: 'DELETE' }); } catch {}
|
|
1023
|
+
}
|
|
1024
|
+
async function fetchServerSessions(agentId) {
|
|
1025
|
+
try { const r = await fetch(`/api/agents/${agentId}/session-tabs`); const d = await r.json(); return d.tabs || []; } catch { return []; }
|
|
1026
|
+
}
|
|
1027
|
+
async function fetchTabHistory(agentId, tabId) {
|
|
1028
|
+
try { const r = await fetch(`/api/agents/${agentId}/session-tabs/${tabId}/history`); const d = await r.json(); return d.messages || []; } catch { return []; }
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// ── Tab operations ──────────────────────────────────────────
|
|
1032
|
+
function createTab(agentId, label) {
|
|
1033
|
+
const tabs = getAgentTabs(agentId);
|
|
1034
|
+
const num = tabs.length + 1;
|
|
1035
|
+
const id = 'tab-' + Date.now().toString(36);
|
|
1036
|
+
const finalLabel = label || ('Session ' + num);
|
|
1037
|
+
tabs.push({ id, label: finalLabel, createdAt: Date.now() });
|
|
1038
|
+
activeTab[agentId] = id;
|
|
1039
|
+
saveTabs();
|
|
1040
|
+
syncTabCreate(agentId, id, finalLabel);
|
|
1041
|
+
renderedFrameAgent = null;
|
|
1042
|
+
renderChat();
|
|
1043
|
+
scrollChat();
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function closeTab(agentId, tabId) {
|
|
1047
|
+
const tabs = getAgentTabs(agentId);
|
|
1048
|
+
if (tabs.length <= 1) {
|
|
1049
|
+
// Last tab — don't close, open picker instead so user can switch sessions
|
|
1050
|
+
openSessionPicker(agentId);
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
const idx = tabs.findIndex(t => t.id === tabId);
|
|
1054
|
+
if (idx < 0) return;
|
|
1055
|
+
const key = tabKey(agentId, tabId);
|
|
1056
|
+
const ss = streamState[key];
|
|
1057
|
+
if (ss && (ss.isThinking || ss.jobId)) {
|
|
1058
|
+
if (!confirm('This session has an active stream. Close anyway?')) return;
|
|
1059
|
+
}
|
|
1060
|
+
tabs.splice(idx, 1);
|
|
1061
|
+
// Keep history on server; only clear in-memory state
|
|
1062
|
+
delete chatHistory[key];
|
|
1063
|
+
delete streamState[key];
|
|
1064
|
+
if (activeTab[agentId] === tabId) {
|
|
1065
|
+
activeTab[agentId] = tabs[Math.min(idx, tabs.length - 1)].id;
|
|
1066
|
+
}
|
|
1067
|
+
saveTabs();
|
|
1068
|
+
// NOTE: we do NOT delete from server — session stays accessible via picker
|
|
1069
|
+
renderedFrameAgent = null;
|
|
1070
|
+
renderChat();
|
|
1071
|
+
scrollChat();
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
function switchTab(agentId, tabId) {
|
|
1075
|
+
if (activeTab[agentId] === tabId) return;
|
|
1076
|
+
activeTab[agentId] = tabId;
|
|
1077
|
+
saveTabs();
|
|
1078
|
+
updateMessages();
|
|
1079
|
+
scrollChat();
|
|
1080
|
+
document.querySelectorAll('.session-tab').forEach(el => {
|
|
1081
|
+
el.classList.toggle('active', el.dataset.tabId === tabId);
|
|
1082
|
+
});
|
|
1083
|
+
const inp = document.getElementById('chatInput');
|
|
1084
|
+
if (inp) {
|
|
1085
|
+
const agent = agents.find(a => a.id === agentId);
|
|
1086
|
+
inp.placeholder = 'Message ' + (agent?.aliases[0] || agent?.name || '') + '...';
|
|
1087
|
+
inp.focus();
|
|
1088
|
+
}
|
|
1089
|
+
const ss = getStream(activeTabKey(agentId));
|
|
1090
|
+
updateInputButtons(ss);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function startRenameTab(agentId, tabId) {
|
|
1094
|
+
const el = document.querySelector(`.session-tab[data-tab-id="${tabId}"] .tab-label`);
|
|
1095
|
+
if (!el) return;
|
|
1096
|
+
const tab = getAgentTabs(agentId).find(t => t.id === tabId);
|
|
1097
|
+
if (!tab) return;
|
|
1098
|
+
const input = document.createElement('input');
|
|
1099
|
+
input.className = 'tab-rename-input';
|
|
1100
|
+
input.value = tab.label;
|
|
1101
|
+
input.onblur = () => finishRenameTab(agentId, tabId, input.value);
|
|
1102
|
+
input.onkeydown = (e) => { if (e.key === 'Enter') input.blur(); if (e.key === 'Escape') { input.value = tab.label; input.blur(); } };
|
|
1103
|
+
el.replaceWith(input);
|
|
1104
|
+
input.focus();
|
|
1105
|
+
input.select();
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
function finishRenameTab(agentId, tabId, newLabel) {
|
|
1109
|
+
const tab = getAgentTabs(agentId).find(t => t.id === tabId);
|
|
1110
|
+
if (tab && newLabel.trim()) {
|
|
1111
|
+
tab.label = newLabel.trim();
|
|
1112
|
+
saveTabs();
|
|
1113
|
+
syncTabRename(agentId, tabId, newLabel.trim());
|
|
1114
|
+
}
|
|
1115
|
+
renderTabBar(agentId);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function renderTabBar(agentId) {
|
|
1119
|
+
const container = document.getElementById('sessionTabBar');
|
|
1120
|
+
if (!container) return;
|
|
1121
|
+
const tabs = getAgentTabs(agentId);
|
|
1122
|
+
const curTab = activeTab[agentId] || tabs[0]?.id;
|
|
1123
|
+
let html = tabs.map(t => {
|
|
1124
|
+
const isActive = t.id === curTab;
|
|
1125
|
+
const key = tabKey(agentId, t.id);
|
|
1126
|
+
const ss = streamState[key];
|
|
1127
|
+
const hasActivity = !isActive && ss && (ss.isThinking || ss.jobId);
|
|
1128
|
+
return `<div class="session-tab${isActive ? ' active' : ''}${hasActivity ? ' has-activity' : ''}" data-tab-id="${t.id}"
|
|
1129
|
+
onclick="switchTab('${agentId}','${t.id}')"
|
|
1130
|
+
ondblclick="startRenameTab('${agentId}','${t.id}')">
|
|
1131
|
+
<span class="tab-dot"></span>
|
|
1132
|
+
<span class="tab-label">${escapeHtml(t.label)}</span>
|
|
1133
|
+
<span class="tab-close" onclick="event.stopPropagation();closeTab('${agentId}','${t.id}')">×</span>
|
|
1134
|
+
</div>`;
|
|
1135
|
+
}).join('');
|
|
1136
|
+
html += `<div class="session-tab-add" onclick="openSessionPicker('${agentId}')" title="Sessions">+</div>`;
|
|
1137
|
+
container.innerHTML = html;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// ── Session Picker ──────────────────────────────────────────
|
|
1141
|
+
let pickerAgentId = null;
|
|
1142
|
+
|
|
1143
|
+
async function openSessionPicker(agentId) {
|
|
1144
|
+
pickerAgentId = agentId;
|
|
1145
|
+
const modal = document.getElementById('sessionPickerModal');
|
|
1146
|
+
if (!modal) return;
|
|
1147
|
+
modal.classList.add('show');
|
|
1148
|
+
const list = document.getElementById('sessionPickerList');
|
|
1149
|
+
list.innerHTML = '<div style="padding:20px;color:var(--text-muted);text-align:center">Loading…</div>';
|
|
1150
|
+
const openIds = new Set((sessionTabs[agentId] || []).map(t => t.id));
|
|
1151
|
+
const serverSessions = await fetchServerSessions(agentId);
|
|
1152
|
+
if (!serverSessions.length) {
|
|
1153
|
+
list.innerHTML = '<div style="padding:16px 20px;color:var(--text-muted)">No sessions yet. Start a new one below.</div>';
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
list.innerHTML = serverSessions.map(s => {
|
|
1157
|
+
const isOpen = openIds.has(s.id);
|
|
1158
|
+
const isActive = activeTab[agentId] === s.id;
|
|
1159
|
+
const ago = s.lastMessageAt ? timeAgo(s.lastMessageAt) : '';
|
|
1160
|
+
return `<div class="picker-session${isActive ? ' picker-active' : ''}" onclick="pickSession('${agentId}','${s.id}',${JSON.stringify(s.label)})">
|
|
1161
|
+
<div class="picker-session-name">${escapeHtml(s.label)}${isOpen ? ' <span class="picker-badge">open</span>' : ''}</div>
|
|
1162
|
+
<div class="picker-session-meta">${escapeHtml(s.lastPreview || '')}${ago ? ' · ' + ago : ''}</div>
|
|
1163
|
+
</div>`;
|
|
1164
|
+
}).join('');
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function closeSessionPicker() {
|
|
1168
|
+
const modal = document.getElementById('sessionPickerModal');
|
|
1169
|
+
if (modal) modal.classList.remove('show');
|
|
1170
|
+
pickerAgentId = null;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
async function pickSession(agentId, tabId, label) {
|
|
1174
|
+
closeSessionPicker();
|
|
1175
|
+
const openIds = (sessionTabs[agentId] || []).map(t => t.id);
|
|
1176
|
+
if (openIds.includes(tabId)) {
|
|
1177
|
+
// Already open — just switch to it
|
|
1178
|
+
switchTab(agentId, tabId);
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
// Restore: add as a new tab, load history
|
|
1182
|
+
if (!sessionTabs[agentId]) sessionTabs[agentId] = [];
|
|
1183
|
+
sessionTabs[agentId].push({ id: tabId, label, createdAt: Date.now() });
|
|
1184
|
+
activeTab[agentId] = tabId;
|
|
1185
|
+
saveTabs();
|
|
1186
|
+
renderedFrameAgent = null;
|
|
1187
|
+
renderChat();
|
|
1188
|
+
// Load history from server
|
|
1189
|
+
const messages = await fetchTabHistory(agentId, tabId);
|
|
1190
|
+
if (messages.length) {
|
|
1191
|
+
chatHistory[tabKey(agentId, tabId)] = messages;
|
|
1192
|
+
updateMessages();
|
|
1193
|
+
scrollChat();
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
function createNewSession() {
|
|
1198
|
+
const agentId = pickerAgentId;
|
|
1199
|
+
closeSessionPicker();
|
|
1200
|
+
if (agentId) createTab(agentId);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
function timeAgo(iso) {
|
|
1204
|
+
const diff = Date.now() - new Date(iso).getTime();
|
|
1205
|
+
const m = Math.floor(diff / 60000);
|
|
1206
|
+
if (m < 1) return 'just now';
|
|
1207
|
+
if (m < 60) return m + 'm ago';
|
|
1208
|
+
const h = Math.floor(m / 60);
|
|
1209
|
+
if (h < 24) return h + 'h ago';
|
|
1210
|
+
return Math.floor(h / 24) + 'd ago';
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// Canvas — shared module (canvas.js loaded in <head>)
|
|
1214
|
+
Canvas.init({ getAgentId: () => selectedAgent, escapeHtml });
|
|
1215
|
+
|
|
1216
|
+
// ─── Recent Files Tracking ─────────────────────────────────────
|
|
1217
|
+
let recentFiles = []; // [{name, path, ts, tool}]
|
|
1218
|
+
const FILE_TOOLS = new Set(['Write', 'Edit', 'NotebookEdit']);
|
|
1219
|
+
|
|
1220
|
+
function trackFileFromTool(tool) {
|
|
1221
|
+
if (!tool || !tool.input) return;
|
|
1222
|
+
const name = tool.name;
|
|
1223
|
+
let filePath = null;
|
|
1224
|
+
if (FILE_TOOLS.has(name) && tool.input.file_path) {
|
|
1225
|
+
filePath = tool.input.file_path;
|
|
1226
|
+
} else if (name === 'Bash' && tool.input.command) {
|
|
1227
|
+
// Try to detect file creation from bash commands (output redirection, etc.)
|
|
1228
|
+
const m = tool.input.command.match(/>\s*["']?([^\s"'|&;]+\.\w+)/);
|
|
1229
|
+
if (m) filePath = m[1];
|
|
1230
|
+
}
|
|
1231
|
+
if (!filePath) return;
|
|
1232
|
+
const fileName = filePath.split('/').pop() || filePath;
|
|
1233
|
+
// Deduplicate by path
|
|
1234
|
+
const idx = recentFiles.findIndex(f => f.path === filePath);
|
|
1235
|
+
if (idx >= 0) {
|
|
1236
|
+
recentFiles[idx].ts = Date.now();
|
|
1237
|
+
recentFiles[idx].tool = name;
|
|
1238
|
+
} else {
|
|
1239
|
+
recentFiles.unshift({ name: fileName, path: filePath, ts: Date.now(), tool: name });
|
|
1240
|
+
if (recentFiles.length > 20) recentFiles.pop();
|
|
1241
|
+
}
|
|
1242
|
+
renderRecentFiles();
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
function renderRecentFiles() {
|
|
1246
|
+
const bar = document.getElementById('recentFilesBar');
|
|
1247
|
+
if (!bar || recentFiles.length === 0) { if (bar) bar.innerHTML = ''; return; }
|
|
1248
|
+
const icons = {csv:'📊',json:'📋',md:'📝',txt:'📝',html:'🌐',js:'⚡',ts:'⚡',py:'🐍',css:'🎨',pdf:'📄',png:'🖼️',jpg:'🖼️',svg:'🖼️',xlsx:'📊',docx:'📃'};
|
|
1249
|
+
let html = '<span class="recent-files-label">Recent Files</span>';
|
|
1250
|
+
for (const f of recentFiles.slice(0, 10)) {
|
|
1251
|
+
const ext = (f.name.split('.').pop() || '').toLowerCase();
|
|
1252
|
+
const icon = icons[ext] || '📁';
|
|
1253
|
+
const enc = encodeURIComponent(f.path);
|
|
1254
|
+
html += `<span class="recent-file-chip" title="${escapeHtml(f.path)}">
|
|
1255
|
+
<span class="chip-icon" onclick="Canvas.openFile('${enc}')">${icon}</span>
|
|
1256
|
+
<span onclick="Canvas.openFile('${enc}')">${escapeHtml(f.name)}</span>
|
|
1257
|
+
<span class="chip-folder" onclick="event.stopPropagation();fetch('/api/open-folder',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({path:'${enc}'})})" title="Open folder">📂</span>
|
|
1258
|
+
</span>`;
|
|
1259
|
+
}
|
|
1260
|
+
bar.innerHTML = html;
|
|
1261
|
+
}
|
|
1262
|
+
let rawLogsOpen = false;
|
|
1263
|
+
let rawLogsReader = null; // active fetch reader for raw stream
|
|
1264
|
+
let rawMode = false; // false = clean view, true = raw JSON
|
|
1265
|
+
|
|
1266
|
+
// Throttled stream rendering — batch token updates to avoid per-token formatMarkdown() calls
|
|
1267
|
+
function scheduleStreamRender(ss, isViewing) {
|
|
1268
|
+
if (!isViewing()) return;
|
|
1269
|
+
ss.renderDirty = true;
|
|
1270
|
+
if (!ss.renderRaf) {
|
|
1271
|
+
ss.renderRaf = requestAnimationFrame(() => {
|
|
1272
|
+
ss.renderRaf = null;
|
|
1273
|
+
if (!ss.renderDirty) return;
|
|
1274
|
+
ss.renderDirty = false;
|
|
1275
|
+
flushStreamRender(ss, isViewing);
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
function flushStreamRender(ss, isViewing) {
|
|
1280
|
+
if (!isViewing()) return;
|
|
1281
|
+
const streamEl = document.getElementById('streamingContent');
|
|
1282
|
+
const thinkWrap = document.getElementById('thinkingWrap');
|
|
1283
|
+
if (streamEl) {
|
|
1284
|
+
streamEl.innerHTML = formatMarkdown(ss.text);
|
|
1285
|
+
} else {
|
|
1286
|
+
if (thinkWrap) thinkWrap.innerHTML = '';
|
|
1287
|
+
const msgDiv = document.createElement('div');
|
|
1288
|
+
msgDiv.className = 'msg msg-agent';
|
|
1289
|
+
msgDiv.id = 'streamingMsg';
|
|
1290
|
+
msgDiv.innerHTML = `<div id="streamingContent">${formatMarkdown(ss.text)}</div><div class="msg-time">streaming...</div>`;
|
|
1291
|
+
const chatMsgs = document.getElementById('chatMessages');
|
|
1292
|
+
if (chatMsgs) chatMsgs.insertBefore(msgDiv, thinkWrap);
|
|
1293
|
+
}
|
|
1294
|
+
scrollChat();
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
function toggleRawLogs() {
|
|
1298
|
+
rawLogsOpen = !rawLogsOpen;
|
|
1299
|
+
const drawer = document.getElementById('rawLogsDrawer');
|
|
1300
|
+
const btn = document.getElementById('rawLogsBtn');
|
|
1301
|
+
if (drawer) drawer.classList.toggle('open', rawLogsOpen);
|
|
1302
|
+
if (btn) btn.classList.toggle('active', rawLogsOpen);
|
|
1303
|
+
if (rawLogsOpen) connectRawLogs();
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
function connectRawLogs() {
|
|
1307
|
+
const ss = getStream(activeTabKey(selectedAgent));
|
|
1308
|
+
if (!ss.jobId) {
|
|
1309
|
+
const el = document.getElementById('rawLogsContent');
|
|
1310
|
+
if (el) el.textContent = 'No active job. Send a message to start streaming logs.\n';
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
streamRawLogs(ss.jobId);
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
async function streamRawLogs(jobId) {
|
|
1317
|
+
const el = document.getElementById('rawLogsContent');
|
|
1318
|
+
if (!el) return;
|
|
1319
|
+
el.textContent = '';
|
|
1320
|
+
|
|
1321
|
+
try {
|
|
1322
|
+
const res = await fetch(`/api/chat/jobs/${jobId}/raw?after=0`);
|
|
1323
|
+
const reader = res.body.getReader();
|
|
1324
|
+
rawLogsReader = reader;
|
|
1325
|
+
const decoder = new TextDecoder();
|
|
1326
|
+
let buffer = '';
|
|
1327
|
+
|
|
1328
|
+
while (true) {
|
|
1329
|
+
const { done, value } = await reader.read();
|
|
1330
|
+
if (done) break;
|
|
1331
|
+
|
|
1332
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1333
|
+
const lines = buffer.split('\n');
|
|
1334
|
+
buffer = lines.pop() || '';
|
|
1335
|
+
|
|
1336
|
+
for (const line of lines) {
|
|
1337
|
+
if (!line.startsWith('data: ')) continue;
|
|
1338
|
+
const data = line.slice(6);
|
|
1339
|
+
if (data === '[DONE]') {
|
|
1340
|
+
el.textContent += '\n--- Job complete ---\n';
|
|
1341
|
+
break;
|
|
1342
|
+
}
|
|
1343
|
+
if (rawMode) {
|
|
1344
|
+
const span = document.createElement('span');
|
|
1345
|
+
span.className = data.startsWith('[stderr]') ? 'log-line log-stderr' : 'log-line';
|
|
1346
|
+
span.textContent = data + '\n';
|
|
1347
|
+
el.appendChild(span);
|
|
1348
|
+
} else {
|
|
1349
|
+
const fmt = formatLogLine(data);
|
|
1350
|
+
if (fmt) {
|
|
1351
|
+
const span = document.createElement('span');
|
|
1352
|
+
span.className = 'log-line' + (fmt.cls ? ' ' + fmt.cls : '');
|
|
1353
|
+
span.textContent = fmt.icon + ' ' + fmt.text + '\n';
|
|
1354
|
+
el.appendChild(span);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
// Auto-scroll
|
|
1358
|
+
const drawer = document.getElementById('rawLogsDrawer');
|
|
1359
|
+
if (drawer) drawer.scrollTop = drawer.scrollHeight;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
} catch { /* connection closed */ }
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
function toggleRawMode() {
|
|
1366
|
+
rawMode = !rawMode;
|
|
1367
|
+
const btn = document.getElementById('rawModeBtn');
|
|
1368
|
+
if (btn) btn.textContent = rawMode ? '● Raw' : '○ Clean';
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
function formatLogLine(data) {
|
|
1372
|
+
if (data.startsWith('[stderr]')) return { icon: '⚠', text: data.slice(9), cls: 'log-stderr' };
|
|
1373
|
+
try {
|
|
1374
|
+
let evt = JSON.parse(data);
|
|
1375
|
+
// Unwrap stream_event
|
|
1376
|
+
if (evt.type === 'stream_event' && evt.event) evt = evt.event;
|
|
1377
|
+
|
|
1378
|
+
// Text output
|
|
1379
|
+
if (evt.type === 'content_block_delta' && evt.delta?.type === 'text_delta')
|
|
1380
|
+
return { icon: '💬', text: evt.delta.text };
|
|
1381
|
+
|
|
1382
|
+
// Thinking
|
|
1383
|
+
if (evt.type === 'content_block_delta' && evt.delta?.type === 'thinking_delta')
|
|
1384
|
+
return { icon: '🧠', text: evt.delta.thinking };
|
|
1385
|
+
|
|
1386
|
+
// Tool starting
|
|
1387
|
+
if (evt.type === 'content_block_start' && evt.content_block?.type === 'tool_use')
|
|
1388
|
+
return { icon: '🔧', text: evt.content_block.name };
|
|
1389
|
+
|
|
1390
|
+
// Tool call complete (from assistant message)
|
|
1391
|
+
if (evt.type === 'assistant' && evt.message?.content) {
|
|
1392
|
+
const tools = evt.message.content.filter(b => b.type === 'tool_use');
|
|
1393
|
+
if (tools.length) {
|
|
1394
|
+
return { icon: '📖', text: tools.map(t => {
|
|
1395
|
+
const path = t.input?.file_path || t.input?.command?.slice(0, 80) || t.input?.pattern || '';
|
|
1396
|
+
return t.name + (path ? ' ' + path : '');
|
|
1397
|
+
}).join(', ') };
|
|
1398
|
+
}
|
|
1399
|
+
const texts = evt.message.content.filter(b => b.type === 'text' && b.text);
|
|
1400
|
+
if (texts.length) return { icon: '💬', text: texts.map(t => t.text).join(' ') };
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// Tool result
|
|
1404
|
+
if (evt.type === 'user' && evt.message?.content?.[0]?.type === 'tool_result')
|
|
1405
|
+
return { icon: '✓', text: 'tool_result', cls: 'log-dim' };
|
|
1406
|
+
|
|
1407
|
+
// Skip noise
|
|
1408
|
+
if (evt.type === 'content_block_delta' && evt.delta?.type === 'input_json_delta') return null;
|
|
1409
|
+
if (evt.type === 'content_block_stop' || evt.type === 'message_stop' || evt.type === 'message_delta') return null;
|
|
1410
|
+
if (evt.type === 'rate_limit_event') return null;
|
|
1411
|
+
if (evt.type === 'content_block_delta' && evt.delta?.type === 'signature_delta') return null;
|
|
1412
|
+
|
|
1413
|
+
// System init
|
|
1414
|
+
if (evt.type === 'system' && evt.subtype === 'init')
|
|
1415
|
+
return { icon: '●', text: 'Session started — ' + (evt.model || '') + ' | ' + (evt.mcp_servers?.length || 0) + ' MCPs' };
|
|
1416
|
+
if (evt.type === 'system') return { icon: '⚙', text: evt.subtype || 'system' };
|
|
1417
|
+
|
|
1418
|
+
// Result
|
|
1419
|
+
if (evt.type === 'result')
|
|
1420
|
+
return { icon: '🏁', text: 'Done — ' + (evt.duration_ms ? Math.round(evt.duration_ms/1000) + 's' : '') };
|
|
1421
|
+
|
|
1422
|
+
// Message start
|
|
1423
|
+
if (evt.type === 'message_start') return null;
|
|
1424
|
+
|
|
1425
|
+
// Fallback: show type
|
|
1426
|
+
return { icon: '·', text: evt.type || data.slice(0, 80), cls: 'log-dim' };
|
|
1427
|
+
} catch {
|
|
1428
|
+
return { icon: '·', text: data.slice(0, 120) };
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// ─── Theme ─────────────────────────────────────────────────────
|
|
1433
|
+
function toggleTheme() {
|
|
1434
|
+
const html = document.documentElement;
|
|
1435
|
+
const current = html.getAttribute('data-theme');
|
|
1436
|
+
const next = current === 'light' ? '' : 'light';
|
|
1437
|
+
html.setAttribute('data-theme', next);
|
|
1438
|
+
document.querySelector('.theme-toggle').textContent = next === 'light' ? '🌙' : '☀';
|
|
1439
|
+
localStorage.setItem('theme', next);
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
// Init theme
|
|
1443
|
+
(function() {
|
|
1444
|
+
const saved = localStorage.getItem('theme');
|
|
1445
|
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
1446
|
+
const theme = saved !== null ? saved : (prefersDark ? '' : 'light');
|
|
1447
|
+
if (theme) document.documentElement.setAttribute('data-theme', theme);
|
|
1448
|
+
document.querySelector('.theme-toggle').textContent = theme === 'light' ? '🌙' : '☀';
|
|
1449
|
+
})();
|
|
1450
|
+
|
|
1451
|
+
// ─── API ───────────────────────────────────────────────────────
|
|
1452
|
+
async function fetchDashboard() {
|
|
1453
|
+
try {
|
|
1454
|
+
const res = await fetch('/api/dashboard');
|
|
1455
|
+
const data = await res.json();
|
|
1456
|
+
agents = data.agents;
|
|
1457
|
+
availableAccounts = data.claudeAccounts || [];
|
|
1458
|
+
renderStatus(data);
|
|
1459
|
+
buildFilterDropdowns();
|
|
1460
|
+
renderAgentList();
|
|
1461
|
+
} catch (err) {
|
|
1462
|
+
document.getElementById('statusText').textContent = 'Connection failed';
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
let filtersInitialized = false;
|
|
1467
|
+
function buildFilterDropdowns() {
|
|
1468
|
+
const orgSet = new Set();
|
|
1469
|
+
for (const a of agents) {
|
|
1470
|
+
if ((a.agentClass || 'standard') !== 'standard') continue;
|
|
1471
|
+
for (const o of (a.org || [])) {
|
|
1472
|
+
if (o.organization) orgSet.add(o.organization);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
const sel = document.getElementById('sidebarOrgFilter');
|
|
1476
|
+
if (sel) {
|
|
1477
|
+
const prev = sel.value;
|
|
1478
|
+
sel.innerHTML = '<option value="">All Orgs</option>' +
|
|
1479
|
+
[...orgSet].sort().map(o => `<option value="${o}">${o}</option>`).join('');
|
|
1480
|
+
if (prev && [...orgSet].includes(prev)) sel.value = prev;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
|
|
1485
|
+
|
|
1486
|
+
function getFilteredAgents() {
|
|
1487
|
+
const query = (document.getElementById('sidebarSearch')?.value || '').toLowerCase().trim();
|
|
1488
|
+
const orgFilter = document.getElementById('sidebarOrgFilter')?.value || '';
|
|
1489
|
+
|
|
1490
|
+
let result = agents.filter(a => (a.agentClass || 'standard') === 'standard');
|
|
1491
|
+
|
|
1492
|
+
if (query) {
|
|
1493
|
+
result = result.filter(a =>
|
|
1494
|
+
(a.name || '').toLowerCase().includes(query) ||
|
|
1495
|
+
(a.id || '').toLowerCase().includes(query) ||
|
|
1496
|
+
(a.aliases || []).some(al => al.toLowerCase().includes(query)) ||
|
|
1497
|
+
(a.description || '').toLowerCase().includes(query)
|
|
1498
|
+
);
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
if (orgFilter) {
|
|
1502
|
+
result = result.filter(a =>
|
|
1503
|
+
(a.org || []).some(o => o.organization === orgFilter)
|
|
1504
|
+
);
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
return result.slice().sort((a, b) => {
|
|
1508
|
+
const aTime = a.lastMessage && a.lastMessage !== 'never' ? new Date(a.lastMessage).getTime() : 0;
|
|
1509
|
+
const bTime = b.lastMessage && b.lastMessage !== 'never' ? new Date(b.lastMessage).getTime() : 0;
|
|
1510
|
+
return bTime - aTime;
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
// Recover latest exchange from server log when streaming disconnects
|
|
1515
|
+
async function recoverFromLog(agentId, tk) {
|
|
1516
|
+
try {
|
|
1517
|
+
// Wait a moment for the executor to finish writing
|
|
1518
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
1519
|
+
const res = await fetch(`/api/agents/${agentId}`);
|
|
1520
|
+
const data = await res.json();
|
|
1521
|
+
if (data.recentMessages && data.recentMessages.length > 0) {
|
|
1522
|
+
const latest = data.recentMessages[data.recentMessages.length - 1];
|
|
1523
|
+
if (latest.response) {
|
|
1524
|
+
// Check if we already have this response (avoid duplicates)
|
|
1525
|
+
const _hist = chatHistory[tk || agentId] || [];
|
|
1526
|
+
const lastInHistory = _hist[_hist.length - 1];
|
|
1527
|
+
if (lastInHistory?.role === 'agent' && lastInHistory.text === latest.response) return true;
|
|
1528
|
+
_hist.push({ role: 'agent', text: latest.response, time: latest.ts, channel: latest.channel || 'unknown' });
|
|
1529
|
+
return true;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
} catch { /* ignore */ }
|
|
1533
|
+
return false;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
async function fetchAgentMessages(agentId) {
|
|
1537
|
+
try {
|
|
1538
|
+
const res = await fetch(`/api/agents/${agentId}`);
|
|
1539
|
+
const data = await res.json();
|
|
1540
|
+
if (data.recentMessages && data.recentMessages.length > 0) {
|
|
1541
|
+
// Load server history into the default tab only (other tabs are local-only)
|
|
1542
|
+
const defaultKey = tabKey(agentId, 'default');
|
|
1543
|
+
chatHistory[defaultKey] = data.recentMessages.map(m => ([
|
|
1544
|
+
{ role: 'user', text: m.text, time: m.ts, channel: m.channel || 'unknown' },
|
|
1545
|
+
{ role: 'agent', text: m.response, time: m.ts, channel: m.channel || 'unknown' },
|
|
1546
|
+
])).flat().filter(m => m.text);
|
|
1547
|
+
}
|
|
1548
|
+
} catch { /* ignore */ }
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
async function sendMessage(agentId, text, _tabKeyOverride) {
|
|
1552
|
+
// Capture the tab key at send time so it stays stable even if user switches tabs during streaming
|
|
1553
|
+
const tk = _tabKeyOverride || activeTabKey(agentId);
|
|
1554
|
+
if (!chatHistory[tk]) chatHistory[tk] = [];
|
|
1555
|
+
chatHistory[tk].push({ role: 'user', text, time: new Date().toISOString() });
|
|
1556
|
+
renderChat();
|
|
1557
|
+
scrollChat();
|
|
1558
|
+
|
|
1559
|
+
const agent = agents.find(a => a.id === agentId);
|
|
1560
|
+
const useStreaming = agent?.streaming;
|
|
1561
|
+
|
|
1562
|
+
if (useStreaming) {
|
|
1563
|
+
// SSE streaming mode — use per-tab state so switching tabs doesn't bleed
|
|
1564
|
+
const ss = getStream(tk);
|
|
1565
|
+
ss.isThinking = true;
|
|
1566
|
+
ss.text = '';
|
|
1567
|
+
ss.status = '';
|
|
1568
|
+
ss.tools = [];
|
|
1569
|
+
const isViewing = () => selectedAgent === agentId && activeTabKey(agentId) === tk;
|
|
1570
|
+
if (isViewing()) renderChat();
|
|
1571
|
+
scrollChat();
|
|
1572
|
+
|
|
1573
|
+
try {
|
|
1574
|
+
// Step 1: Start job
|
|
1575
|
+
const senderId = tk.split('::')[1] || 'default';
|
|
1576
|
+
const startRes = await fetch(`/api/chat/${agentId}/stream`, {
|
|
1577
|
+
method: 'POST',
|
|
1578
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1579
|
+
body: JSON.stringify({ text, accountOverride: accountOverrides[agentId], senderId }),
|
|
1580
|
+
});
|
|
1581
|
+
const { jobId } = await startRes.json();
|
|
1582
|
+
if (!jobId) throw new Error('No jobId returned');
|
|
1583
|
+
ss.jobId = jobId;
|
|
1584
|
+
// Re-render input area to show Stop button and Queue mode
|
|
1585
|
+
if (isViewing()) updateInputButtons(ss);
|
|
1586
|
+
// Auto-connect raw logs if drawer is open
|
|
1587
|
+
if (rawLogsOpen) streamRawLogs(jobId);
|
|
1588
|
+
|
|
1589
|
+
// Step 2: Stream events with auto-reconnect
|
|
1590
|
+
let lastEventId = 0;
|
|
1591
|
+
let isDone = false;
|
|
1592
|
+
let retries = 0;
|
|
1593
|
+
const MAX_RETRIES = 10;
|
|
1594
|
+
console.log(`[Stream] Starting job ${jobId} for ${agentId}`);
|
|
1595
|
+
|
|
1596
|
+
// Inactivity timeout: if no events for 60s, assume connection dead
|
|
1597
|
+
let lastEventTime = Date.now();
|
|
1598
|
+
const INACTIVITY_TIMEOUT_MS = 60_000;
|
|
1599
|
+
const inactivityCheck = setInterval(() => {
|
|
1600
|
+
if (isDone) { clearInterval(inactivityCheck); return; }
|
|
1601
|
+
if (Date.now() - lastEventTime > INACTIVITY_TIMEOUT_MS) {
|
|
1602
|
+
console.warn(`[Stream] No events for ${INACTIVITY_TIMEOUT_MS/1000}s — assuming connection dead`);
|
|
1603
|
+
isDone = true;
|
|
1604
|
+
clearInterval(inactivityCheck);
|
|
1605
|
+
}
|
|
1606
|
+
}, 10_000);
|
|
1607
|
+
|
|
1608
|
+
while (!isDone && retries < MAX_RETRIES) {
|
|
1609
|
+
try {
|
|
1610
|
+
console.log(`[Stream] Connecting to job ${jobId} after=${lastEventId} retry=${retries}`);
|
|
1611
|
+
const streamRes = await fetch(`/api/chat/jobs/${jobId}/stream?after=${lastEventId}`);
|
|
1612
|
+
if (!streamRes.ok) {
|
|
1613
|
+
console.warn(`[Stream] HTTP ${streamRes.status} from stream endpoint`);
|
|
1614
|
+
if (streamRes.status === 404) {
|
|
1615
|
+
// Service restarted — job is gone from memory. Post the exchange
|
|
1616
|
+
// back to the new server so it gets saved to conversation_log.jsonl.
|
|
1617
|
+
console.log(`[Stream] 404 = service restart. Recovering exchange (textLen=${ss.text.length})`);
|
|
1618
|
+
try {
|
|
1619
|
+
await fetch(`/api/agents/${agentId}/recover`, {
|
|
1620
|
+
method: 'POST',
|
|
1621
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1622
|
+
body: JSON.stringify({ userText: text, response: ss.text }),
|
|
1623
|
+
});
|
|
1624
|
+
} catch { /* best effort */ }
|
|
1625
|
+
if (ss.text) {
|
|
1626
|
+
chatHistory[tk].push({ role: 'agent', text: ss.text, time: new Date().toISOString(), tools: ss.tools.length ? [...ss.tools] : undefined });
|
|
1627
|
+
ss.text = '';
|
|
1628
|
+
}
|
|
1629
|
+
isDone = true;
|
|
1630
|
+
break;
|
|
1631
|
+
}
|
|
1632
|
+
retries++;
|
|
1633
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
1634
|
+
continue;
|
|
1635
|
+
}
|
|
1636
|
+
const reader = streamRes.body.getReader();
|
|
1637
|
+
const decoder = new TextDecoder();
|
|
1638
|
+
let buffer = '';
|
|
1639
|
+
let eventsThisConnection = 0;
|
|
1640
|
+
retries = 0; // reset on successful connect
|
|
1641
|
+
|
|
1642
|
+
while (true) {
|
|
1643
|
+
const { done, value } = await reader.read();
|
|
1644
|
+
if (done) {
|
|
1645
|
+
console.log(`[Stream] Reader done. Events this connection: ${eventsThisConnection}, isDone: ${isDone}, lastEventId: ${lastEventId}`);
|
|
1646
|
+
break;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1650
|
+
const lines = buffer.split('\n');
|
|
1651
|
+
buffer = lines.pop() || '';
|
|
1652
|
+
|
|
1653
|
+
for (const line of lines) {
|
|
1654
|
+
// Track event IDs for reconnect
|
|
1655
|
+
if (line.startsWith('id: ')) {
|
|
1656
|
+
lastEventId = parseInt(line.slice(4)) + 1;
|
|
1657
|
+
continue;
|
|
1658
|
+
}
|
|
1659
|
+
if (!line.startsWith('data: ')) continue;
|
|
1660
|
+
const data = line.slice(6);
|
|
1661
|
+
if (data === '[DONE]') { isDone = true; console.log(`[Stream] Received [DONE] for job ${jobId}`); continue; }
|
|
1662
|
+
lastEventTime = Date.now(); // reset inactivity timer on any event
|
|
1663
|
+
eventsThisConnection++;
|
|
1664
|
+
|
|
1665
|
+
try {
|
|
1666
|
+
const event = JSON.parse(data);
|
|
1667
|
+
if (event.type === 'text') {
|
|
1668
|
+
ss.text += event.data;
|
|
1669
|
+
ss.isThinking = false;
|
|
1670
|
+
scheduleStreamRender(ss, isViewing);
|
|
1671
|
+
} else if (event.type === 'tool') {
|
|
1672
|
+
ss.status = event.data;
|
|
1673
|
+
if (event.tool) {
|
|
1674
|
+
const hasInput = event.tool.input && Object.keys(event.tool.input).length > 0;
|
|
1675
|
+
const existingIdx = hasInput ? ss.tools.findLastIndex(t => t.name === event.tool.name && (!t.input || Object.keys(t.input).length === 0)) : -1;
|
|
1676
|
+
if (existingIdx >= 0) {
|
|
1677
|
+
ss.tools[existingIdx].input = event.tool.input;
|
|
1678
|
+
} else {
|
|
1679
|
+
ss.tools.push({ name: event.tool.name, input: event.tool.input, ts: Date.now() });
|
|
1680
|
+
}
|
|
1681
|
+
// Track file creation/modification for recent files bar
|
|
1682
|
+
if (hasInput) trackFileFromTool(event.tool);
|
|
1683
|
+
}
|
|
1684
|
+
if (isViewing()) {
|
|
1685
|
+
const toolsWrap = document.getElementById('streamingToolsWrap');
|
|
1686
|
+
if (toolsWrap) {
|
|
1687
|
+
toolsWrap.innerHTML = renderActivityFeed(ss.tools, true);
|
|
1688
|
+
} else {
|
|
1689
|
+
renderChat();
|
|
1690
|
+
}
|
|
1691
|
+
scrollChat();
|
|
1692
|
+
}
|
|
1693
|
+
} else if (event.type === 'status') {
|
|
1694
|
+
ss.status = event.data;
|
|
1695
|
+
if (isViewing()) {
|
|
1696
|
+
const thinkWrap = document.getElementById('thinkingWrap');
|
|
1697
|
+
if (thinkWrap && !ss.text) {
|
|
1698
|
+
thinkWrap.innerHTML = `<div class="thinking"><div class="thinking-dots"><span></span><span></span><span></span></div><span class="thinking-text">${ss.status || 'Thinking...'}</span></div>`;
|
|
1699
|
+
}
|
|
1700
|
+
scrollChat();
|
|
1701
|
+
}
|
|
1702
|
+
} else if (event.type === 'heartbeat') {
|
|
1703
|
+
// Backend heartbeat — connection alive, timer already reset above
|
|
1704
|
+
} else if (event.type === 'done') {
|
|
1705
|
+
// Agent finished — executor's final event
|
|
1706
|
+
isDone = true;
|
|
1707
|
+
} else if (event.type === 'stopped') {
|
|
1708
|
+
// User stopped the stream — finalize partial response
|
|
1709
|
+
isDone = true;
|
|
1710
|
+
} else if (event.type === 'error') {
|
|
1711
|
+
ss.isThinking = false;
|
|
1712
|
+
ss.text = '';
|
|
1713
|
+
ss.status = '';
|
|
1714
|
+
chatHistory[tk].push({ role: 'agent', text: `Error: ${event.data}`, time: new Date().toISOString() });
|
|
1715
|
+
if (isViewing()) { renderChat(); scrollChat(); }
|
|
1716
|
+
isDone = true;
|
|
1717
|
+
}
|
|
1718
|
+
} catch { /* skip malformed event */ }
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
// Stream ended naturally — if not done, reconnect silently
|
|
1723
|
+
if (!isDone) {
|
|
1724
|
+
retries++;
|
|
1725
|
+
console.warn(`[Stream] Stream ended without [DONE]. Retry ${retries}/${MAX_RETRIES}. lastEventId=${lastEventId}, textLen=${ss.text.length}`);
|
|
1726
|
+
// Only show reconnecting after 3+ retries (indicates real problem)
|
|
1727
|
+
if (retries >= 3 && isViewing()) {
|
|
1728
|
+
const thinkWrap = document.getElementById('thinkingWrap');
|
|
1729
|
+
if (thinkWrap) thinkWrap.innerHTML = `<div class="thinking"><div class="thinking-dots"><span></span><span></span><span></span></div><span class="thinking-text">Reconnecting... (${retries}/${MAX_RETRIES})</span></div>`;
|
|
1730
|
+
}
|
|
1731
|
+
await new Promise(r => setTimeout(r, 500));
|
|
1732
|
+
}
|
|
1733
|
+
} catch (streamErr) {
|
|
1734
|
+
// Connection failed — retry silently
|
|
1735
|
+
retries++;
|
|
1736
|
+
console.error(`[Stream] Connection error:`, streamErr, `Retry ${retries}/${MAX_RETRIES}`);
|
|
1737
|
+
if (retries >= 3 && retries < MAX_RETRIES && isViewing()) {
|
|
1738
|
+
const thinkWrap = document.getElementById('thinkingWrap');
|
|
1739
|
+
if (thinkWrap) thinkWrap.innerHTML = `<div class="thinking"><div class="thinking-dots"><span></span><span></span><span></span></div><span class="thinking-text">Reconnecting... (${retries}/${MAX_RETRIES})</span></div>`;
|
|
1740
|
+
}
|
|
1741
|
+
if (retries < MAX_RETRIES) {
|
|
1742
|
+
await new Promise(r => setTimeout(r, 500 * retries));
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// Finalize — flush any pending throttled render
|
|
1748
|
+
if (ss.renderRaf) { cancelAnimationFrame(ss.renderRaf); ss.renderRaf = null; }
|
|
1749
|
+
if (ss.renderDirty) { ss.renderDirty = false; flushStreamRender(ss, isViewing); }
|
|
1750
|
+
clearInterval(inactivityCheck);
|
|
1751
|
+
ss.isThinking = false;
|
|
1752
|
+
ss.jobId = null;
|
|
1753
|
+
if (ss.text) {
|
|
1754
|
+
speakText(ss.text);
|
|
1755
|
+
chatHistory[tk].push({ role: 'agent', text: ss.text, time: new Date().toISOString(), tools: ss.tools.length ? [...ss.tools] : undefined });
|
|
1756
|
+
} else if (!isDone) {
|
|
1757
|
+
// Stream disconnected without completing — reload from server log
|
|
1758
|
+
// The executor runs to completion regardless, so the log should have the response
|
|
1759
|
+
await recoverFromLog(agentId, tk);
|
|
1760
|
+
}
|
|
1761
|
+
ss.text = '';
|
|
1762
|
+
ss.status = '';
|
|
1763
|
+
ss.tools = [];
|
|
1764
|
+
} catch (err) {
|
|
1765
|
+
ss.isThinking = false;
|
|
1766
|
+
ss.jobId = null;
|
|
1767
|
+
// Save partial response if any text was streamed before disconnect
|
|
1768
|
+
if (ss.text) {
|
|
1769
|
+
chatHistory[tk].push({ role: 'agent', text: ss.text, time: new Date().toISOString(), tools: ss.tools.length ? [...ss.tools] : undefined });
|
|
1770
|
+
} else {
|
|
1771
|
+
// Try to recover from server log before falling back to error
|
|
1772
|
+
const recovered = await recoverFromLog(agentId, tk);
|
|
1773
|
+
if (!recovered) {
|
|
1774
|
+
chatHistory[tk].push({ role: 'agent', text: `Connection lost. The agent may still be running — check back shortly.`, time: new Date().toISOString() });
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
ss.text = '';
|
|
1778
|
+
ss.status = '';
|
|
1779
|
+
ss.tools = [];
|
|
1780
|
+
}
|
|
1781
|
+
if (isViewing()) { renderChat(); scrollChat(); }
|
|
1782
|
+
// Auto-send next queued message if any
|
|
1783
|
+
if (ss.queue.length > 0) {
|
|
1784
|
+
const next = ss.queue.shift();
|
|
1785
|
+
sendMessage(agentId, next, tk);
|
|
1786
|
+
}
|
|
1787
|
+
} else {
|
|
1788
|
+
// Regular non-streaming mode
|
|
1789
|
+
isThinking = true;
|
|
1790
|
+
renderChat();
|
|
1791
|
+
scrollChat();
|
|
1792
|
+
|
|
1793
|
+
try {
|
|
1794
|
+
const senderIdNs = tk.split('::')[1] || 'default';
|
|
1795
|
+
const res = await fetch(`/api/chat/${agentId}`, {
|
|
1796
|
+
method: 'POST',
|
|
1797
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1798
|
+
body: JSON.stringify({ text, accountOverride: accountOverrides[agentId], senderId: senderIdNs }),
|
|
1799
|
+
});
|
|
1800
|
+
const data = await res.json();
|
|
1801
|
+
isThinking = false;
|
|
1802
|
+
|
|
1803
|
+
if (data.ok) {
|
|
1804
|
+
chatHistory[tk].push({ role: 'agent', text: data.response, time: new Date().toISOString() });
|
|
1805
|
+
} else {
|
|
1806
|
+
chatHistory[tk].push({ role: 'agent', text: `Error: ${data.error}`, time: new Date().toISOString() });
|
|
1807
|
+
}
|
|
1808
|
+
} catch (err) {
|
|
1809
|
+
isThinking = false;
|
|
1810
|
+
chatHistory[tk].push({ role: 'agent', text: `Connection error: ${err.message}`, time: new Date().toISOString() });
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
renderChat();
|
|
1815
|
+
scrollChat();
|
|
1816
|
+
fetchDashboard();
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
// ─── Render ────────────────────────────────────────────────────
|
|
1820
|
+
function renderStatus(data) {
|
|
1821
|
+
const upMin = Math.floor(data.uptime / 60);
|
|
1822
|
+
const upH = Math.floor(upMin / 60);
|
|
1823
|
+
const upStr = upH > 0 ? `${upH}h ${upMin % 60}m` : `${upMin}m`;
|
|
1824
|
+
document.getElementById('statusText').textContent = `Running · ${upStr} · ${data.agents.length} agents`;
|
|
1825
|
+
document.getElementById('channelPills').innerHTML = data.channels
|
|
1826
|
+
.map(c => `<span class="channel-pill">${c}</span>`).join('');
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
function renderAgentList() {
|
|
1830
|
+
const list = document.getElementById('agentList');
|
|
1831
|
+
const allSorted = getFilteredAgents();
|
|
1832
|
+
|
|
1833
|
+
// Detect active agents (currently streaming/thinking)
|
|
1834
|
+
const activeIds = new Set();
|
|
1835
|
+
for (const [id, ss] of Object.entries(streamState)) {
|
|
1836
|
+
if (ss.isThinking || ss.jobId) activeIds.add(id);
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
const activeAgents = allSorted.filter(a => activeIds.has(a.id));
|
|
1840
|
+
const recentAgents = allSorted
|
|
1841
|
+
.filter(a => !activeIds.has(a.id) && a.lastMessage && a.lastMessage !== 'never')
|
|
1842
|
+
.slice(0, 10);
|
|
1843
|
+
const recentIds = new Set([...activeIds, ...recentAgents.map(a => a.id)]);
|
|
1844
|
+
const restAgents = allSorted.filter(a => !recentIds.has(a.id));
|
|
1845
|
+
|
|
1846
|
+
function compactCard(a, showPulse) {
|
|
1847
|
+
const initials = a.aliases[0]?.replace('@','').slice(0,2).toUpperCase() || a.id.slice(0,2).toUpperCase();
|
|
1848
|
+
const sel = selectedAgent === a.id ? ' selected' : '';
|
|
1849
|
+
const blur = isAgentOrgHidden(a) ? ' name-blurred' : '';
|
|
1850
|
+
const time = a.lastMessage && a.lastMessage !== 'never' ? formatRelativeTime(a.lastMessage) : '';
|
|
1851
|
+
const pulse = showPulse ? '<div class="thinking-pulse" title="Processing..."></div>' : '';
|
|
1852
|
+
return '<div class="agent-card-compact' + sel + '" onclick="selectAgent(\''+a.id+'\')"><div class="compact-avatar">'+initials+'</div><div class="compact-info"><div class="compact-name'+blur+'">'+a.name+'</div><div class="compact-time">'+((a.aliases||[])[0]||a.id)+(time?' · '+time:'')+'</div></div>'+pulse+'</div>';
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
let html = '';
|
|
1856
|
+
|
|
1857
|
+
// Active section
|
|
1858
|
+
if (activeAgents.length) {
|
|
1859
|
+
html += '<div class="sidebar-section open"><div class="sidebar-section-header"><span style="color:var(--amber)">●</span> Active<span class="section-count">'+activeAgents.length+'</span></div><div class="section-body">';
|
|
1860
|
+
html += activeAgents.map(a => compactCard(a, true)).join('');
|
|
1861
|
+
html += '</div></div>';
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
// Recent section
|
|
1865
|
+
if (recentAgents.length) {
|
|
1866
|
+
html += '<div class="sidebar-section open"><div class="sidebar-section-header">Recent<span class="section-count">'+recentAgents.length+'</span></div><div class="section-body">';
|
|
1867
|
+
html += recentAgents.map(a => compactCard(a, false)).join('');
|
|
1868
|
+
html += '</div></div>';
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
// All agents section (collapsed)
|
|
1872
|
+
if (restAgents.length) {
|
|
1873
|
+
html += '<div class="sidebar-section" id="allAgentsSection"><div class="sidebar-section-header collapsible" onclick="this.parentElement.classList.toggle(\'open\')"><span class="section-arrow">▶</span> All Agents<span class="section-count">'+restAgents.length+'</span></div><div class="section-body">';
|
|
1874
|
+
html += restAgents.map(a => compactCard(a, false)).join('');
|
|
1875
|
+
html += '</div></div>';
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
if (!html) html = '<div style="padding:20px;text-align:center;color:var(--text-muted);font-size:12px">No agents</div>';
|
|
1879
|
+
list.innerHTML = html;
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
// Track which agent the frame was built for — avoid rebuilding header/input unnecessarily
|
|
1883
|
+
let renderedFrameAgent = null;
|
|
1884
|
+
|
|
1885
|
+
function renderChat() {
|
|
1886
|
+
if (!selectedAgent) return;
|
|
1887
|
+
const agent = agents.find(a => a.id === selectedAgent);
|
|
1888
|
+
if (!agent) return;
|
|
1889
|
+
|
|
1890
|
+
// Sync globals from selected agent's active tab stream state
|
|
1891
|
+
const ss = getStream(activeTabKey(selectedAgent));
|
|
1892
|
+
isThinking = ss.isThinking;
|
|
1893
|
+
streamingText = ss.text;
|
|
1894
|
+
streamingStatus = ss.status;
|
|
1895
|
+
streamingTools = ss.tools;
|
|
1896
|
+
|
|
1897
|
+
// Only rebuild the full frame (header + input) when switching agents
|
|
1898
|
+
if (renderedFrameAgent !== selectedAgent) {
|
|
1899
|
+
renderedFrameAgent = selectedAgent;
|
|
1900
|
+
const main = document.getElementById('mainArea');
|
|
1901
|
+
main.innerHTML = `
|
|
1902
|
+
<div class="chat-header">
|
|
1903
|
+
<div class="agent-avatar" style="width:40px;height:40px;font-size:14px">
|
|
1904
|
+
${agent.aliases[0]?.replace('@','').slice(0,2).toUpperCase() || '??'}
|
|
1905
|
+
</div>
|
|
1906
|
+
<div class="chat-header-info">
|
|
1907
|
+
<div class="chat-header-name${isAgentOrgHidden(agent) ? ' name-blurred' : ''}">${agent.name}</div>
|
|
1908
|
+
<div class="chat-header-alias"><span class="${isAgentOrgHidden(agent) ? 'name-blurred' : ''}">${agent.aliases[0] || agent.id}</span> · ${agent.persistent?'persistent session':'single-shot'}</div>
|
|
1909
|
+
</div>
|
|
1910
|
+
<div class="chat-header-actions">
|
|
1911
|
+
${availableAccounts.length > 0 ? `<select class="header-btn" style="padding:2px 6px;font-size:11px;cursor:pointer" onchange="accountOverrides['${agent.id}']=this.value||undefined" title="Claude Account override (in-memory only)">
|
|
1912
|
+
<option value="">${agent.claudeAccount || 'default'}</option>
|
|
1913
|
+
${availableAccounts.filter(a => a !== agent.claudeAccount).map(a => `<option value="${a}" ${accountOverrides[agent.id]===a?'selected':''}>${a}</option>`).join('')}
|
|
1914
|
+
</select>` : ''}
|
|
1915
|
+
<button class="canvas-toggle-btn${Canvas.isOpen() ? ' active' : ''}" onclick="Canvas.toggle()" title="Toggle preview panel">▨ Canvas</button>
|
|
1916
|
+
${agent.persistent ? `<button class="header-btn danger" onclick="confirmResetSession('${agent.id}')">↺ Reset session</button>` : ''}
|
|
1917
|
+
</div>
|
|
1918
|
+
</div>
|
|
1919
|
+
<div class="session-tabs" id="sessionTabBar"></div>
|
|
1920
|
+
<div class="chat-body-wrap${Canvas.isOpen() ? ' canvas-open' : ''}" id="chatBodyWrap">
|
|
1921
|
+
<div class="chat-messages" id="chatMessages"></div>
|
|
1922
|
+
${Canvas.getPanelHtml()}
|
|
1923
|
+
</div>
|
|
1924
|
+
<div class="recent-files-bar" id="recentFilesBar"></div>
|
|
1925
|
+
<div class="chat-input-wrap">
|
|
1926
|
+
<div class="file-drop-zone hidden" id="fileDropZone"
|
|
1927
|
+
onclick="document.getElementById('fileInput').click()"
|
|
1928
|
+
ondragover="event.preventDefault();this.classList.add('dragover')"
|
|
1929
|
+
ondragleave="this.classList.remove('dragover')"
|
|
1930
|
+
ondrop="event.preventDefault();this.classList.remove('dragover');handleFileDrop(event)">
|
|
1931
|
+
<input type="file" id="fileInput" multiple onchange="handleFileSelect(event)">
|
|
1932
|
+
<div id="fileListArea">
|
|
1933
|
+
<span>Drop files here or click to browse</span>
|
|
1934
|
+
</div>
|
|
1935
|
+
</div>
|
|
1936
|
+
<div class="chat-input-box">
|
|
1937
|
+
<button class="clip-btn" id="clipBtn" onclick="toggleFileZone()" title="Attach files">📎</button>
|
|
1938
|
+
<button class="mic-btn" id="micBtn" onclick="toggleVoice()" title="Click to speak">🎤</button>
|
|
1939
|
+
<button class="mic-btn" id="voiceModeBtn" onclick="toggleVoiceMode()" title="Read responses aloud" style="font-size:14px">🔇</button>
|
|
1940
|
+
<button class="raw-logs-btn${rawLogsOpen ? ' active' : ''}" id="rawLogsBtn" onclick="toggleRawLogs()" title="Raw logs (live feed)">⌘</button>
|
|
1941
|
+
<textarea class="chat-textarea" id="chatInput" placeholder="Message ${agent.aliases[0] || agent.name}..." rows="1"
|
|
1942
|
+
onkeydown="handleKey(event)" oninput="autoResize(this)"></textarea>
|
|
1943
|
+
${(ss.jobId || ss.isThinking) ? `<button class="stop-btn" onclick="stopStreaming()" title="Stop (Esc)">■</button>` : ''}
|
|
1944
|
+
<button class="send-btn${ss.jobId ? ' queue-mode' : ''}" id="sendBtn" onclick="doSend()">${ss.jobId ? 'Queue' : '→'}</button>
|
|
1945
|
+
</div>
|
|
1946
|
+
<div id="queueContainer"></div>
|
|
1947
|
+
<div class="raw-logs-drawer${rawLogsOpen ? ' open' : ''}" id="rawLogsDrawer">
|
|
1948
|
+
<div class="raw-logs-header">
|
|
1949
|
+
<span style="font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.05em">Activity</span>
|
|
1950
|
+
<button class="raw-logs-mode-btn" id="rawModeBtn" onclick="toggleRawMode()">${rawMode ? '● Raw' : '○ Clean'}</button>
|
|
1951
|
+
</div>
|
|
1952
|
+
<pre id="rawLogsContent"></pre>
|
|
1953
|
+
</div>
|
|
1954
|
+
</div>
|
|
1955
|
+
`;
|
|
1956
|
+
setTimeout(() => {
|
|
1957
|
+
const inp = document.getElementById('chatInput');
|
|
1958
|
+
if (inp) inp.focus();
|
|
1959
|
+
renderQueueUI();
|
|
1960
|
+
renderTabBar(selectedAgent);
|
|
1961
|
+
}, 50);
|
|
1962
|
+
} else {
|
|
1963
|
+
// Even on message-only updates, refresh tab bar for activity dots
|
|
1964
|
+
renderTabBar(selectedAgent);
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
// Update only the messages area — leaves header, input, and canvas untouched
|
|
1968
|
+
updateMessages();
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
function updateMessages() {
|
|
1972
|
+
const chatEl = document.getElementById('chatMessages');
|
|
1973
|
+
if (!chatEl) return;
|
|
1974
|
+
|
|
1975
|
+
const agent = agents.find(a => a.id === selectedAgent);
|
|
1976
|
+
if (!agent) return;
|
|
1977
|
+
|
|
1978
|
+
const key = activeTabKey(selectedAgent);
|
|
1979
|
+
const messages = chatHistory[key] || [];
|
|
1980
|
+
const ss = getStream(key);
|
|
1981
|
+
isThinking = ss.isThinking;
|
|
1982
|
+
streamingText = ss.text;
|
|
1983
|
+
streamingStatus = ss.status;
|
|
1984
|
+
streamingTools = ss.tools;
|
|
1985
|
+
|
|
1986
|
+
// Build messages HTML
|
|
1987
|
+
let html = '';
|
|
1988
|
+
if (messages.length === 0 && !streamingText && !streamingTools.length && !isThinking) {
|
|
1989
|
+
html = `<div class="msg-system">Start a conversation with ${agent.aliases[0] || agent.name}</div>`;
|
|
1990
|
+
} else {
|
|
1991
|
+
html = messages.map(m => {
|
|
1992
|
+
const cls = m.role === 'user' ? 'msg-user' : 'msg-agent';
|
|
1993
|
+
const formatted = m.role === 'agent' ? formatMarkdown(m.text) : escapeHtml(m.text);
|
|
1994
|
+
const time = formatTime(m.time);
|
|
1995
|
+
const channelBadge = m.channel && m.channel !== 'web' && m.channel !== 'unknown' ? `<span class="channel-badge">${m.channel}</span>` : '';
|
|
1996
|
+
const toolHtml = (m.tools && m.tools.length) ? renderActivityFeed(m.tools, false) : '';
|
|
1997
|
+
return `${toolHtml}<div class="msg ${cls}">${formatted}<div class="msg-time">${channelBadge}${time}</div></div>`;
|
|
1998
|
+
}).join('');
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
// Streaming state
|
|
2002
|
+
html += `<div id="streamingToolsWrap">${streamingTools.length ? renderActivityFeed(streamingTools, true) : ''}</div>`;
|
|
2003
|
+
if (streamingText) {
|
|
2004
|
+
html += `<div class="msg msg-agent" id="streamingMsg"><div id="streamingContent">${formatMarkdown(streamingText)}</div><div class="msg-time">streaming...</div></div>`;
|
|
2005
|
+
}
|
|
2006
|
+
html += `<div id="thinkingWrap">${isThinking && !streamingText && !streamingTools.length ? `<div class="thinking"><div class="thinking-dots"><span></span><span></span><span></span></div><span class="thinking-text">${streamingStatus || 'Thinking...'}</span></div>` : ''}</div>`;
|
|
2007
|
+
|
|
2008
|
+
chatEl.innerHTML = html;
|
|
2009
|
+
|
|
2010
|
+
// Update input buttons (stop/queue) without touching textarea
|
|
2011
|
+
updateInputButtons(ss);
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
function scrollChat() {
|
|
2015
|
+
setTimeout(() => {
|
|
2016
|
+
const el = document.getElementById('chatMessages');
|
|
2017
|
+
if (el) el.scrollTop = el.scrollHeight;
|
|
2018
|
+
}, 60);
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
// ─── Actions ───────────────────────────────────────────────────
|
|
2022
|
+
async function selectAgent(id) {
|
|
2023
|
+
selectedAgent = id;
|
|
2024
|
+
renderAgentList();
|
|
2025
|
+
await fetchAgentMessages(id);
|
|
2026
|
+
renderChat();
|
|
2027
|
+
scrollChat();
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
async function doSend() {
|
|
2031
|
+
const input = document.getElementById('chatInput');
|
|
2032
|
+
if (!input || !selectedAgent) return;
|
|
2033
|
+
let text = input.value.trim();
|
|
2034
|
+
if (!text && pendingFiles.length === 0) return;
|
|
2035
|
+
|
|
2036
|
+
// Upload files first if any
|
|
2037
|
+
if (pendingFiles.length > 0) {
|
|
2038
|
+
const uploaded = await uploadFiles(selectedAgent);
|
|
2039
|
+
if (uploaded.length > 0) {
|
|
2040
|
+
const fileNotes = uploaded.map(f =>
|
|
2041
|
+
`[File ${f.mode === 'permanent' ? 'saved' : 'attached'}: ${f.name} at ${f.path}]`
|
|
2042
|
+
).join('\n');
|
|
2043
|
+
text = text ? `${text}\n\n${fileNotes}` : fileNotes;
|
|
2044
|
+
}
|
|
2045
|
+
// Hide drop zone after upload
|
|
2046
|
+
const zone = document.getElementById('fileDropZone');
|
|
2047
|
+
if (zone) zone.classList.add('hidden');
|
|
2048
|
+
const btn = document.getElementById('clipBtn');
|
|
2049
|
+
if (btn) btn.classList.remove('has-files');
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
input.value = '';
|
|
2053
|
+
autoResize(input);
|
|
2054
|
+
|
|
2055
|
+
// Handle /whoami command locally
|
|
2056
|
+
const _tk = activeTabKey(selectedAgent);
|
|
2057
|
+
if (text.trim().toLowerCase() === '/whoami') {
|
|
2058
|
+
if (!chatHistory[_tk]) chatHistory[_tk] = [];
|
|
2059
|
+
chatHistory[_tk].push({ role: 'user', text, time: new Date().toISOString() });
|
|
2060
|
+
updateMessages(); scrollChat();
|
|
2061
|
+
try {
|
|
2062
|
+
const r = await fetch('/api/whoami/' + encodeURIComponent(selectedAgent));
|
|
2063
|
+
const d = await r.json();
|
|
2064
|
+
const now = new Date().toLocaleTimeString();
|
|
2065
|
+
let info = '`claude auth status` ran at ' + now + '\n\n';
|
|
2066
|
+
if (d.email) {
|
|
2067
|
+
info += '| Field | Value |\n|-------|-------|\n'
|
|
2068
|
+
+ '| **Logged In** | ' + (d.loggedIn ? 'Yes ✓' : 'No ✗') + ' |\n'
|
|
2069
|
+
+ '| **Email** | ' + d.email + ' |\n'
|
|
2070
|
+
+ '| **Org** | ' + (d.orgName || 'N/A') + ' |\n'
|
|
2071
|
+
+ '| **Plan** | ' + (d.subscriptionType || 'N/A') + ' |\n'
|
|
2072
|
+
+ '| **Auth Method** | ' + (d.authMethod || 'N/A') + ' |\n'
|
|
2073
|
+
+ '| **Account Key** | ' + (d.accountName || 'default') + ' |\n'
|
|
2074
|
+
+ '| **Config Dir** | `' + (d.configDir || 'N/A') + '` |\n'
|
|
2075
|
+
+ '| **Agent** | ' + selectedAgent + ' |';
|
|
2076
|
+
} else if (d.loggedIn === false) {
|
|
2077
|
+
info += '**Not logged in.** This agent\'s configured account is not authenticated.\n\n'
|
|
2078
|
+
+ '- Account key: ' + (d.accountName || 'default') + '\n'
|
|
2079
|
+
+ '- Config dir: `' + (d.configDir || 'N/A') + '`\n'
|
|
2080
|
+
+ '- Go to `/settings` to re-authorize.';
|
|
2081
|
+
} else {
|
|
2082
|
+
info += 'Unexpected response: ```\n' + JSON.stringify(d, null, 2) + '\n```';
|
|
2083
|
+
}
|
|
2084
|
+
chatHistory[_tk].push({ role: 'agent', text: info, time: new Date().toISOString() });
|
|
2085
|
+
} catch (e) {
|
|
2086
|
+
chatHistory[_tk].push({ role: 'agent', text: 'Error: ' + e.message, time: new Date().toISOString() });
|
|
2087
|
+
}
|
|
2088
|
+
updateMessages(); scrollChat();
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
// If streaming is active, queue the message instead of sending
|
|
2093
|
+
const ss = getStream(_tk);
|
|
2094
|
+
if (ss.isThinking || ss.jobId || ss.text) {
|
|
2095
|
+
ss.queue.push(text);
|
|
2096
|
+
renderQueueUI();
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
sendMessage(selectedAgent, text, _tk);
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
function updateInputButtons(ss) {
|
|
2104
|
+
const box = document.querySelector('.chat-input-box');
|
|
2105
|
+
if (!box) return;
|
|
2106
|
+
const sendBtn = document.getElementById('sendBtn');
|
|
2107
|
+
// Add or remove stop button
|
|
2108
|
+
const existingStop = box.querySelector('.stop-btn');
|
|
2109
|
+
const showStop = ss.jobId || ss.isThinking;
|
|
2110
|
+
if (showStop && !existingStop) {
|
|
2111
|
+
const stopBtn = document.createElement('button');
|
|
2112
|
+
stopBtn.className = 'stop-btn';
|
|
2113
|
+
stopBtn.onclick = stopStreaming;
|
|
2114
|
+
stopBtn.title = 'Stop (Esc)';
|
|
2115
|
+
stopBtn.innerHTML = '\u25A0';
|
|
2116
|
+
if (sendBtn) box.insertBefore(stopBtn, sendBtn);
|
|
2117
|
+
} else if (!showStop && existingStop) {
|
|
2118
|
+
existingStop.remove();
|
|
2119
|
+
}
|
|
2120
|
+
// Update send button
|
|
2121
|
+
if (sendBtn) {
|
|
2122
|
+
if (ss.jobId) {
|
|
2123
|
+
sendBtn.classList.add('queue-mode');
|
|
2124
|
+
sendBtn.textContent = 'Queue';
|
|
2125
|
+
sendBtn.disabled = false;
|
|
2126
|
+
} else {
|
|
2127
|
+
sendBtn.classList.remove('queue-mode');
|
|
2128
|
+
sendBtn.innerHTML = '\u2192';
|
|
2129
|
+
sendBtn.disabled = false;
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
function renderQueueUI() {
|
|
2135
|
+
const container = document.getElementById('queueContainer');
|
|
2136
|
+
if (!container || !selectedAgent) return;
|
|
2137
|
+
const ss = getStream(activeTabKey(selectedAgent));
|
|
2138
|
+
if (!ss.queue.length) {
|
|
2139
|
+
container.innerHTML = '';
|
|
2140
|
+
return;
|
|
2141
|
+
}
|
|
2142
|
+
container.innerHTML = `<div class="msg-queue">${ss.queue.map((msg, i) => {
|
|
2143
|
+
const truncated = msg.length > 60 ? msg.slice(0, 60) + '...' : msg;
|
|
2144
|
+
return `<div class="msg-queue-item">
|
|
2145
|
+
<span class="msg-queue-pos">${i + 1}</span>
|
|
2146
|
+
<span class="msg-queue-text" title="${msg.replace(/"/g, '"')}">${truncated.replace(/</g, '<')}</span>
|
|
2147
|
+
<div class="msg-queue-actions">
|
|
2148
|
+
<button class="msg-queue-btn" onclick="editQueueItem(${i})" title="Edit">✎</button>
|
|
2149
|
+
<button class="msg-queue-btn danger" onclick="deleteQueueItem(${i})" title="Delete">✕</button>
|
|
2150
|
+
</div>
|
|
2151
|
+
</div>`;
|
|
2152
|
+
}).join('')}</div>`;
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
function editQueueItem(idx) {
|
|
2156
|
+
if (!selectedAgent) return;
|
|
2157
|
+
const ss = getStream(activeTabKey(selectedAgent));
|
|
2158
|
+
if (idx < 0 || idx >= ss.queue.length) return;
|
|
2159
|
+
const text = ss.queue.splice(idx, 1)[0];
|
|
2160
|
+
const input = document.getElementById('chatInput');
|
|
2161
|
+
if (input) {
|
|
2162
|
+
input.value = text;
|
|
2163
|
+
input.focus();
|
|
2164
|
+
autoResize(input);
|
|
2165
|
+
}
|
|
2166
|
+
renderQueueUI();
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
function deleteQueueItem(idx) {
|
|
2170
|
+
if (!selectedAgent) return;
|
|
2171
|
+
const ss = getStream(activeTabKey(selectedAgent));
|
|
2172
|
+
if (idx < 0 || idx >= ss.queue.length) return;
|
|
2173
|
+
ss.queue.splice(idx, 1);
|
|
2174
|
+
renderQueueUI();
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
async function stopStreaming() {
|
|
2178
|
+
if (!selectedAgent) return;
|
|
2179
|
+
const agentId = selectedAgent;
|
|
2180
|
+
const tk = activeTabKey(agentId);
|
|
2181
|
+
const ss = getStream(tk);
|
|
2182
|
+
if (ss.jobId) {
|
|
2183
|
+
try {
|
|
2184
|
+
await fetch(`/api/chat/jobs/${ss.jobId}/stop`, { method: 'POST' });
|
|
2185
|
+
} catch (e) {
|
|
2186
|
+
console.warn('Stop request failed:', e);
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
// Force-clear UI state even if jobId is already null (desynced state)
|
|
2190
|
+
if (ss.isThinking) {
|
|
2191
|
+
ss.isThinking = false;
|
|
2192
|
+
ss.jobId = null;
|
|
2193
|
+
if (ss.text) {
|
|
2194
|
+
chatHistory[tk].push({ role: 'agent', text: ss.text, time: new Date().toISOString(), tools: ss.tools.length ? [...ss.tools] : undefined });
|
|
2195
|
+
}
|
|
2196
|
+
ss.text = '';
|
|
2197
|
+
ss.status = '';
|
|
2198
|
+
ss.tools = [];
|
|
2199
|
+
if (selectedAgent === agentId) { renderChat(); scrollChat(); }
|
|
2200
|
+
}
|
|
2201
|
+
// Drain queue: auto-send next queued message after stop
|
|
2202
|
+
if (ss.queue.length > 0) {
|
|
2203
|
+
const next = ss.queue.shift();
|
|
2204
|
+
renderQueueUI();
|
|
2205
|
+
sendMessage(agentId, next, tk);
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
function handleKey(e) {
|
|
2210
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
2211
|
+
e.preventDefault();
|
|
2212
|
+
doSend();
|
|
2213
|
+
}
|
|
2214
|
+
// Escape key stops streaming
|
|
2215
|
+
if (e.key === 'Escape') {
|
|
2216
|
+
stopStreaming();
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
function autoResize(el) {
|
|
2221
|
+
el.style.height = 'auto';
|
|
2222
|
+
el.style.height = Math.min(el.scrollHeight, 140) + 'px';
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
// ─── Modal: New Agent ──────────────────────────────────────────
|
|
2226
|
+
let availableMcps = [];
|
|
2227
|
+
let routeCount = 0;
|
|
2228
|
+
|
|
2229
|
+
async function openNewAgentModal() {
|
|
2230
|
+
// Fetch available MCPs
|
|
2231
|
+
try {
|
|
2232
|
+
const res = await fetch('/api/mcps');
|
|
2233
|
+
const data = await res.json();
|
|
2234
|
+
availableMcps = data.mcps || [];
|
|
2235
|
+
} catch { availableMcps = []; }
|
|
2236
|
+
|
|
2237
|
+
// Render MCP checkboxes
|
|
2238
|
+
const mcpGroup = document.getElementById('f-mcps');
|
|
2239
|
+
const mcpHint = document.getElementById('f-mcps-hint');
|
|
2240
|
+
if (availableMcps.length > 0) {
|
|
2241
|
+
mcpGroup.innerHTML = availableMcps.map(m =>
|
|
2242
|
+
`<div class="checkbox-pill" data-val="${m}" onclick="togglePill(this)">${m}</div>`
|
|
2243
|
+
).join('');
|
|
2244
|
+
mcpHint.textContent = 'Select which MCP servers this agent can use.';
|
|
2245
|
+
} else {
|
|
2246
|
+
mcpGroup.innerHTML = '';
|
|
2247
|
+
mcpHint.textContent = 'No MCPs registered. Add them in config.json → mcps.';
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
// Clear form
|
|
2251
|
+
document.getElementById('f-agentId').value = '';
|
|
2252
|
+
document.getElementById('f-alias').value = '';
|
|
2253
|
+
document.getElementById('f-name').value = '';
|
|
2254
|
+
document.getElementById('f-desc').value = '';
|
|
2255
|
+
document.getElementById('f-workspace').value = '~';
|
|
2256
|
+
document.getElementById('f-persistent').classList.add('checked');
|
|
2257
|
+
|
|
2258
|
+
// Reset routes
|
|
2259
|
+
routeCount = 0;
|
|
2260
|
+
document.getElementById('routeEntries').innerHTML = '';
|
|
2261
|
+
addRouteEntry();
|
|
2262
|
+
|
|
2263
|
+
// Reset tools to all checked
|
|
2264
|
+
document.querySelectorAll('#f-tools .checkbox-pill').forEach(p => p.classList.add('checked'));
|
|
2265
|
+
|
|
2266
|
+
document.getElementById('newAgentModal').classList.add('show');
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
function closeModal() {
|
|
2270
|
+
document.getElementById('newAgentModal').classList.remove('show');
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
function togglePill(el) {
|
|
2274
|
+
el.classList.toggle('checked');
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
function autoAlias() {
|
|
2278
|
+
const id = document.getElementById('f-agentId').value.trim();
|
|
2279
|
+
const aliasEl = document.getElementById('f-alias');
|
|
2280
|
+
if (!aliasEl.value || aliasEl.value === '@' + aliasEl.dataset.auto) {
|
|
2281
|
+
const clean = id.replace(/-/g, '');
|
|
2282
|
+
aliasEl.value = clean ? '@' + clean : '';
|
|
2283
|
+
aliasEl.dataset.auto = clean;
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
function addRouteEntry() {
|
|
2288
|
+
routeCount++;
|
|
2289
|
+
const container = document.getElementById('routeEntries');
|
|
2290
|
+
const channels = agents.length > 0
|
|
2291
|
+
? [...new Set(agents.flatMap(a => a.routes.map(r => r.split(':')[0])))]
|
|
2292
|
+
: ['telegram', 'slack', 'imessage'];
|
|
2293
|
+
|
|
2294
|
+
const div = document.createElement('div');
|
|
2295
|
+
div.className = 'route-entry';
|
|
2296
|
+
div.id = `route-${routeCount}`;
|
|
2297
|
+
div.innerHTML = `
|
|
2298
|
+
<select>
|
|
2299
|
+
${channels.map(c => `<option value="${c}">${c}</option>`).join('')}
|
|
2300
|
+
</select>
|
|
2301
|
+
<input placeholder="Chat/Channel ID" />
|
|
2302
|
+
<button class="route-remove" onclick="this.parentElement.remove()">×</button>
|
|
2303
|
+
`;
|
|
2304
|
+
container.appendChild(div);
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
async function createAgent() {
|
|
2308
|
+
const btn = document.getElementById('createBtn');
|
|
2309
|
+
btn.disabled = true;
|
|
2310
|
+
btn.textContent = 'Creating...';
|
|
2311
|
+
|
|
2312
|
+
const agentId = document.getElementById('f-agentId').value.trim();
|
|
2313
|
+
const alias = document.getElementById('f-alias').value.trim();
|
|
2314
|
+
const name = document.getElementById('f-name').value.trim();
|
|
2315
|
+
const description = document.getElementById('f-desc').value.trim();
|
|
2316
|
+
const workspace = document.getElementById('f-workspace').value.trim() || '~';
|
|
2317
|
+
const persistent = document.getElementById('f-persistent').classList.contains('checked');
|
|
2318
|
+
|
|
2319
|
+
const tools = [...document.querySelectorAll('#f-tools .checkbox-pill.checked')]
|
|
2320
|
+
.map(el => el.dataset.val).filter(Boolean);
|
|
2321
|
+
|
|
2322
|
+
const mcps = [...document.querySelectorAll('#f-mcps .checkbox-pill.checked')]
|
|
2323
|
+
.map(el => el.dataset.val).filter(Boolean);
|
|
2324
|
+
|
|
2325
|
+
const routeEls = document.querySelectorAll('.route-entry');
|
|
2326
|
+
const routes = [...routeEls].map(el => {
|
|
2327
|
+
const channel = el.querySelector('select').value;
|
|
2328
|
+
const chatId = el.querySelector('input').value.trim();
|
|
2329
|
+
return chatId ? { channel, chatId, requireMention: true } : null;
|
|
2330
|
+
}).filter(Boolean);
|
|
2331
|
+
|
|
2332
|
+
if (!agentId || !name || !alias) {
|
|
2333
|
+
alert('Agent ID, Name, and Alias are required.');
|
|
2334
|
+
btn.disabled = false;
|
|
2335
|
+
btn.textContent = 'Create Agent';
|
|
2336
|
+
return;
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
try {
|
|
2340
|
+
const res = await fetch('/api/agents', {
|
|
2341
|
+
method: 'POST',
|
|
2342
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2343
|
+
body: JSON.stringify({ agentId, name, description, alias, workspace, persistent, tools, mcps, routes }),
|
|
2344
|
+
});
|
|
2345
|
+
const data = await res.json();
|
|
2346
|
+
|
|
2347
|
+
if (data.ok) {
|
|
2348
|
+
closeModal();
|
|
2349
|
+
await fetchDashboard();
|
|
2350
|
+
selectAgent(agentId);
|
|
2351
|
+
} else {
|
|
2352
|
+
alert('Error: ' + (data.error || 'Unknown error'));
|
|
2353
|
+
}
|
|
2354
|
+
} catch (err) {
|
|
2355
|
+
alert('Error: ' + err.message);
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
btn.disabled = false;
|
|
2359
|
+
btn.textContent = 'Create Agent';
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
// Close modal on overlay click
|
|
2363
|
+
document.getElementById('newAgentModal').addEventListener('click', function(e) {
|
|
2364
|
+
if (e.target === this) closeModal();
|
|
2365
|
+
});
|
|
2366
|
+
|
|
2367
|
+
// Close modal on Escape
|
|
2368
|
+
document.addEventListener('keydown', function(e) {
|
|
2369
|
+
if (e.key === 'Escape') closeModal();
|
|
2370
|
+
});
|
|
2371
|
+
|
|
2372
|
+
async function resetSession(agentId) {
|
|
2373
|
+
if (!confirm('Reset session? This clears conversation history. Use /opcompact first to save important context.')) return;
|
|
2374
|
+
await sendMessage(agentId, '/opreset');
|
|
2375
|
+
}
|
|
2376
|
+
function confirmResetSession(agentId) { resetSession(agentId); }
|
|
2377
|
+
|
|
2378
|
+
function viewConfig(agentId) {
|
|
2379
|
+
const agent = agents.find(a => a.id === agentId);
|
|
2380
|
+
if (!agent) return;
|
|
2381
|
+
const configStr = JSON.stringify({
|
|
2382
|
+
id: agent.id,
|
|
2383
|
+
name: agent.name,
|
|
2384
|
+
aliases: agent.aliases,
|
|
2385
|
+
workspace: agent.workspace,
|
|
2386
|
+
persistent: agent.persistent,
|
|
2387
|
+
mcps: agent.mcps,
|
|
2388
|
+
skills: agent.skills,
|
|
2389
|
+
tools: agent.tools,
|
|
2390
|
+
routes: agent.routes,
|
|
2391
|
+
}, null, 2);
|
|
2392
|
+
|
|
2393
|
+
const _vk = activeTabKey(agentId);
|
|
2394
|
+
if (!chatHistory[_vk]) chatHistory[_vk] = [];
|
|
2395
|
+
chatHistory[_vk].push({
|
|
2396
|
+
role: 'agent',
|
|
2397
|
+
text: '```json\n' + configStr + '\n```',
|
|
2398
|
+
time: new Date().toISOString()
|
|
2399
|
+
});
|
|
2400
|
+
renderChat();
|
|
2401
|
+
scrollChat();
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
// ─── Formatting ────────────────────────────────────────────────
|
|
2405
|
+
function escapeHtml(s) {
|
|
2406
|
+
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
function formatMarkdown(text) {
|
|
2410
|
+
if (!text) return '';
|
|
2411
|
+
let html = escapeHtml(text);
|
|
2412
|
+
// Code blocks
|
|
2413
|
+
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>');
|
|
2414
|
+
// Inline code
|
|
2415
|
+
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
2416
|
+
// Bold
|
|
2417
|
+
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
|
2418
|
+
// Detect file paths and add download/preview buttons (via shared Canvas module)
|
|
2419
|
+
html = Canvas.injectFileButtons(html, escapeHtml);
|
|
2420
|
+
// Break run-on sentences: period followed immediately by capital letter
|
|
2421
|
+
html = html.replace(/\.([A-Z])/g, '.<br>$1');
|
|
2422
|
+
// Paragraph breaks (double newline) get spacing, single newlines get line breaks
|
|
2423
|
+
html = html.replace(/\n{2,}/g, '</p><p>');
|
|
2424
|
+
html = html.replace(/\n/g, '<br>');
|
|
2425
|
+
html = '<p>' + html + '</p>';
|
|
2426
|
+
return html;
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
function toolDetail(t) {
|
|
2430
|
+
if (!t.input) return t.name || 'tool';
|
|
2431
|
+
if (t.name === 'Bash' && t.input.command) return `Bash: ${t.input.command.slice(0, 80)}`;
|
|
2432
|
+
if (t.name === 'Read' && t.input.file_path) return `Read ${t.input.file_path.split('/').pop()}`;
|
|
2433
|
+
if (t.name === 'Write' && t.input.file_path) return `Write ${t.input.file_path.split('/').pop()}`;
|
|
2434
|
+
if (t.name === 'Edit' && t.input.file_path) return `Edit ${t.input.file_path.split('/').pop()}`;
|
|
2435
|
+
if (t.name === 'Grep' && t.input.pattern) return `Grep "${t.input.pattern}"`;
|
|
2436
|
+
if (t.name === 'Glob' && t.input.pattern) return `Glob ${t.input.pattern}`;
|
|
2437
|
+
if (t.name === 'WebSearch') return `WebSearch: ${(t.input.query || t.input.search_query || '').slice(0, 60)}`;
|
|
2438
|
+
if (t.name === 'WebFetch') return `WebFetch: ${(t.input.url || '').slice(0, 60)}`;
|
|
2439
|
+
return t.name || 'tool';
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
function renderActivityFeed(tools, isLive) {
|
|
2443
|
+
if (!tools || !tools.length) return '';
|
|
2444
|
+
const collapsed = !isLive; // auto-collapse when done
|
|
2445
|
+
const lines = tools.map((t, i) => {
|
|
2446
|
+
const isLast = i === tools.length - 1;
|
|
2447
|
+
const dotClass = (isLive && isLast) ? 'active' : 'done';
|
|
2448
|
+
return `<div class="activity-line"><span class="activity-dot ${dotClass}"></span><span class="activity-name">${escapeHtml(toolDetail(t))}</span></div>`;
|
|
2449
|
+
});
|
|
2450
|
+
const uid = 'af-' + Math.random().toString(36).slice(2, 8);
|
|
2451
|
+
const headerText = isLive ? 'Working...' : `${tools.length} steps`;
|
|
2452
|
+
const arrow = collapsed ? '▸' : '▾';
|
|
2453
|
+
return `<div class="activity-feed ${collapsed ? 'collapsed' : ''}" id="${uid}">` +
|
|
2454
|
+
`<div class="activity-header" onclick="this.parentElement.classList.toggle('collapsed')" style="cursor:pointer">${arrow} ${headerText}</div>` +
|
|
2455
|
+
`<div class="activity-lines">${lines.join('')}</div></div>`;
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
function downloadFile(encodedPath) {
|
|
2459
|
+
if (!selectedAgent) return;
|
|
2460
|
+
const path = decodeURIComponent(encodedPath);
|
|
2461
|
+
const a = document.createElement('a');
|
|
2462
|
+
a.href = `/api/agents/${selectedAgent}/download?path=${encodeURIComponent(path)}`;
|
|
2463
|
+
a.download = path.split('/').pop() || 'download';
|
|
2464
|
+
document.body.appendChild(a);
|
|
2465
|
+
a.click();
|
|
2466
|
+
document.body.removeChild(a);
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
function formatTime(ts) {
|
|
2470
|
+
if (!ts) return '';
|
|
2471
|
+
try {
|
|
2472
|
+
const d = new Date(ts);
|
|
2473
|
+
const now = new Date();
|
|
2474
|
+
const isToday = d.toDateString() === now.toDateString();
|
|
2475
|
+
if (isToday) return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
2476
|
+
return d.toLocaleDateString([], { month: 'short', day: 'numeric' }) + ' ' +
|
|
2477
|
+
d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
2478
|
+
} catch { return ts; }
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
function formatRelativeTime(ts) {
|
|
2482
|
+
if (!ts) return '';
|
|
2483
|
+
try {
|
|
2484
|
+
const d = new Date(ts);
|
|
2485
|
+
const now = new Date();
|
|
2486
|
+
const diff = now - d;
|
|
2487
|
+
const mins = Math.floor(diff / 60000);
|
|
2488
|
+
if (mins < 1) return 'just now';
|
|
2489
|
+
if (mins < 60) return mins + 'm ago';
|
|
2490
|
+
const hours = Math.floor(mins / 60);
|
|
2491
|
+
if (hours < 24) return hours + 'h ago';
|
|
2492
|
+
const days = Math.floor(hours / 24);
|
|
2493
|
+
if (days === 1) return 'yesterday';
|
|
2494
|
+
if (days < 7) return days + 'd ago';
|
|
2495
|
+
return d.toLocaleDateString([], { month: 'short', day: 'numeric' });
|
|
2496
|
+
} catch { return ''; }
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
// ─── File Upload ───────────────────────────────────────────────
|
|
2500
|
+
let pendingFiles = []; // {file: File, mode: 'temp'|'permanent'}
|
|
2501
|
+
|
|
2502
|
+
function toggleFileZone() {
|
|
2503
|
+
const zone = document.getElementById('fileDropZone');
|
|
2504
|
+
if (!zone) return;
|
|
2505
|
+
zone.classList.toggle('hidden');
|
|
2506
|
+
const btn = document.getElementById('clipBtn');
|
|
2507
|
+
if (btn) btn.classList.toggle('has-files', !zone.classList.contains('hidden'));
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
function handleFileDrop(e) {
|
|
2511
|
+
const files = e.dataTransfer?.files;
|
|
2512
|
+
if (files) addFiles(files);
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
function handleFileSelect(e) {
|
|
2516
|
+
const files = e.target?.files;
|
|
2517
|
+
if (files) addFiles(files);
|
|
2518
|
+
e.target.value = ''; // reset so same file can be re-selected
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
// Handle Ctrl+V paste of images
|
|
2522
|
+
document.addEventListener('paste', function(e) {
|
|
2523
|
+
if (!selectedAgent) return;
|
|
2524
|
+
const items = e.clipboardData?.items;
|
|
2525
|
+
if (!items) return;
|
|
2526
|
+
const imageFiles = [];
|
|
2527
|
+
for (const item of items) {
|
|
2528
|
+
if (item.type.startsWith('image/')) {
|
|
2529
|
+
const file = item.getAsFile();
|
|
2530
|
+
if (file) imageFiles.push(file);
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
if (imageFiles.length > 0) {
|
|
2534
|
+
e.preventDefault();
|
|
2535
|
+
addFiles(imageFiles);
|
|
2536
|
+
// Show the drop zone if hidden
|
|
2537
|
+
const zone = document.getElementById('fileDropZone');
|
|
2538
|
+
if (zone) zone.classList.remove('hidden');
|
|
2539
|
+
const btn = document.getElementById('clipBtn');
|
|
2540
|
+
if (btn) btn.classList.add('has-files');
|
|
2541
|
+
}
|
|
2542
|
+
});
|
|
2543
|
+
|
|
2544
|
+
function addFiles(fileList) {
|
|
2545
|
+
for (const f of fileList) {
|
|
2546
|
+
if (!pendingFiles.find(p => p.file.name === f.name && p.file.size === f.size)) {
|
|
2547
|
+
pendingFiles.push({ file: f, mode: 'temp' });
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
renderFileList();
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
function removeFile(idx) {
|
|
2554
|
+
pendingFiles.splice(idx, 1);
|
|
2555
|
+
renderFileList();
|
|
2556
|
+
if (pendingFiles.length === 0) {
|
|
2557
|
+
const btn = document.getElementById('clipBtn');
|
|
2558
|
+
if (btn) btn.classList.remove('has-files');
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
function toggleFileMode(idx) {
|
|
2563
|
+
pendingFiles[idx].mode = pendingFiles[idx].mode === 'temp' ? 'permanent' : 'temp';
|
|
2564
|
+
renderFileList();
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
function renderFileList() {
|
|
2568
|
+
const area = document.getElementById('fileListArea');
|
|
2569
|
+
if (!area) return;
|
|
2570
|
+
if (pendingFiles.length === 0) {
|
|
2571
|
+
area.innerHTML = '<span>Drop files here or click to browse</span>';
|
|
2572
|
+
return;
|
|
2573
|
+
}
|
|
2574
|
+
area.innerHTML = '<div class="file-list">' + pendingFiles.map((p, i) => {
|
|
2575
|
+
const size = p.file.size > 1024*1024
|
|
2576
|
+
? (p.file.size/(1024*1024)).toFixed(1) + ' MB'
|
|
2577
|
+
: (p.file.size/1024).toFixed(0) + ' KB';
|
|
2578
|
+
const storeIcon = p.mode === 'permanent' ? '💾' : '⏳';
|
|
2579
|
+
const storeTitle = p.mode === 'permanent' ? 'Permanent (click for temp)' : 'Temp (click to save permanently)';
|
|
2580
|
+
return `<div class="file-pill" onclick="event.stopPropagation()">
|
|
2581
|
+
<span class="file-pill-name">${escapeHtml(p.file.name)}</span>
|
|
2582
|
+
<span class="file-pill-size">${size}</span>
|
|
2583
|
+
<span class="file-pill-store ${p.mode==='permanent'?'active':''}" onclick="toggleFileMode(${i})" title="${storeTitle}">${storeIcon}</span>
|
|
2584
|
+
<span class="file-pill-remove" onclick="removeFile(${i})">×</span>
|
|
2585
|
+
</div>`;
|
|
2586
|
+
}).join('') + '</div>';
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
async function uploadFiles(agentId) {
|
|
2590
|
+
const uploaded = [];
|
|
2591
|
+
for (const p of pendingFiles) {
|
|
2592
|
+
try {
|
|
2593
|
+
const formData = new FormData();
|
|
2594
|
+
formData.append('file', p.file);
|
|
2595
|
+
formData.append('mode', p.mode);
|
|
2596
|
+
|
|
2597
|
+
const resp = await fetch(`/api/upload/${agentId}`, {
|
|
2598
|
+
method: 'POST',
|
|
2599
|
+
body: formData,
|
|
2600
|
+
});
|
|
2601
|
+
const data = await resp.json();
|
|
2602
|
+
if (data.ok) {
|
|
2603
|
+
uploaded.push({ name: data.fileName, path: data.path, mode: data.mode });
|
|
2604
|
+
}
|
|
2605
|
+
} catch (err) {
|
|
2606
|
+
console.warn('Upload failed:', p.file.name, err);
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
pendingFiles = [];
|
|
2610
|
+
renderFileList();
|
|
2611
|
+
return uploaded;
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
// ─── Voice Input ───────────────────────────────────────────────
|
|
2615
|
+
let recognition = null;
|
|
2616
|
+
let isRecording = false;
|
|
2617
|
+
|
|
2618
|
+
function toggleVoice() {
|
|
2619
|
+
if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
|
|
2620
|
+
alert('Speech recognition not supported in this browser. Use Chrome or Edge.');
|
|
2621
|
+
return;
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
if (isRecording) {
|
|
2625
|
+
stopVoice();
|
|
2626
|
+
} else {
|
|
2627
|
+
startVoice();
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
function startVoice() {
|
|
2632
|
+
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
2633
|
+
recognition = new SpeechRecognition();
|
|
2634
|
+
recognition.continuous = true;
|
|
2635
|
+
recognition.interimResults = true;
|
|
2636
|
+
recognition.lang = 'en-US';
|
|
2637
|
+
|
|
2638
|
+
let finalTranscript = '';
|
|
2639
|
+
const input = document.getElementById('chatInput');
|
|
2640
|
+
const existingText = input ? input.value : '';
|
|
2641
|
+
|
|
2642
|
+
recognition.onresult = (event) => {
|
|
2643
|
+
let interim = '';
|
|
2644
|
+
for (let i = event.resultIndex; i < event.results.length; i++) {
|
|
2645
|
+
if (event.results[i].isFinal) {
|
|
2646
|
+
finalTranscript += event.results[i][0].transcript + ' ';
|
|
2647
|
+
} else {
|
|
2648
|
+
interim += event.results[i][0].transcript;
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
if (input) {
|
|
2652
|
+
input.value = existingText + (existingText ? ' ' : '') + finalTranscript + interim;
|
|
2653
|
+
autoResize(input);
|
|
2654
|
+
}
|
|
2655
|
+
};
|
|
2656
|
+
|
|
2657
|
+
recognition.onerror = (event) => {
|
|
2658
|
+
console.warn('Speech recognition error:', event.error);
|
|
2659
|
+
stopVoice();
|
|
2660
|
+
};
|
|
2661
|
+
|
|
2662
|
+
recognition.onend = () => {
|
|
2663
|
+
// If still in recording mode, it ended unexpectedly — clean up
|
|
2664
|
+
if (isRecording) stopVoice();
|
|
2665
|
+
};
|
|
2666
|
+
|
|
2667
|
+
recognition.start();
|
|
2668
|
+
isRecording = true;
|
|
2669
|
+
|
|
2670
|
+
const btn = document.getElementById('micBtn');
|
|
2671
|
+
if (btn) {
|
|
2672
|
+
btn.classList.add('recording');
|
|
2673
|
+
btn.title = 'Click to stop';
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
function stopVoice() {
|
|
2678
|
+
if (recognition) {
|
|
2679
|
+
recognition.stop();
|
|
2680
|
+
recognition = null;
|
|
2681
|
+
}
|
|
2682
|
+
isRecording = false;
|
|
2683
|
+
|
|
2684
|
+
const btn = document.getElementById('micBtn');
|
|
2685
|
+
if (btn) {
|
|
2686
|
+
btn.classList.remove('recording');
|
|
2687
|
+
btn.title = 'Click to speak';
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2690
|
+
// Focus the input so user can edit or send
|
|
2691
|
+
const input = document.getElementById('chatInput');
|
|
2692
|
+
if (input) input.focus();
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
// ─── Canvas / Preview Panel ────────────────────────────────────
|
|
2696
|
+
function getFileIcon(ext) {
|
|
2697
|
+
const icons = {
|
|
2698
|
+
html:'🌐', htm:'🌐',
|
|
2699
|
+
csv:'📊', tsv:'📊', xlsx:'📊', xls:'📊',
|
|
2700
|
+
md:'📝', markdown:'📝',
|
|
2701
|
+
png:'🖼', jpg:'🖼', jpeg:'🖼', gif:'🖼', svg:'🖼',
|
|
2702
|
+
pdf:'📄',
|
|
2703
|
+
docx:'📄', doc:'📄',
|
|
2704
|
+
pptx:'📊', ppt:'📊',
|
|
2705
|
+
json:'{}',
|
|
2706
|
+
js:'JS', ts:'TS', py:'PY', rb:'RB', go:'GO', rs:'RS', java:'JV',
|
|
2707
|
+
xml:'〈〉', yaml:'⚙', yml:'⚙',
|
|
2708
|
+
txt:'📃',
|
|
2709
|
+
};
|
|
2710
|
+
return icons[ext] || '📁';
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
// Canvas functions provided by shared canvas.js module (getFileType, toggleCanvas, openInCanvas, etc.)
|
|
2714
|
+
// Backwards-compat globals: openInCanvas, closeCanvas, toggleCanvas, navigatePptxSlide, sortCsvTable, renderXlsxSheet
|
|
2715
|
+
|
|
2716
|
+
// (removed: inline canvas functions — now in canvas.js)
|
|
2717
|
+
// ─── Close docs dropdown on outside click ──────────────────────
|
|
2718
|
+
document.addEventListener('click', (e) => {
|
|
2719
|
+
const dd = document.getElementById('docsDropdown');
|
|
2720
|
+
if (dd && !dd.contains(e.target)) {
|
|
2721
|
+
document.getElementById('docsMenu')?.classList.remove('open');
|
|
2722
|
+
}
|
|
2723
|
+
});
|
|
2724
|
+
|
|
2725
|
+
// ─── Init ──────────────────────────────────────────────────────
|
|
2726
|
+
fetchDashboard().then(() => {
|
|
2727
|
+
// Check URL hash for deep-link to agent (from org page "Chat" or activity page)
|
|
2728
|
+
// Format: #agentId or #agentId?ts=2026-03-25T12:00:00Z
|
|
2729
|
+
const rawHash = window.location.hash.slice(1);
|
|
2730
|
+
const [hashId, hashParams] = rawHash.split('?');
|
|
2731
|
+
const tsParam = hashParams ? new URLSearchParams(hashParams).get('ts') : null;
|
|
2732
|
+
const msgParam = hashParams ? new URLSearchParams(hashParams).get('msg') : null;
|
|
2733
|
+
|
|
2734
|
+
if (hashId && agents.find(a => a.id === hashId)) {
|
|
2735
|
+
// Auto-filter to this agent's org/dept
|
|
2736
|
+
const agent = agents.find(a => a.id === hashId);
|
|
2737
|
+
selectAgent(hashId).then(() => {
|
|
2738
|
+
// Scroll to specific message if ts param provided
|
|
2739
|
+
if (tsParam) {
|
|
2740
|
+
setTimeout(() => scrollToMessage(decodeURIComponent(tsParam)), 300);
|
|
2741
|
+
}
|
|
2742
|
+
// Pre-fill and auto-send message if msg param provided
|
|
2743
|
+
if (msgParam) {
|
|
2744
|
+
setTimeout(() => {
|
|
2745
|
+
const inp = document.getElementById('chatInput');
|
|
2746
|
+
if (inp) { inp.value = decodeURIComponent(msgParam); doSend(); }
|
|
2747
|
+
}, 300);
|
|
2748
|
+
}
|
|
2749
|
+
});
|
|
2750
|
+
window.location.hash = ''; // clean up URL
|
|
2751
|
+
}
|
|
2752
|
+
});
|
|
2753
|
+
setInterval(fetchDashboard, 30000);
|
|
2754
|
+
|
|
2755
|
+
// ─── Scroll to specific message by timestamp ───────────
|
|
2756
|
+
function scrollToMessage(ts) {
|
|
2757
|
+
if (!selectedAgent) return;
|
|
2758
|
+
const messages = chatHistory[activeTabKey(selectedAgent)] || [];
|
|
2759
|
+
// Find the message index matching this timestamp
|
|
2760
|
+
const idx = messages.findIndex(m => m.time === ts);
|
|
2761
|
+
if (idx < 0) return;
|
|
2762
|
+
|
|
2763
|
+
const chatEl = document.getElementById('chatMessages');
|
|
2764
|
+
if (!chatEl) return;
|
|
2765
|
+
const msgEls = chatEl.querySelectorAll('.msg');
|
|
2766
|
+
if (idx >= msgEls.length) return;
|
|
2767
|
+
|
|
2768
|
+
const targetEl = msgEls[idx];
|
|
2769
|
+
targetEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
2770
|
+
// Flash highlight
|
|
2771
|
+
targetEl.style.transition = 'box-shadow 0.3s';
|
|
2772
|
+
targetEl.style.boxShadow = '0 0 0 2px var(--accent), 0 0 20px rgba(34,211,238,0.3)';
|
|
2773
|
+
setTimeout(() => { targetEl.style.boxShadow = ''; }, 3000);
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2776
|
+
// ─── Sidebar Toggle ────────────────────────────────────
|
|
2777
|
+
function toggleSidebar() {
|
|
2778
|
+
const sidebar = document.querySelector('.sidebar');
|
|
2779
|
+
const expandBtn = document.getElementById('sidebarExpandBtn');
|
|
2780
|
+
sidebar.classList.toggle('collapsed');
|
|
2781
|
+
const isCollapsed = sidebar.classList.contains('collapsed');
|
|
2782
|
+
if (expandBtn) expandBtn.classList.toggle('visible', isCollapsed);
|
|
2783
|
+
localStorage.setItem('sidebarCollapsed', isCollapsed);
|
|
2784
|
+
}
|
|
2785
|
+
// Restore sidebar state
|
|
2786
|
+
(function() {
|
|
2787
|
+
if (localStorage.getItem('sidebarCollapsed') === 'true') {
|
|
2788
|
+
document.querySelector('.sidebar')?.classList.add('collapsed');
|
|
2789
|
+
document.getElementById('sidebarExpandBtn')?.classList.add('visible');
|
|
2790
|
+
}
|
|
2791
|
+
})();
|
|
2792
|
+
</script>
|
|
2793
|
+
|
|
2794
|
+
<!-- Session Picker Modal -->
|
|
2795
|
+
<div class="modal-overlay" id="sessionPickerModal" onclick="if(event.target===this)closeSessionPicker()">
|
|
2796
|
+
<div class="modal" style="width:420px">
|
|
2797
|
+
<div class="modal-header">
|
|
2798
|
+
<span class="modal-title">Sessions</span>
|
|
2799
|
+
<button class="modal-close" onclick="closeSessionPicker()">×</button>
|
|
2800
|
+
</div>
|
|
2801
|
+
<div id="sessionPickerList"></div>
|
|
2802
|
+
<div class="picker-footer">
|
|
2803
|
+
<button class="picker-new-btn" onclick="createNewSession()">+ New Session</button>
|
|
2804
|
+
</div>
|
|
2805
|
+
</div>
|
|
2806
|
+
</div>
|
|
2807
|
+
<script>(function(){fetch("/api/config/service").then(function(r){return r.json()}).then(function(d){if(d.gymEnabled)document.querySelectorAll(".gym-tab-link").forEach(function(el){el.style.display=""});if(d.gymOnlyMode){var keep=["/gym","/org","/admin"];document.querySelectorAll(".tab-group .tab-btn").forEach(function(el){var h=el.getAttribute("href");if(h&&keep.indexOf(h)===-1&&!el.classList.contains("gym-tab-link"))el.style.display="none"});document.querySelectorAll(".topbar-right .gear-btn").forEach(function(el){var h=el.getAttribute("href");if(h&&["/marketplace","/monitor","/user-guide"].indexOf(h)!==-1)el.style.display="none"})}}).catch(function(){})})()</script>
|
|
2808
|
+
</body>
|
|
2809
|
+
</html>
|