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
package/public/home.html
ADDED
|
@@ -0,0 +1,1930 @@
|
|
|
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
|
+
:root{
|
|
16
|
+
--bg-deep:#060a13;
|
|
17
|
+
--bg-surface:rgba(12,18,33,0.92);
|
|
18
|
+
--bg-card:rgba(16,22,40,0.85);
|
|
19
|
+
--bg-input:rgba(0,0,0,0.35);
|
|
20
|
+
--border-dim:rgba(56,189,248,0.08);
|
|
21
|
+
--border-glow:rgba(56,189,248,0.18);
|
|
22
|
+
--border-active:rgba(56,189,248,0.45);
|
|
23
|
+
--text-primary:rgba(255,255,255,0.92);
|
|
24
|
+
--text-secondary:rgba(255,255,255,0.68);
|
|
25
|
+
--text-muted:rgba(148,163,184,0.55);
|
|
26
|
+
--accent:#22d3ee;
|
|
27
|
+
--accent-soft:#38bdf8;
|
|
28
|
+
--accent-bg:rgba(6,182,212,0.15);
|
|
29
|
+
--purple:rgba(139,92,246,0.7);
|
|
30
|
+
--purple-bg:rgba(139,92,246,0.12);
|
|
31
|
+
--green:#4ade80;
|
|
32
|
+
--green-bg:rgba(74,222,128,0.1);
|
|
33
|
+
--amber:#fbbf24;
|
|
34
|
+
--shadow:0 2px 12px rgba(0,0,0,0.3);
|
|
35
|
+
--radius:12px;
|
|
36
|
+
--font-sans:'DM Sans',system-ui,-apple-system,sans-serif;
|
|
37
|
+
--font-mono:'IBM Plex Mono',monospace;
|
|
38
|
+
--font-display:'Syne',sans-serif;
|
|
39
|
+
}
|
|
40
|
+
[data-theme="light"]{
|
|
41
|
+
--bg-deep:#f4f6f9;
|
|
42
|
+
--bg-surface:rgba(255,255,255,0.95);
|
|
43
|
+
--bg-card:rgba(255,255,255,0.9);
|
|
44
|
+
--bg-input:rgba(0,0,0,0.04);
|
|
45
|
+
--border-dim:rgba(0,0,0,0.08);
|
|
46
|
+
--border-glow:rgba(14,116,144,0.18);
|
|
47
|
+
--border-active:rgba(14,116,144,0.45);
|
|
48
|
+
--text-primary:rgba(15,23,42,0.92);
|
|
49
|
+
--text-secondary:rgba(51,65,85,0.8);
|
|
50
|
+
--text-muted:rgba(100,116,139,0.6);
|
|
51
|
+
--accent:#0891b2;
|
|
52
|
+
--accent-soft:#0e7490;
|
|
53
|
+
--accent-bg:rgba(14,116,144,0.08);
|
|
54
|
+
--purple:rgba(109,40,217,0.75);
|
|
55
|
+
--purple-bg:rgba(139,92,246,0.08);
|
|
56
|
+
--green:#16a34a;
|
|
57
|
+
--green-bg:rgba(22,163,74,0.08);
|
|
58
|
+
--amber:#d97706;
|
|
59
|
+
--shadow:0 1px 8px rgba(0,0,0,0.06);
|
|
60
|
+
}
|
|
61
|
+
html,body{width:100%;height:100%;overflow:hidden;background:var(--bg-deep);font-family:var(--font-sans);color:var(--text-primary);transition:background .3s,color .3s}
|
|
62
|
+
|
|
63
|
+
/* ─── Layout ──────────────────────────────────────────── */
|
|
64
|
+
.home{display:flex;flex-direction:column;height:100vh;position:relative}
|
|
65
|
+
|
|
66
|
+
/* ─── Landing State (centered) ────────────────────────── */
|
|
67
|
+
.landing{
|
|
68
|
+
flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;
|
|
69
|
+
transition:all 0.5s ease;padding:20px;
|
|
70
|
+
}
|
|
71
|
+
.landing.hidden{opacity:0;pointer-events:none;position:absolute;top:0;left:0;right:0;bottom:0}
|
|
72
|
+
|
|
73
|
+
.landing-logo{font-family:var(--font-display);font-size:28px;font-weight:800;color:var(--accent);margin-bottom:8px}
|
|
74
|
+
.landing-sub{font-size:14px;color:var(--text-muted);margin-bottom:32px}
|
|
75
|
+
.landing-input-wrap{width:100%;max-width:600px}
|
|
76
|
+
.landing-input-box{
|
|
77
|
+
display:flex;align-items:center;gap:10px;
|
|
78
|
+
background:var(--bg-surface);border:1px solid var(--border-glow);
|
|
79
|
+
border-radius:16px;padding:8px 12px;
|
|
80
|
+
box-shadow:0 4px 24px rgba(0,0,0,0.3),0 0 40px rgba(34,211,238,0.06);
|
|
81
|
+
transition:border-color .3s;
|
|
82
|
+
}
|
|
83
|
+
.landing-input-box:focus-within{border-color:var(--border-active)}
|
|
84
|
+
.landing-textarea{
|
|
85
|
+
flex:1;background:transparent;border:none;outline:none;
|
|
86
|
+
color:var(--text-primary);font-family:var(--font-sans);font-size:15px;
|
|
87
|
+
resize:none;max-height:120px;line-height:1.5;padding:8px 4px;
|
|
88
|
+
}
|
|
89
|
+
.landing-textarea::placeholder{color:var(--text-muted)}
|
|
90
|
+
.landing-send{
|
|
91
|
+
width:44px;height:44px;border-radius:12px;border:none;
|
|
92
|
+
background:var(--accent);color:#000;cursor:pointer;
|
|
93
|
+
font-size:18px;font-weight:700;display:flex;align-items:center;justify-content:center;
|
|
94
|
+
transition:all .2s;flex-shrink:0;
|
|
95
|
+
}
|
|
96
|
+
.landing-send:hover{filter:brightness(1.1);transform:scale(1.05)}
|
|
97
|
+
.agent-count{font-size:12px;color:var(--text-muted);margin-top:16px}
|
|
98
|
+
|
|
99
|
+
/* ─── Agent Tiles ─────────────────────────────────────── */
|
|
100
|
+
.tiles-wrap{margin-top:16px;display:flex;align-items:center;gap:8px;max-width:600px;flex-wrap:wrap;justify-content:center}
|
|
101
|
+
.org-select{
|
|
102
|
+
font-family:var(--font-mono);font-size:11px;padding:4px 10px;border-radius:8px;
|
|
103
|
+
border:1px solid var(--border-glow);background:var(--bg-input);
|
|
104
|
+
color:var(--text-primary);outline:none;cursor:pointer;
|
|
105
|
+
}
|
|
106
|
+
.agent-tiles{
|
|
107
|
+
display:flex;gap:8px;flex-wrap:wrap;align-items:center;
|
|
108
|
+
}
|
|
109
|
+
.agent-tile{
|
|
110
|
+
display:flex;align-items:center;gap:6px;
|
|
111
|
+
padding:6px 12px;border-radius:10px;
|
|
112
|
+
background:var(--bg-card);border:1px solid var(--border-dim);
|
|
113
|
+
cursor:pointer;transition:all .2s;font-size:12px;
|
|
114
|
+
}
|
|
115
|
+
.agent-tile:hover{border-color:var(--border-glow);background:var(--bg-surface)}
|
|
116
|
+
.tile-avatar{
|
|
117
|
+
width:24px;height:24px;border-radius:6px;
|
|
118
|
+
background:var(--accent-bg);border:1px solid rgba(34,211,238,0.3);
|
|
119
|
+
display:flex;align-items:center;justify-content:center;
|
|
120
|
+
font-family:var(--font-mono);font-size:9px;font-weight:600;color:var(--accent);
|
|
121
|
+
}
|
|
122
|
+
.tile-name{color:var(--text-secondary);font-weight:500}
|
|
123
|
+
.tile-time{color:var(--text-muted);font-size:10px}
|
|
124
|
+
|
|
125
|
+
/* ─── Chat State ──────────────────────────────────────── */
|
|
126
|
+
.chat-view{
|
|
127
|
+
flex:1;display:flex;flex-direction:column;
|
|
128
|
+
opacity:0;pointer-events:none;position:absolute;top:0;left:0;right:0;bottom:0;
|
|
129
|
+
transition:all 0.5s ease;overflow:hidden;
|
|
130
|
+
}
|
|
131
|
+
.chat-view.active{opacity:1;pointer-events:all;position:relative}
|
|
132
|
+
|
|
133
|
+
/* ─── Topbar (matches /ui) ────────────────────────────── */
|
|
134
|
+
.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);flex-shrink:0;overflow:hidden;max-width:100vw}
|
|
135
|
+
.tab-group{display:flex;gap:0;flex-shrink:1;min-width:0}
|
|
136
|
+
.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}
|
|
137
|
+
.tab-btn:hover{color:var(--text-secondary)}
|
|
138
|
+
.tab-btn.active{color:var(--accent)}
|
|
139
|
+
.tab-btn.active::after{content:'';position:absolute;bottom:0;left:10px;right:10px;height:2px;background:var(--accent);border-radius:1px}
|
|
140
|
+
.topbar-right{margin-left:auto;display:flex;align-items:center;gap:8px;flex-shrink:0}
|
|
141
|
+
.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}
|
|
142
|
+
.gym-nav-btn:hover{opacity:.88;transform:translateY(-1px)}
|
|
143
|
+
.logo-mark{width:28px;height:28px;border-radius:8px;object-fit:contain}
|
|
144
|
+
.logo-text{font-family:var(--font-display);font-size:15px;font-weight:700;color:var(--accent)}
|
|
145
|
+
/* Second-level bar for hub actions */
|
|
146
|
+
.hub-actions-bar{
|
|
147
|
+
padding:6px 20px;border-bottom:1px solid var(--border-dim);
|
|
148
|
+
display:flex;align-items:center;gap:8px;
|
|
149
|
+
background:var(--bg-surface);flex-shrink:0;
|
|
150
|
+
}
|
|
151
|
+
.hub-actions-bar .hub-label{font-family:var(--font-display);font-size:12px;font-weight:700;color:var(--accent);margin-right:8px}
|
|
152
|
+
.header-btn{font-family:var(--font-sans);font-size:11px;font-weight:600;padding:4px 10px;border-radius:6px;border:1px solid var(--border-dim);background:transparent;color:var(--text-muted);cursor:pointer;transition:all .2s;text-decoration:none}
|
|
153
|
+
.header-btn:hover{border-color:var(--border-glow);color:var(--text-secondary)}
|
|
154
|
+
.header-btn.danger{color:var(--text-muted);border-color:rgba(248,113,113,0.2)}
|
|
155
|
+
.header-btn.danger:hover{color:#f87171;border-color:rgba(248,113,113,0.4);background:rgba(248,113,113,0.08)}
|
|
156
|
+
.docs-dropdown{position:relative;display:inline-block}
|
|
157
|
+
.docs-btn{display:flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:6px;border:1px solid var(--border-dim);background:transparent;color:var(--text-muted);cursor:pointer;font-size:13px;transition:all .2s}
|
|
158
|
+
.docs-btn:hover{border-color:var(--border-glow);color:var(--text-secondary)}
|
|
159
|
+
.docs-menu{display:none;position:absolute;top:34px;right:0;min-width:130px;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)}
|
|
160
|
+
.docs-menu.open{display:block}
|
|
161
|
+
.docs-menu a{display:flex;align-items:center;gap:8px;padding:6px 10px;font-size:11px;font-weight:500;color:var(--text-secondary);text-decoration:none;border-radius:6px;transition:all .15s}
|
|
162
|
+
.docs-menu a:hover{background:var(--accent-bg);color:var(--accent)}
|
|
163
|
+
.docs-menu a .dm-icon{font-size:13px;width:16px;text-align:center}
|
|
164
|
+
.theme-toggle,.gear-btn{
|
|
165
|
+
background:none;border:1px solid var(--border-dim);border-radius:6px;
|
|
166
|
+
padding:4px 8px;cursor:pointer;font-size:14px;color:var(--text-muted);transition:all .2s;
|
|
167
|
+
text-decoration:none;}
|
|
168
|
+
.theme-toggle:hover,.gear-btn:hover{border-color:var(--border-glow);color:var(--text-secondary)}
|
|
169
|
+
|
|
170
|
+
/* Landing needs padding below topbar */
|
|
171
|
+
.landing{padding-top:48px}
|
|
172
|
+
.landing-header .logo{font-family:var(--font-display);font-size:16px;font-weight:700;color:var(--accent)}
|
|
173
|
+
|
|
174
|
+
.chat-messages-home{
|
|
175
|
+
flex:1;overflow-y:auto;padding:20px;min-height:0;
|
|
176
|
+
display:flex;flex-direction:column;gap:12px;
|
|
177
|
+
}
|
|
178
|
+
.chat-body-wrap.canvas-open .chat-messages-home{flex:0 0 55%}
|
|
179
|
+
|
|
180
|
+
/* ─── Recent Files Bar ─────────────────────────────────────── */
|
|
181
|
+
.recent-files-bar{
|
|
182
|
+
display:flex;align-items:center;gap:6px;padding:4px 12px;
|
|
183
|
+
border-top:1px solid var(--border-dim);background:var(--bg-surface);
|
|
184
|
+
overflow-x:auto;flex-shrink:0;
|
|
185
|
+
}
|
|
186
|
+
.recent-files-bar:empty{display:none}
|
|
187
|
+
.recent-files-label{
|
|
188
|
+
font-family:var(--font-mono);font-size:9px;font-weight:600;
|
|
189
|
+
color:var(--text-muted);text-transform:uppercase;letter-spacing:.04em;
|
|
190
|
+
white-space:nowrap;flex-shrink:0;
|
|
191
|
+
}
|
|
192
|
+
.recent-file-chip{
|
|
193
|
+
display:inline-flex;align-items:center;gap:4px;
|
|
194
|
+
padding:2px 8px;border-radius:5px;
|
|
195
|
+
border:1px solid var(--border-dim);background:var(--bg-input);
|
|
196
|
+
font-family:var(--font-mono);font-size:10px;color:var(--text-secondary);
|
|
197
|
+
cursor:pointer;transition:all .15s;white-space:nowrap;flex-shrink:0;
|
|
198
|
+
}
|
|
199
|
+
.recent-file-chip:hover{border-color:var(--border-glow);color:var(--text-primary);background:var(--accent-bg)}
|
|
200
|
+
.recent-file-chip .chip-icon{font-size:12px}
|
|
201
|
+
.recent-file-chip .chip-folder{font-size:10px;opacity:.5;margin-left:2px;cursor:pointer}
|
|
202
|
+
.recent-file-chip .chip-folder:hover{opacity:1}
|
|
203
|
+
.chat-messages-home::-webkit-scrollbar{width:4px}
|
|
204
|
+
.chat-messages-home::-webkit-scrollbar-thumb{background:var(--border-glow);border-radius:2px}
|
|
205
|
+
|
|
206
|
+
.msg{padding:12px 16px;border-radius:14px;max-width:75%;line-height:1.6;font-size:14px;word-wrap:break-word;position:relative}
|
|
207
|
+
.msg-user{background:rgba(56,189,248,0.08);align-self:flex-end;border-bottom-right-radius:4px}
|
|
208
|
+
.msg-agent{background:rgba(139,92,246,0.06);align-self:flex-start;border-bottom-left-radius:4px}
|
|
209
|
+
.msg-time{font-size:10px;color:var(--text-muted);margin-top:6px;display:flex;align-items:center;gap:6px}
|
|
210
|
+
.msg code{background:rgba(0,0,0,0.3);padding:1px 5px;border-radius:4px;font-family:var(--font-mono);font-size:12px}
|
|
211
|
+
.msg pre{background:rgba(0,0,0,0.3);padding:12px;border-radius:8px;overflow-x:auto;margin:8px 0}
|
|
212
|
+
.msg pre code{background:none;padding:0}
|
|
213
|
+
|
|
214
|
+
.via-badge{
|
|
215
|
+
display:inline-flex;align-items:center;gap:3px;
|
|
216
|
+
padding:1px 8px;border-radius:4px;
|
|
217
|
+
background:var(--purple-bg);color:var(--purple);
|
|
218
|
+
font-family:var(--font-mono);font-size:10px;font-weight:500;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.thinking{display:flex;align-items:center;gap:10px;padding:12px 16px;color:var(--text-muted);font-size:13px}
|
|
222
|
+
.thinking-dots span{display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--accent);margin:0 2px;animation:bounce .6s infinite alternate}
|
|
223
|
+
.thinking-dots span:nth-child(2){animation-delay:.2s}
|
|
224
|
+
.thinking-dots span:nth-child(3){animation-delay:.4s}
|
|
225
|
+
@keyframes bounce{to{opacity:.3;transform:translateY(-4px)}}
|
|
226
|
+
|
|
227
|
+
.chat-input-home{
|
|
228
|
+
padding:12px 20px 16px;border-top:1px solid var(--border-dim);
|
|
229
|
+
background:var(--bg-surface);flex-shrink:0;
|
|
230
|
+
}
|
|
231
|
+
.chat-input-home-box{
|
|
232
|
+
display:flex;align-items:center;gap:10px;max-width:800px;margin:0 auto;
|
|
233
|
+
}
|
|
234
|
+
.chat-input-home-box textarea{
|
|
235
|
+
flex:1;background:var(--bg-input);border:1px solid var(--border-dim);
|
|
236
|
+
border-radius:12px;padding:10px 14px;
|
|
237
|
+
color:var(--text-primary);font-family:var(--font-sans);font-size:14px;
|
|
238
|
+
resize:none;max-height:120px;line-height:1.5;outline:none;
|
|
239
|
+
transition:border-color .2s;
|
|
240
|
+
}
|
|
241
|
+
.chat-input-home-box textarea:focus{border-color:var(--border-active)}
|
|
242
|
+
.chat-input-home-box textarea::placeholder{color:var(--text-muted)}
|
|
243
|
+
.chat-input-home-box > .home-send-btn{
|
|
244
|
+
width:40px;height:40px;border-radius:10px;border:none;
|
|
245
|
+
background:var(--accent);color:#000;cursor:pointer;
|
|
246
|
+
font-size:16px;font-weight:700;flex-shrink:0;transition:all .2s;
|
|
247
|
+
}
|
|
248
|
+
.chat-input-home-box > .home-send-btn:hover{filter:brightness(1.1);transform:scale(1.05)}
|
|
249
|
+
|
|
250
|
+
/* Stop & Queue */
|
|
251
|
+
.home-stop-btn{
|
|
252
|
+
width:40px;height:40px;border-radius:10px;border:none;
|
|
253
|
+
background:rgba(239,68,68,0.85);color:#fff;cursor:pointer;
|
|
254
|
+
display:flex;align-items:center;justify-content:center;
|
|
255
|
+
font-size:16px;transition:all .2s;flex-shrink:0;
|
|
256
|
+
}
|
|
257
|
+
.home-stop-btn:hover{filter:brightness(1.1);transform:scale(1.05);background:#ef4444}
|
|
258
|
+
.home-send-btn.queue-mode{background:var(--amber);color:#fff;font-size:11px;font-weight:700;font-family:var(--font-mono);width:auto;padding:0 14px;letter-spacing:.02em}
|
|
259
|
+
.home-send-btn.queue-mode:hover{filter:brightness(1.1);transform:scale(1.05)}
|
|
260
|
+
.home-queue{
|
|
261
|
+
max-width:800px;margin:4px auto 0;display:flex;flex-direction:column;gap:4px;
|
|
262
|
+
}
|
|
263
|
+
.home-queue-item{
|
|
264
|
+
display:flex;align-items:center;gap:8px;padding:4px 10px;
|
|
265
|
+
background:var(--bg-card);border:1px solid var(--border-dim);border-radius:8px;
|
|
266
|
+
font-size:11px;font-family:var(--font-mono);color:var(--text-secondary);
|
|
267
|
+
}
|
|
268
|
+
.home-queue-item .q-pos{color:var(--accent);font-weight:600;flex-shrink:0}
|
|
269
|
+
.home-queue-item .q-text{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
270
|
+
.home-queue-item .q-btn{
|
|
271
|
+
background:none;border:1px solid var(--border-dim);color:var(--text-muted);
|
|
272
|
+
width:22px;height:22px;border-radius:4px;cursor:pointer;font-size:10px;
|
|
273
|
+
display:flex;align-items:center;justify-content:center;
|
|
274
|
+
}
|
|
275
|
+
.home-queue-item .q-btn:hover{border-color:var(--border-glow);color:var(--text-secondary)}
|
|
276
|
+
.home-queue-item .q-btn.danger:hover{border-color:rgba(239,68,68,.3);color:#ef4444}
|
|
277
|
+
|
|
278
|
+
/* ─── Session Tabs ──────────────────────────────────────────── */
|
|
279
|
+
.session-tabs{
|
|
280
|
+
display:flex;align-items:center;gap:0;
|
|
281
|
+
padding:0 16px;
|
|
282
|
+
border-bottom:1px solid var(--border-dim);
|
|
283
|
+
background:var(--bg-surface);
|
|
284
|
+
min-height:32px;
|
|
285
|
+
overflow-x:auto;
|
|
286
|
+
scrollbar-width:none;
|
|
287
|
+
flex-shrink:0;
|
|
288
|
+
}
|
|
289
|
+
.session-tabs::-webkit-scrollbar{display:none}
|
|
290
|
+
.session-tab{
|
|
291
|
+
display:flex;align-items:center;gap:4px;
|
|
292
|
+
padding:6px 10px;
|
|
293
|
+
font-family:var(--font-mono);font-size:11px;
|
|
294
|
+
color:var(--text-muted);cursor:pointer;
|
|
295
|
+
border-bottom:2px solid transparent;
|
|
296
|
+
white-space:nowrap;transition:all .15s;
|
|
297
|
+
position:relative;
|
|
298
|
+
}
|
|
299
|
+
.session-tab:hover{color:var(--text-secondary);background:rgba(255,255,255,.03)}
|
|
300
|
+
.session-tab.active{
|
|
301
|
+
color:var(--accent);border-bottom-color:var(--accent);
|
|
302
|
+
background:rgba(34,211,238,.05);
|
|
303
|
+
}
|
|
304
|
+
.session-tab .tab-label{max-width:120px;overflow:hidden;text-overflow:ellipsis}
|
|
305
|
+
.session-tab .tab-close{
|
|
306
|
+
display:none;font-size:13px;line-height:1;
|
|
307
|
+
padding:0 2px;border-radius:3px;color:var(--text-muted);
|
|
308
|
+
cursor:pointer;margin-left:2px;
|
|
309
|
+
}
|
|
310
|
+
.session-tab:hover .tab-close{display:inline}
|
|
311
|
+
.session-tab .tab-close:hover{color:#ef4444;background:rgba(239,68,68,.15)}
|
|
312
|
+
.session-tab .tab-dot{
|
|
313
|
+
width:5px;height:5px;border-radius:50%;
|
|
314
|
+
background:var(--accent);display:none;
|
|
315
|
+
}
|
|
316
|
+
.session-tab.has-activity .tab-dot{display:inline-block}
|
|
317
|
+
.session-tab-add{
|
|
318
|
+
padding:4px 8px;font-size:14px;line-height:1;
|
|
319
|
+
color:var(--text-muted);cursor:pointer;
|
|
320
|
+
border-radius:4px;transition:all .15s;
|
|
321
|
+
flex-shrink:0;
|
|
322
|
+
}
|
|
323
|
+
.session-tab-add:hover{color:var(--accent);background:rgba(255,255,255,.05)}
|
|
324
|
+
.tab-rename-input{
|
|
325
|
+
font-family:var(--font-mono);font-size:11px;
|
|
326
|
+
background:transparent;border:1px solid var(--accent);
|
|
327
|
+
color:var(--text-primary);padding:1px 4px;border-radius:3px;
|
|
328
|
+
outline:none;width:90px;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/* Bottom agent bar in chat mode */
|
|
332
|
+
.chat-agent-bar{
|
|
333
|
+
display:flex;align-items:center;gap:8px;max-width:800px;margin:8px auto 0;flex-wrap:wrap;
|
|
334
|
+
}
|
|
335
|
+
.chat-agent-bar .org-select{flex-shrink:0}
|
|
336
|
+
.chat-tiles{
|
|
337
|
+
display:flex;gap:6px;flex-wrap:wrap;align-items:center;
|
|
338
|
+
}
|
|
339
|
+
.chat-tiles .agent-tile{padding:4px 10px;font-size:11px}
|
|
340
|
+
.chat-tiles .tile-avatar{width:20px;height:20px;font-size:8px;border-radius:5px}
|
|
341
|
+
|
|
342
|
+
/* @mention autocomplete */
|
|
343
|
+
.autocomplete{
|
|
344
|
+
position:absolute;bottom:100%;left:60px;right:60px;
|
|
345
|
+
max-height:240px;overflow-y:auto;
|
|
346
|
+
background:var(--bg-surface);border:1px solid var(--border-glow);
|
|
347
|
+
border-radius:10px;box-shadow:0 -4px 20px rgba(0,0,0,0.3);
|
|
348
|
+
display:none;z-index:20;
|
|
349
|
+
}
|
|
350
|
+
.autocomplete.visible{display:block}
|
|
351
|
+
.autocomplete-item{
|
|
352
|
+
display:flex;align-items:center;gap:10px;
|
|
353
|
+
padding:8px 14px;cursor:pointer;transition:background .15s;
|
|
354
|
+
}
|
|
355
|
+
.autocomplete-item:hover,.autocomplete-item.selected{background:var(--accent-bg)}
|
|
356
|
+
.autocomplete-item-avatar{
|
|
357
|
+
width:26px;height:26px;border-radius:6px;flex-shrink:0;
|
|
358
|
+
background:var(--accent-bg);border:1px solid rgba(34,211,238,0.3);
|
|
359
|
+
display:flex;align-items:center;justify-content:center;
|
|
360
|
+
font-family:var(--font-mono);font-size:9px;font-weight:600;color:var(--accent);
|
|
361
|
+
}
|
|
362
|
+
.autocomplete-item-info{flex:1;min-width:0}
|
|
363
|
+
.autocomplete-item-alias{font-family:var(--font-mono);font-size:12px;color:var(--accent);font-weight:500}
|
|
364
|
+
.autocomplete-item-name{font-size:11px;color:var(--text-muted)}
|
|
365
|
+
|
|
366
|
+
/* Input action buttons */
|
|
367
|
+
.input-action{
|
|
368
|
+
width:36px;height:36px;border-radius:10px;border:1px solid var(--border-dim);
|
|
369
|
+
background:transparent;color:var(--text-muted);cursor:pointer;
|
|
370
|
+
display:flex;align-items:center;justify-content:center;
|
|
371
|
+
font-size:16px;transition:all .2s;flex-shrink:0;
|
|
372
|
+
}
|
|
373
|
+
.input-action:hover{border-color:var(--border-glow);color:var(--text-secondary)}
|
|
374
|
+
.input-action.recording{border-color:#ef4444;color:#ef4444;animation:pulse-rec 1.5s infinite}
|
|
375
|
+
.input-action.has-files{border-color:var(--accent);color:var(--accent)}
|
|
376
|
+
.home-file-drop-zone{
|
|
377
|
+
display:none;padding:10px 14px;margin-bottom:6px;
|
|
378
|
+
border:2px dashed var(--border-glow);border-radius:10px;
|
|
379
|
+
background:var(--bg-input);font-size:12px;color:var(--text-muted);cursor:pointer;
|
|
380
|
+
transition:all .2s;min-height:52px;
|
|
381
|
+
flex-direction:column;align-items:center;justify-content:center;gap:6px;
|
|
382
|
+
}
|
|
383
|
+
.home-file-drop-zone.visible{display:flex}
|
|
384
|
+
.home-file-drop-zone.dragover{border-color:var(--accent);background:var(--accent-bg);color:var(--accent)}
|
|
385
|
+
.home-file-drop-zone input[type=file]{display:none}
|
|
386
|
+
.home-file-list{display:flex;flex-wrap:wrap;gap:6px;width:100%;justify-content:flex-start}
|
|
387
|
+
.home-file-pill{
|
|
388
|
+
display:inline-flex;align-items:center;gap:6px;padding:4px 9px;border-radius:7px;
|
|
389
|
+
background:var(--bg-surface);border:1px solid var(--border-dim);
|
|
390
|
+
font-family:var(--font-mono);font-size:10px;color:var(--text-primary);max-width:220px;
|
|
391
|
+
}
|
|
392
|
+
.home-file-pill-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
|
|
393
|
+
.home-file-pill-size{color:var(--text-muted);flex-shrink:0}
|
|
394
|
+
.home-file-pill-remove{cursor:pointer;color:var(--text-muted);font-size:13px;flex-shrink:0}
|
|
395
|
+
.home-file-pill-remove:hover{color:#ef4444}
|
|
396
|
+
@keyframes pulse-rec{0%,100%{opacity:1}50%{opacity:.5}}
|
|
397
|
+
|
|
398
|
+
/* Drawer overlay */
|
|
399
|
+
.drawer-overlay{
|
|
400
|
+
position:fixed;top:0;left:0;right:0;bottom:0;
|
|
401
|
+
background:rgba(0,0,0,0.5);backdrop-filter:blur(4px);
|
|
402
|
+
z-index:100;opacity:0;pointer-events:none;transition:opacity .3s;
|
|
403
|
+
}
|
|
404
|
+
.drawer-overlay.open{opacity:1;pointer-events:all}
|
|
405
|
+
|
|
406
|
+
/* Drawer panel */
|
|
407
|
+
.drawer{
|
|
408
|
+
position:fixed;top:0;left:0;bottom:0;width:320px;
|
|
409
|
+
background:var(--bg-surface);border-right:1px solid var(--border-dim);
|
|
410
|
+
z-index:101;transform:translateX(-100%);transition:transform .3s ease;
|
|
411
|
+
display:flex;flex-direction:column;
|
|
412
|
+
}
|
|
413
|
+
.drawer-overlay.open .drawer{transform:translateX(0)}
|
|
414
|
+
.drawer-header{
|
|
415
|
+
padding:16px 20px;border-bottom:1px solid var(--border-dim);
|
|
416
|
+
display:flex;align-items:center;justify-content:space-between;
|
|
417
|
+
}
|
|
418
|
+
.drawer-header-title{font-family:var(--font-display);font-size:15px;font-weight:700;color:var(--accent)}
|
|
419
|
+
.drawer-close{background:none;border:none;color:var(--text-muted);font-size:20px;cursor:pointer}
|
|
420
|
+
.drawer-close:hover{color:var(--text-primary)}
|
|
421
|
+
.drawer-list{flex:1;overflow-y:auto;padding:12px}
|
|
422
|
+
.drawer-agent{
|
|
423
|
+
display:flex;align-items:center;gap:10px;
|
|
424
|
+
padding:10px 12px;border-radius:10px;cursor:pointer;
|
|
425
|
+
transition:all .2s;margin-bottom:4px;
|
|
426
|
+
}
|
|
427
|
+
.drawer-agent:hover{background:var(--bg-card)}
|
|
428
|
+
.drawer-agent-avatar{
|
|
429
|
+
width:32px;height:32px;border-radius:8px;flex-shrink:0;
|
|
430
|
+
background:var(--accent-bg);border:1px solid rgba(34,211,238,0.3);
|
|
431
|
+
display:flex;align-items:center;justify-content:center;
|
|
432
|
+
font-family:var(--font-mono);font-size:11px;font-weight:600;color:var(--accent);
|
|
433
|
+
}
|
|
434
|
+
.drawer-agent-info{flex:1;min-width:0}
|
|
435
|
+
.drawer-agent-name{font-size:13px;font-weight:500;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
436
|
+
.drawer-agent-alias{font-size:11px;color:var(--text-muted);font-family:var(--font-mono)}
|
|
437
|
+
.drawer-agent-time{font-size:10px;color:var(--text-muted)}
|
|
438
|
+
|
|
439
|
+
/* Logs drawer (inline bottom panel — pushes chat up) */
|
|
440
|
+
.logs-drawer{
|
|
441
|
+
height:0;overflow:hidden;
|
|
442
|
+
background:var(--bg-surface);border-top:1px solid var(--border-dim);
|
|
443
|
+
transition:height .3s ease;
|
|
444
|
+
display:flex;flex-direction:column;flex-shrink:0;
|
|
445
|
+
}
|
|
446
|
+
.logs-drawer.open{height:240px}
|
|
447
|
+
.logs-header{
|
|
448
|
+
padding:8px 16px;border-bottom:1px solid var(--border-dim);
|
|
449
|
+
display:flex;align-items:center;justify-content:space-between;flex-shrink:0;
|
|
450
|
+
}
|
|
451
|
+
.logs-content{
|
|
452
|
+
flex:1;overflow-y:auto;padding:8px 16px;
|
|
453
|
+
font-family:var(--font-mono);font-size:11px;line-height:1.6;color:var(--text-secondary);
|
|
454
|
+
white-space:pre-wrap;word-break:break-all;
|
|
455
|
+
}
|
|
456
|
+
.logs-content::-webkit-scrollbar{width:4px}
|
|
457
|
+
.logs-content::-webkit-scrollbar-thumb{background:var(--border-glow);border-radius:2px}
|
|
458
|
+
.log-entry{padding:2px 0;border-bottom:1px solid rgba(255,255,255,0.03)}
|
|
459
|
+
.log-entry .log-time{color:var(--text-muted);margin-right:8px}
|
|
460
|
+
.log-entry .log-agent{color:var(--accent);font-weight:500;margin-right:6px}
|
|
461
|
+
.log-entry .log-channel{color:var(--purple);margin-right:6px}
|
|
462
|
+
.log-line{opacity:0.7}
|
|
463
|
+
.log-line:last-child{opacity:1}
|
|
464
|
+
.log-stderr{color:#ef4444}
|
|
465
|
+
.log-dim{opacity:0.4}
|
|
466
|
+
</style>
|
|
467
|
+
</head>
|
|
468
|
+
<body>
|
|
469
|
+
<div class="home">
|
|
470
|
+
<!-- Topbar (same as /ui) -->
|
|
471
|
+
<div class="topbar" id="mainTopbar">
|
|
472
|
+
<a href="/" style="display:flex;align-items:center;gap:10px;margin-right:28px;text-decoration:none">
|
|
473
|
+
<img class="logo-mark" src="/MyAIforOne-logomark-transparent.svg" alt="MyAIforOne">
|
|
474
|
+
<span class="logo-text">MyAIforOne</span>
|
|
475
|
+
</a>
|
|
476
|
+
<nav class="tab-group">
|
|
477
|
+
<a class="tab-btn active" href="/">Home</a>
|
|
478
|
+
<a class="tab-btn" href="/org">Agents</a>
|
|
479
|
+
<a class="tab-btn" href="/ui">Chat</a>
|
|
480
|
+
<a class="tab-btn" href="/library">Library</a>
|
|
481
|
+
<a class="tab-btn" href="/lab">Lab</a>
|
|
482
|
+
</nav>
|
|
483
|
+
<div class="topbar-right">
|
|
484
|
+
<a class="gym-nav-btn gym-tab-link" href="/gym" style="display:none">Gym</a>
|
|
485
|
+
<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>
|
|
486
|
+
<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>
|
|
487
|
+
<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>
|
|
488
|
+
<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>
|
|
489
|
+
<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>
|
|
490
|
+
<button class="theme-toggle" onclick="toggleTheme()" title="Toggle light/dark mode" id="themeBtn">🌙</button>
|
|
491
|
+
</div>
|
|
492
|
+
</div>
|
|
493
|
+
|
|
494
|
+
<!-- Landing state -->
|
|
495
|
+
<div class="landing" id="landing">
|
|
496
|
+
<div class="landing-logo">MyAIforOne</div>
|
|
497
|
+
<div class="landing-sub">Ask me anything</div>
|
|
498
|
+
<div class="landing-input-wrap">
|
|
499
|
+
<div class="landing-input-box">
|
|
500
|
+
<textarea class="landing-textarea" id="landingInput" placeholder="What do you need?" rows="1"
|
|
501
|
+
onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendFirst()}"
|
|
502
|
+
oninput="autoResize(this)"></textarea>
|
|
503
|
+
<button class="landing-send" onclick="sendFirst()">→</button>
|
|
504
|
+
</div>
|
|
505
|
+
<div class="agent-count" id="agentCount"></div>
|
|
506
|
+
</div>
|
|
507
|
+
<div class="tiles-wrap" id="tilesWrap">
|
|
508
|
+
<select class="org-select" id="orgFilters" onchange="setOrgFilter(this.value)"></select>
|
|
509
|
+
<div class="agent-tiles" id="agentTiles"></div>
|
|
510
|
+
</div>
|
|
511
|
+
</div>
|
|
512
|
+
|
|
513
|
+
<!-- Chat state -->
|
|
514
|
+
<div class="chat-view" id="chatView">
|
|
515
|
+
<div class="hub-actions-bar">
|
|
516
|
+
<span class="hub-label">@hub</span>
|
|
517
|
+
<select class="header-btn" id="accountSelect" style="padding:2px 6px;cursor:pointer;display:none" onchange="setAccountOverride(this.value)" title="Claude Account override"></select>
|
|
518
|
+
<button class="header-btn danger" onclick="confirmResetSession()" title="Reset hub session">↺ Reset session</button>
|
|
519
|
+
<button class="header-btn" id="configureHubLink" onclick="openConfigOverlay()" title="Configure hub agent">⚙ Configure</button>
|
|
520
|
+
<button class="canvas-toggle-btn" onclick="Canvas.toggle()" title="Toggle preview panel">▨ Canvas</button>
|
|
521
|
+
</div>
|
|
522
|
+
<div class="session-tabs" id="homeSessionTabBar"></div>
|
|
523
|
+
<div class="chat-body-wrap" id="chatBodyWrap">
|
|
524
|
+
<div class="chat-messages-home chat-messages" id="chatMessages"></div>
|
|
525
|
+
<div class="canvas-panel" id="canvasPanel">
|
|
526
|
+
<div class="canvas-resize-handle" id="canvasResizeHandle"></div>
|
|
527
|
+
<div class="canvas-header">
|
|
528
|
+
<span class="canvas-file-icon">▨</span>
|
|
529
|
+
<span class="canvas-filename" style="color:var(--text-muted)">No file open</span>
|
|
530
|
+
<button class="canvas-header-btn" onclick="Canvas.close()" title="Close">✕</button>
|
|
531
|
+
</div>
|
|
532
|
+
<div class="canvas-content" style="display:flex;align-items:center;justify-content:center;padding:40px;color:var(--text-muted);font-size:13px">
|
|
533
|
+
<div style="text-align:center">
|
|
534
|
+
<div style="font-size:32px;opacity:.3;margin-bottom:8px">▨</div>
|
|
535
|
+
<div>Click "Preview" on a file path to open it here</div>
|
|
536
|
+
</div>
|
|
537
|
+
</div>
|
|
538
|
+
</div>
|
|
539
|
+
</div>
|
|
540
|
+
<div class="recent-files-bar" id="recentFilesBar"></div>
|
|
541
|
+
<div class="logs-drawer" id="logsDrawer">
|
|
542
|
+
<div class="logs-header">
|
|
543
|
+
<span class="drawer-header-title">Raw Logs</span>
|
|
544
|
+
<div style="display:flex;gap:8px;align-items:center">
|
|
545
|
+
<button class="input-action" style="width:28px;height:28px;font-size:12px" onclick="clearLogs()" title="Clear">✕</button>
|
|
546
|
+
<button class="input-action" style="width:28px;height:28px;font-size:12px" onclick="toggleLogs()" title="Close">▼</button>
|
|
547
|
+
</div>
|
|
548
|
+
</div>
|
|
549
|
+
<div class="logs-content" id="logsContent"></div>
|
|
550
|
+
</div>
|
|
551
|
+
<div class="chat-input-home" style="position:relative">
|
|
552
|
+
<div class="autocomplete" id="autocomplete"></div>
|
|
553
|
+
<div class="home-file-drop-zone" id="homeFileDropZone"
|
|
554
|
+
onclick="document.getElementById('homeFileInput').click()"
|
|
555
|
+
ondragover="event.preventDefault();this.classList.add('dragover')"
|
|
556
|
+
ondragleave="this.classList.remove('dragover')"
|
|
557
|
+
ondrop="event.preventDefault();this.classList.remove('dragover');handleHomeFileDrop(event)">
|
|
558
|
+
<input type="file" id="homeFileInput" multiple onchange="handleHomeFiles(event)" style="display:none">
|
|
559
|
+
<div id="homeFileListArea"><span>Drop files here or click to browse</span></div>
|
|
560
|
+
</div>
|
|
561
|
+
<div class="chat-input-home-box">
|
|
562
|
+
<button class="input-action" onclick="toggleDrawer()" title="Agents">☰</button>
|
|
563
|
+
<button class="input-action" onclick="toggleLogs()" title="Raw Logs (live claude -p output)">⌘</button>
|
|
564
|
+
<button class="input-action" id="homeClipBtn" onclick="toggleHomeFileZone()" title="Attach file">📎</button>
|
|
565
|
+
<button class="input-action" id="homeMicBtn" onclick="toggleHomeVoice()" title="Voice input">🎤</button>
|
|
566
|
+
<button class="input-action" id="voiceModeBtn" onclick="toggleVoiceMode()" title="Read responses aloud" style="font-size:14px">🔇</button>
|
|
567
|
+
<textarea id="chatInput" placeholder="Ask anything... (type @ to mention an agent)" rows="1"
|
|
568
|
+
onkeydown="handleChatKeydown(event)"
|
|
569
|
+
oninput="autoResize(this);checkAutocomplete(this)"></textarea>
|
|
570
|
+
<button class="home-send-btn" id="homeSendBtn" onclick="sendMessage()">→</button>
|
|
571
|
+
</div>
|
|
572
|
+
<div id="homeQueueContainer"></div>
|
|
573
|
+
<div class="chat-agent-bar">
|
|
574
|
+
<select class="org-select" id="chatOrgFilters" onchange="setOrgFilter(this.value)"></select>
|
|
575
|
+
<div class="chat-tiles" id="chatTiles"></div>
|
|
576
|
+
</div>
|
|
577
|
+
</div>
|
|
578
|
+
</div>
|
|
579
|
+
|
|
580
|
+
<!-- Agent drawer -->
|
|
581
|
+
<div class="drawer-overlay" id="drawerOverlay" onclick="toggleDrawer()">
|
|
582
|
+
<div class="drawer" onclick="event.stopPropagation()">
|
|
583
|
+
<div class="drawer-header">
|
|
584
|
+
<span class="drawer-header-title">Agents</span>
|
|
585
|
+
<button class="drawer-close" onclick="toggleDrawer()">×</button>
|
|
586
|
+
</div>
|
|
587
|
+
<div class="drawer-list" id="drawerList"></div>
|
|
588
|
+
</div>
|
|
589
|
+
</div>
|
|
590
|
+
|
|
591
|
+
</div>
|
|
592
|
+
|
|
593
|
+
<script>
|
|
594
|
+
let agents = [];
|
|
595
|
+
let messages = [];
|
|
596
|
+
let hubAgentId = null;
|
|
597
|
+
|
|
598
|
+
// Initialize Canvas module — uses hubAgentId for file downloads
|
|
599
|
+
Canvas.init({ getAgentId: () => hubAgentId, escapeHtml: s => (s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>') });
|
|
600
|
+
|
|
601
|
+
// ─── Recent Files Tracking ─────────────────────────────────────
|
|
602
|
+
let recentFiles = [];
|
|
603
|
+
const FILE_TOOLS = new Set(['Write', 'Edit', 'NotebookEdit']);
|
|
604
|
+
|
|
605
|
+
function trackFileFromTool(tool) {
|
|
606
|
+
if (!tool || !tool.input) return;
|
|
607
|
+
let filePath = null;
|
|
608
|
+
if (FILE_TOOLS.has(tool.name) && tool.input.file_path) {
|
|
609
|
+
filePath = tool.input.file_path;
|
|
610
|
+
} else if (tool.name === 'Bash' && tool.input.command) {
|
|
611
|
+
const m = tool.input.command.match(/>\s*["']?([^\s"'|&;]+\.\w+)/);
|
|
612
|
+
if (m) filePath = m[1];
|
|
613
|
+
}
|
|
614
|
+
if (!filePath) return;
|
|
615
|
+
const fileName = filePath.split('/').pop() || filePath;
|
|
616
|
+
const idx = recentFiles.findIndex(f => f.path === filePath);
|
|
617
|
+
if (idx >= 0) { recentFiles[idx].ts = Date.now(); }
|
|
618
|
+
else { recentFiles.unshift({ name: fileName, path: filePath, ts: Date.now() }); if (recentFiles.length > 20) recentFiles.pop(); }
|
|
619
|
+
renderRecentFiles();
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function renderRecentFiles() {
|
|
623
|
+
const bar = document.getElementById('recentFilesBar');
|
|
624
|
+
if (!bar || recentFiles.length === 0) { if (bar) bar.innerHTML = ''; return; }
|
|
625
|
+
const icons = {csv:'📊',json:'📋',md:'📝',txt:'📝',html:'🌐',js:'⚡',ts:'⚡',py:'🐍',css:'🎨',pdf:'📄',png:'🖼️',jpg:'🖼️',svg:'🖼️',xlsx:'📊',docx:'📃'};
|
|
626
|
+
let html = '<span class="recent-files-label">Recent Files</span>';
|
|
627
|
+
for (const f of recentFiles.slice(0, 10)) {
|
|
628
|
+
const ext = (f.name.split('.').pop() || '').toLowerCase();
|
|
629
|
+
const icon = icons[ext] || '📁';
|
|
630
|
+
const enc = encodeURIComponent(f.path);
|
|
631
|
+
html += `<span class="recent-file-chip" title="${escapeHtml(f.path)}"><span class="chip-icon" onclick="Canvas.openFile('${enc}')">${icon}</span><span onclick="Canvas.openFile('${enc}')">${escapeHtml(f.name)}</span><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></span>`;
|
|
632
|
+
}
|
|
633
|
+
bar.innerHTML = html;
|
|
634
|
+
}
|
|
635
|
+
let isThinking = false;
|
|
636
|
+
let streamingText = '';
|
|
637
|
+
let voiceModeOn = false;
|
|
638
|
+
let homeQueue = [];
|
|
639
|
+
let currentJobId = null;
|
|
640
|
+
let availableAccounts = [];
|
|
641
|
+
let accountOverride = undefined;
|
|
642
|
+
|
|
643
|
+
// ─── Session Tabs ──────────────────────────────────────────────
|
|
644
|
+
// Home page tabs — each tab has its own message list, streaming state, and queue.
|
|
645
|
+
let homeTabs = []; // [{id, label, createdAt}]
|
|
646
|
+
let homeActiveTabId = null;
|
|
647
|
+
let tabMessages = {}; // tabId -> messages[]
|
|
648
|
+
let tabStreaming = {}; // tabId -> {text, isThinking, jobId, queue}
|
|
649
|
+
|
|
650
|
+
function loadHomeTabs() {
|
|
651
|
+
try {
|
|
652
|
+
const t = localStorage.getItem('homeTabs');
|
|
653
|
+
const a = localStorage.getItem('homeActiveTab');
|
|
654
|
+
if (t) homeTabs = JSON.parse(t);
|
|
655
|
+
if (a) homeActiveTabId = JSON.parse(a);
|
|
656
|
+
} catch {}
|
|
657
|
+
if (!homeTabs.length) {
|
|
658
|
+
homeTabs = [{ id: 'default', label: 'Session 1', createdAt: Date.now() }];
|
|
659
|
+
homeActiveTabId = 'default';
|
|
660
|
+
}
|
|
661
|
+
if (!homeActiveTabId) homeActiveTabId = homeTabs[0]?.id;
|
|
662
|
+
}
|
|
663
|
+
function saveHomeTabs() {
|
|
664
|
+
try {
|
|
665
|
+
localStorage.setItem('homeTabs', JSON.stringify(homeTabs));
|
|
666
|
+
localStorage.setItem('homeActiveTab', JSON.stringify(homeActiveTabId));
|
|
667
|
+
} catch {}
|
|
668
|
+
}
|
|
669
|
+
loadHomeTabs();
|
|
670
|
+
|
|
671
|
+
function getTabState(tabId) {
|
|
672
|
+
if (!tabStreaming[tabId]) tabStreaming[tabId] = { text: '', isThinking: false, jobId: null, queue: [] };
|
|
673
|
+
return tabStreaming[tabId];
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function syncFromActiveTab() {
|
|
677
|
+
const ts = getTabState(homeActiveTabId);
|
|
678
|
+
messages = tabMessages[homeActiveTabId] || [];
|
|
679
|
+
isThinking = ts.isThinking;
|
|
680
|
+
streamingText = ts.text;
|
|
681
|
+
currentJobId = ts.jobId;
|
|
682
|
+
homeQueue = ts.queue;
|
|
683
|
+
}
|
|
684
|
+
function syncToActiveTab() {
|
|
685
|
+
tabMessages[homeActiveTabId] = messages;
|
|
686
|
+
const ts = getTabState(homeActiveTabId);
|
|
687
|
+
ts.isThinking = isThinking;
|
|
688
|
+
ts.text = streamingText;
|
|
689
|
+
ts.jobId = currentJobId;
|
|
690
|
+
ts.queue = homeQueue;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function createHomeTab() {
|
|
694
|
+
syncToActiveTab();
|
|
695
|
+
const num = homeTabs.length + 1;
|
|
696
|
+
const id = 'htab-' + Date.now().toString(36);
|
|
697
|
+
homeTabs.push({ id, label: 'Session ' + num, createdAt: Date.now() });
|
|
698
|
+
homeActiveTabId = id;
|
|
699
|
+
tabMessages[id] = [];
|
|
700
|
+
tabStreaming[id] = { text: '', isThinking: false, jobId: null, queue: [] };
|
|
701
|
+
saveHomeTabs();
|
|
702
|
+
syncFromActiveTab();
|
|
703
|
+
renderMessages();
|
|
704
|
+
scrollChat();
|
|
705
|
+
renderHomeTabBar();
|
|
706
|
+
updateHomeInputButtons();
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function closeHomeTab(tabId) {
|
|
710
|
+
if (homeTabs.length <= 1) return;
|
|
711
|
+
const ts = getTabState(tabId);
|
|
712
|
+
if (ts.isThinking || ts.jobId) {
|
|
713
|
+
if (!confirm('This session has an active stream. Close anyway?')) return;
|
|
714
|
+
}
|
|
715
|
+
const idx = homeTabs.findIndex(t => t.id === tabId);
|
|
716
|
+
if (idx < 0) return;
|
|
717
|
+
homeTabs.splice(idx, 1);
|
|
718
|
+
delete tabMessages[tabId];
|
|
719
|
+
delete tabStreaming[tabId];
|
|
720
|
+
if (homeActiveTabId === tabId) {
|
|
721
|
+
homeActiveTabId = homeTabs[Math.min(idx, homeTabs.length - 1)].id;
|
|
722
|
+
}
|
|
723
|
+
saveHomeTabs();
|
|
724
|
+
syncFromActiveTab();
|
|
725
|
+
renderMessages();
|
|
726
|
+
scrollChat();
|
|
727
|
+
renderHomeTabBar();
|
|
728
|
+
updateHomeInputButtons();
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function switchHomeTab(tabId) {
|
|
732
|
+
if (homeActiveTabId === tabId) return;
|
|
733
|
+
syncToActiveTab();
|
|
734
|
+
homeActiveTabId = tabId;
|
|
735
|
+
saveHomeTabs();
|
|
736
|
+
syncFromActiveTab();
|
|
737
|
+
renderMessages();
|
|
738
|
+
scrollChat();
|
|
739
|
+
renderHomeTabBar();
|
|
740
|
+
updateHomeInputButtons();
|
|
741
|
+
renderHomeQueue();
|
|
742
|
+
const inp = document.getElementById('chatInput');
|
|
743
|
+
if (inp) inp.focus();
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function startRenameHomeTab(tabId) {
|
|
747
|
+
const el = document.querySelector(`.session-tab[data-tab-id="${tabId}"] .tab-label`);
|
|
748
|
+
if (!el) return;
|
|
749
|
+
const tab = homeTabs.find(t => t.id === tabId);
|
|
750
|
+
if (!tab) return;
|
|
751
|
+
const input = document.createElement('input');
|
|
752
|
+
input.className = 'tab-rename-input';
|
|
753
|
+
input.value = tab.label;
|
|
754
|
+
input.onblur = () => finishRenameHomeTab(tabId, input.value);
|
|
755
|
+
input.onkeydown = (e) => { if (e.key === 'Enter') input.blur(); if (e.key === 'Escape') { input.value = tab.label; input.blur(); } };
|
|
756
|
+
el.replaceWith(input);
|
|
757
|
+
input.focus();
|
|
758
|
+
input.select();
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function finishRenameHomeTab(tabId, newLabel) {
|
|
762
|
+
const tab = homeTabs.find(t => t.id === tabId);
|
|
763
|
+
if (tab && newLabel.trim()) {
|
|
764
|
+
tab.label = newLabel.trim();
|
|
765
|
+
saveHomeTabs();
|
|
766
|
+
}
|
|
767
|
+
renderHomeTabBar();
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function renderHomeTabBar() {
|
|
771
|
+
const container = document.getElementById('homeSessionTabBar');
|
|
772
|
+
if (!container) return;
|
|
773
|
+
let html = homeTabs.map(t => {
|
|
774
|
+
const isActive = t.id === homeActiveTabId;
|
|
775
|
+
const ts = tabStreaming[t.id];
|
|
776
|
+
const hasActivity = !isActive && ts && (ts.isThinking || ts.jobId);
|
|
777
|
+
return `<div class="session-tab${isActive ? ' active' : ''}${hasActivity ? ' has-activity' : ''}" data-tab-id="${t.id}"
|
|
778
|
+
onclick="switchHomeTab('${t.id}')"
|
|
779
|
+
ondblclick="startRenameHomeTab('${t.id}')">
|
|
780
|
+
<span class="tab-dot"></span>
|
|
781
|
+
<span class="tab-label">${escapeHtml(t.label)}</span>
|
|
782
|
+
${homeTabs.length > 1 ? `<span class="tab-close" onclick="event.stopPropagation();closeHomeTab('${t.id}')">×</span>` : ''}
|
|
783
|
+
</div>`;
|
|
784
|
+
}).join('');
|
|
785
|
+
html += `<div class="session-tab-add" onclick="createHomeTab()" title="New session">+</div>`;
|
|
786
|
+
container.innerHTML = html;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function toggleVoiceMode() {
|
|
790
|
+
voiceModeOn = !voiceModeOn;
|
|
791
|
+
const btn = document.getElementById('voiceModeBtn');
|
|
792
|
+
if (btn) {
|
|
793
|
+
btn.textContent = voiceModeOn ? '🔊' : '🔇';
|
|
794
|
+
btn.style.color = voiceModeOn ? 'var(--accent)' : '';
|
|
795
|
+
btn.title = voiceModeOn ? 'Voice mode on — click to mute' : 'Read responses aloud';
|
|
796
|
+
}
|
|
797
|
+
if (!voiceModeOn) window.speechSynthesis?.cancel();
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function speakText(text) {
|
|
801
|
+
if (!voiceModeOn || !window.speechSynthesis) return;
|
|
802
|
+
// Strip markdown symbols and [via:...] tags before speaking
|
|
803
|
+
const clean = text
|
|
804
|
+
.replace(/\[via:[^\]]+\]/g, '')
|
|
805
|
+
.replace(/[*_`#>~]/g, '')
|
|
806
|
+
.replace(/\n+/g, ' ')
|
|
807
|
+
.trim();
|
|
808
|
+
if (!clean) return;
|
|
809
|
+
window.speechSynthesis.cancel();
|
|
810
|
+
const utterance = new SpeechSynthesisUtterance(clean);
|
|
811
|
+
utterance.rate = 1.05;
|
|
812
|
+
window.speechSynthesis.speak(utterance);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// ─── Init ──────────────────────────────────────────────
|
|
816
|
+
async function init() {
|
|
817
|
+
try {
|
|
818
|
+
const res = await fetch('/api/dashboard');
|
|
819
|
+
const data = await res.json();
|
|
820
|
+
agents = data.agents || [];
|
|
821
|
+
availableAccounts = data.claudeAccounts || [];
|
|
822
|
+
|
|
823
|
+
// Use the default group agent from config, or find first group agent
|
|
824
|
+
hubAgentId = data.defaultGroupAgent
|
|
825
|
+
|| agents.find(a => a.subAgents)?.id
|
|
826
|
+
|| agents[0]?.id;
|
|
827
|
+
|
|
828
|
+
// Update configure link to point to hub agent dashboard
|
|
829
|
+
const configLink = document.getElementById('configureHubLink');
|
|
830
|
+
// configureHubLink is now a button; hubAgentId used by openConfigOverlay()
|
|
831
|
+
|
|
832
|
+
// Populate account dropdown
|
|
833
|
+
if (availableAccounts.length > 0 && hubAgentId) {
|
|
834
|
+
const sel = document.getElementById('accountSelect');
|
|
835
|
+
const hubAgent = agents.find(a => a.id === hubAgentId);
|
|
836
|
+
const defaultAcct = hubAgent?.claudeAccount || 'default';
|
|
837
|
+
sel.innerHTML = `<option value="">${defaultAcct}</option>` +
|
|
838
|
+
availableAccounts.filter(a => a !== hubAgent?.claudeAccount).map(a =>
|
|
839
|
+
`<option value="${a}">${a}</option>`
|
|
840
|
+
).join('');
|
|
841
|
+
sel.style.display = '';
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// ─── Fresh install detection ─────────────────────────
|
|
845
|
+
const personalAgents = agents.filter(a => a.agentClass !== 'platform' && !a.subAgents);
|
|
846
|
+
const channelList = data.channels || [];
|
|
847
|
+
const hasChannels = channelList.length > 0;
|
|
848
|
+
const hasPersonalAgents = personalAgents.length > 0;
|
|
849
|
+
const onboardingDismissed = localStorage.getItem('onboardingDismissed');
|
|
850
|
+
|
|
851
|
+
const isSetupMode = new URLSearchParams(window.location.search).get('setup') === 'true';
|
|
852
|
+
|
|
853
|
+
if (!hasPersonalAgents && !onboardingDismissed && !isSetupMode) {
|
|
854
|
+
// Fresh install — redirect to /monitor which has setup checklist
|
|
855
|
+
window.location.href = '/monitor';
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// If coming from monitor "Start Setup" button, go straight to hub chat with onboarding prompt
|
|
860
|
+
if (isSetupMode) {
|
|
861
|
+
history.replaceState(null, '', '/'); // clean URL
|
|
862
|
+
transitionToChat();
|
|
863
|
+
const onboardingPrompt = "I just set up MyAIforOne for the first time. Please use the /onboarding skill to walk me through connecting a messaging channel and creating my first personal agent.";
|
|
864
|
+
addMessage('user', onboardingPrompt);
|
|
865
|
+
syncToActiveTab();
|
|
866
|
+
executeHub(onboardingPrompt, homeActiveTabId);
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Agent count
|
|
871
|
+
document.getElementById('agentCount').textContent =
|
|
872
|
+
`Powered by ${agents.length} agents · ${data.mcpCount || 0} integrations`;
|
|
873
|
+
|
|
874
|
+
// Render recent agent tiles
|
|
875
|
+
renderTiles();
|
|
876
|
+
|
|
877
|
+
// Load previous messages from hub's conversation log into the default tab
|
|
878
|
+
if (hubAgentId) {
|
|
879
|
+
try {
|
|
880
|
+
const msgRes = await fetch(`/api/agents/${hubAgentId}`);
|
|
881
|
+
const msgData = await msgRes.json();
|
|
882
|
+
if (msgData.recentMessages && msgData.recentMessages.length > 0) {
|
|
883
|
+
const loaded = msgData.recentMessages.map(m => ([
|
|
884
|
+
{ role: 'user', text: m.text, time: m.ts },
|
|
885
|
+
{ role: 'agent', text: m.response, time: m.ts },
|
|
886
|
+
])).flat().filter(m => m.text);
|
|
887
|
+
// Put into default tab if it has no messages yet
|
|
888
|
+
const defId = homeTabs[0]?.id || 'default';
|
|
889
|
+
if (!tabMessages[defId] || !tabMessages[defId].length) {
|
|
890
|
+
tabMessages[defId] = loaded;
|
|
891
|
+
}
|
|
892
|
+
syncFromActiveTab();
|
|
893
|
+
// Returning user — skip landing, go straight to chat
|
|
894
|
+
transitionToChat();
|
|
895
|
+
renderMessages();
|
|
896
|
+
scrollChat();
|
|
897
|
+
}
|
|
898
|
+
} catch { /* ignore */ }
|
|
899
|
+
}
|
|
900
|
+
renderHomeTabBar();
|
|
901
|
+
} catch { /* ignore */ }
|
|
902
|
+
|
|
903
|
+
// Check for ?agent= param (e.g. from Apps registry "@ Dev" button)
|
|
904
|
+
const agentParam = new URLSearchParams(window.location.search).get('agent');
|
|
905
|
+
if(agentParam) {
|
|
906
|
+
const found = agents.find(a => a.id === agentParam);
|
|
907
|
+
const alias = found?.aliases?.[0] || ('@' + agentParam);
|
|
908
|
+
// Clean URL without reloading
|
|
909
|
+
history.replaceState(null, '', '/');
|
|
910
|
+
mentionAgent(alias);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
let activeOrgFilter = 'recent'; // 'recent' | org name
|
|
915
|
+
|
|
916
|
+
function renderTiles() {
|
|
917
|
+
// Build org list from all agents
|
|
918
|
+
const orgs = [...new Set(
|
|
919
|
+
agents.flatMap(a => (a.org || []).map(o => o.organization).filter(Boolean))
|
|
920
|
+
)].sort();
|
|
921
|
+
|
|
922
|
+
// Render filter dropdowns (landing + chat)
|
|
923
|
+
const optionsHtml = [
|
|
924
|
+
`<option value="recent"${activeOrgFilter==='recent'?' selected':''}>Recent</option>`,
|
|
925
|
+
...orgs.map(org => `<option value="${org.replace(/"/g,'"')}"${activeOrgFilter===org?' selected':''}>${org}</option>`)
|
|
926
|
+
].join('');
|
|
927
|
+
const f1 = document.getElementById('orgFilters');
|
|
928
|
+
const f2 = document.getElementById('chatOrgFilters');
|
|
929
|
+
if (f1) f1.innerHTML = optionsHtml;
|
|
930
|
+
if (f2) f2.innerHTML = optionsHtml;
|
|
931
|
+
|
|
932
|
+
// Filter agents
|
|
933
|
+
let filtered;
|
|
934
|
+
if (activeOrgFilter === 'recent') {
|
|
935
|
+
filtered = agents
|
|
936
|
+
.filter(a => a.lastMessage && a.lastMessage !== 'never' && !a.subAgents)
|
|
937
|
+
.sort((a, b) => new Date(b.lastMessage) - new Date(a.lastMessage))
|
|
938
|
+
.slice(0, 7);
|
|
939
|
+
} else {
|
|
940
|
+
filtered = agents
|
|
941
|
+
.filter(a => !a.subAgents && (a.org || []).some(o => o.organization === activeOrgFilter))
|
|
942
|
+
.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
const html = filtered.map(a => {
|
|
946
|
+
const initials = (a.aliases?.[0] || a.id).replace('@','').slice(0,2).toUpperCase();
|
|
947
|
+
const alias = a.aliases?.[0] || ('@' + a.id);
|
|
948
|
+
const time = activeOrgFilter === 'recent' ? `<span class="tile-time">${formatRelativeTime(a.lastMessage)}</span>` : '';
|
|
949
|
+
return `<div class="agent-tile" onclick="mentionAgent('${alias}')">
|
|
950
|
+
<div class="tile-avatar">${initials}</div>
|
|
951
|
+
<span class="tile-name">${a.name}</span>
|
|
952
|
+
${time}
|
|
953
|
+
</div>`;
|
|
954
|
+
}).join('') || `<span style="font-size:12px;color:var(--text-muted)">No agents in this org</span>`;
|
|
955
|
+
|
|
956
|
+
const t1 = document.getElementById('agentTiles');
|
|
957
|
+
const t2 = document.getElementById('chatTiles');
|
|
958
|
+
if (t1) t1.innerHTML = html;
|
|
959
|
+
if (t2) t2.innerHTML = html;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function setOrgFilter(org) {
|
|
963
|
+
activeOrgFilter = org;
|
|
964
|
+
renderTiles();
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// ─── Mention agent from tile ───────────────────────────
|
|
968
|
+
function mentionAgent(alias) {
|
|
969
|
+
if (!document.getElementById('chatView').classList.contains('active')) {
|
|
970
|
+
transitionToChat();
|
|
971
|
+
}
|
|
972
|
+
const input = document.getElementById('chatInput');
|
|
973
|
+
const val = input.value;
|
|
974
|
+
// If input already ends with the alias, just focus
|
|
975
|
+
if (val.trimEnd().endsWith(alias)) {
|
|
976
|
+
input.focus();
|
|
977
|
+
input.setSelectionRange(input.value.length, input.value.length);
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
// Prepend alias (clear any existing @-start or append)
|
|
981
|
+
input.value = alias + ' ' + val.trimStart();
|
|
982
|
+
input.focus();
|
|
983
|
+
input.setSelectionRange(input.value.length, input.value.length);
|
|
984
|
+
autoResize(input);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// ─── Transition ────────────────────────────────────────
|
|
988
|
+
function transitionToChat() {
|
|
989
|
+
document.getElementById('landing').classList.add('hidden');
|
|
990
|
+
document.getElementById('chatView').classList.add('active');
|
|
991
|
+
setTimeout(() => {
|
|
992
|
+
document.getElementById('chatInput').focus();
|
|
993
|
+
scrollChat();
|
|
994
|
+
renderHomeTabBar();
|
|
995
|
+
}, 100);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// ─── @alias parsing ────────────────────────────────────
|
|
999
|
+
function parseDirectAgent(text) {
|
|
1000
|
+
const match = text.match(/^@(\S+)\s+([\s\S]+)/);
|
|
1001
|
+
if (!match) return null;
|
|
1002
|
+
const alias = '@' + match[1];
|
|
1003
|
+
const agent = agents.find(a => (a.aliases || []).some(al => al.toLowerCase() === alias.toLowerCase()));
|
|
1004
|
+
if (!agent) return null;
|
|
1005
|
+
return { agentId: agent.id, message: match[2].trim(), agent };
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// ─── Send first message (from landing) ─────────────────
|
|
1009
|
+
function sendFirst() {
|
|
1010
|
+
const input = document.getElementById('landingInput');
|
|
1011
|
+
const text = input.value.trim();
|
|
1012
|
+
if (!text || !hubAgentId) return;
|
|
1013
|
+
input.value = '';
|
|
1014
|
+
transitionToChat();
|
|
1015
|
+
addMessage('user', text);
|
|
1016
|
+
syncToActiveTab();
|
|
1017
|
+
const direct = parseDirectAgent(text);
|
|
1018
|
+
if (direct) {
|
|
1019
|
+
executeDirectAgent(direct.agentId, direct.message, homeActiveTabId);
|
|
1020
|
+
} else {
|
|
1021
|
+
executeHub(text, homeActiveTabId);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// ─── Send message (from chat) ──────────────────────────
|
|
1026
|
+
async function sendMessage(queuedText) {
|
|
1027
|
+
const input = document.getElementById('chatInput');
|
|
1028
|
+
let text = queuedText || input.value.trim();
|
|
1029
|
+
if ((!text && !homeFiles.length) || !hubAgentId) return;
|
|
1030
|
+
const tabId = homeActiveTabId;
|
|
1031
|
+
const ts = getTabState(tabId);
|
|
1032
|
+
|
|
1033
|
+
// Queue if already streaming or job active
|
|
1034
|
+
if ((ts.isThinking || ts.jobId) && !queuedText) {
|
|
1035
|
+
ts.queue.push(text);
|
|
1036
|
+
homeQueue = ts.queue;
|
|
1037
|
+
input.value = '';
|
|
1038
|
+
autoResize(input);
|
|
1039
|
+
hideAutocomplete();
|
|
1040
|
+
renderHomeQueue();
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
if (!queuedText) { input.value = ''; autoResize(input); hideAutocomplete(); }
|
|
1045
|
+
|
|
1046
|
+
// Upload any attached files and append file notes to text
|
|
1047
|
+
if (homeFiles.length > 0) {
|
|
1048
|
+
const uploaded = await uploadHomeFiles(hubAgentId);
|
|
1049
|
+
if (uploaded.length > 0) {
|
|
1050
|
+
const fileNotes = uploaded.map(f => `[File attached: ${f.name} at ${f.path}]`).join('\n');
|
|
1051
|
+
text = text ? `${text}\n\n${fileNotes}` : fileNotes;
|
|
1052
|
+
}
|
|
1053
|
+
homeFiles = [];
|
|
1054
|
+
renderHomeFiles();
|
|
1055
|
+
const zone = document.getElementById('homeFileDropZone');
|
|
1056
|
+
if (zone) zone.classList.remove('visible');
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
addMessage('user', text, tabId);
|
|
1060
|
+
syncToActiveTab();
|
|
1061
|
+
updateHomeInputButtons();
|
|
1062
|
+
const direct = parseDirectAgent(text);
|
|
1063
|
+
if (direct) {
|
|
1064
|
+
executeDirectAgent(direct.agentId, direct.message, tabId);
|
|
1065
|
+
} else {
|
|
1066
|
+
executeHub(text, tabId);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
async function uploadHomeFiles(agentId) {
|
|
1071
|
+
const uploaded = [];
|
|
1072
|
+
for (const f of homeFiles) {
|
|
1073
|
+
try {
|
|
1074
|
+
const formData = new FormData();
|
|
1075
|
+
formData.append('file', f);
|
|
1076
|
+
formData.append('mode', 'temporary');
|
|
1077
|
+
const resp = await fetch(`/api/upload/${agentId}`, { method: 'POST', body: formData });
|
|
1078
|
+
const data = await resp.json();
|
|
1079
|
+
if (data.ok) uploaded.push({ name: data.fileName, path: data.path });
|
|
1080
|
+
} catch (err) {
|
|
1081
|
+
console.warn('Upload failed:', f.name, err);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
return uploaded;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// ─── Direct agent execution (bypasses hub) ─────────────
|
|
1088
|
+
async function executeDirectAgent(agentId, text, tabId) {
|
|
1089
|
+
const tid = tabId || homeActiveTabId;
|
|
1090
|
+
const ts = getTabState(tid);
|
|
1091
|
+
ts.isThinking = true;
|
|
1092
|
+
if (tid === homeActiveTabId) { isThinking = true; }
|
|
1093
|
+
renderMessages();
|
|
1094
|
+
scrollChat();
|
|
1095
|
+
|
|
1096
|
+
try {
|
|
1097
|
+
const res = await fetch('/api/delegate', {
|
|
1098
|
+
method: 'POST',
|
|
1099
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1100
|
+
body: JSON.stringify({ agentId, text }),
|
|
1101
|
+
});
|
|
1102
|
+
const data = await res.json();
|
|
1103
|
+
ts.isThinking = false;
|
|
1104
|
+
if (tid === homeActiveTabId) isThinking = false;
|
|
1105
|
+
if (data.ok) {
|
|
1106
|
+
addMessage('agent', data.response + ` [via:${agentId}]`, tid);
|
|
1107
|
+
} else {
|
|
1108
|
+
addMessage('agent', 'Error: ' + (data.error || 'Unknown error'), tid);
|
|
1109
|
+
}
|
|
1110
|
+
} catch (err) {
|
|
1111
|
+
ts.isThinking = false;
|
|
1112
|
+
if (tid === homeActiveTabId) isThinking = false;
|
|
1113
|
+
addMessage('agent', 'Connection error: ' + err.message, tid);
|
|
1114
|
+
}
|
|
1115
|
+
renderMessages();
|
|
1116
|
+
scrollChat();
|
|
1117
|
+
renderHomeTabBar();
|
|
1118
|
+
updateHomeInputButtons();
|
|
1119
|
+
// Drain queue: auto-send next queued message
|
|
1120
|
+
if (ts.queue.length > 0) {
|
|
1121
|
+
const next = ts.queue.shift();
|
|
1122
|
+
if (tid === homeActiveTabId) { homeQueue = ts.queue; }
|
|
1123
|
+
renderHomeQueue();
|
|
1124
|
+
sendMessage(next);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// ─── @mention autocomplete ─────────────────────────────
|
|
1129
|
+
let acSelectedIdx = -1;
|
|
1130
|
+
let acFiltered = [];
|
|
1131
|
+
|
|
1132
|
+
function checkAutocomplete(el) {
|
|
1133
|
+
const text = el.value;
|
|
1134
|
+
const cursorPos = el.selectionStart;
|
|
1135
|
+
// Find @mention being typed at cursor position
|
|
1136
|
+
const before = text.slice(0, cursorPos);
|
|
1137
|
+
const atMatch = before.match(/@(\w*)$/);
|
|
1138
|
+
|
|
1139
|
+
if (!atMatch) { hideAutocomplete(); return; }
|
|
1140
|
+
|
|
1141
|
+
const query = atMatch[1].toLowerCase();
|
|
1142
|
+
acFiltered = agents
|
|
1143
|
+
.filter(a => !a.subAgents) // exclude group agents
|
|
1144
|
+
.filter(a => {
|
|
1145
|
+
const alias = (a.aliases?.[0] || '').toLowerCase();
|
|
1146
|
+
const name = a.name.toLowerCase();
|
|
1147
|
+
return alias.includes(query) || name.includes(query) || a.id.includes(query);
|
|
1148
|
+
})
|
|
1149
|
+
.slice(0, 8);
|
|
1150
|
+
|
|
1151
|
+
if (acFiltered.length === 0) { hideAutocomplete(); return; }
|
|
1152
|
+
|
|
1153
|
+
acSelectedIdx = 0;
|
|
1154
|
+
renderAutocomplete();
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
function renderAutocomplete() {
|
|
1158
|
+
const el = document.getElementById('autocomplete');
|
|
1159
|
+
el.innerHTML = acFiltered.map((a, i) => {
|
|
1160
|
+
const initials = (a.aliases?.[0] || a.id).replace('@','').slice(0,2).toUpperCase();
|
|
1161
|
+
const alias = a.aliases?.[0] || a.id;
|
|
1162
|
+
return `<div class="autocomplete-item${i === acSelectedIdx ? ' selected' : ''}" onmousedown="selectAutocomplete(${i})">
|
|
1163
|
+
<div class="autocomplete-item-avatar">${initials}</div>
|
|
1164
|
+
<div class="autocomplete-item-info">
|
|
1165
|
+
<div class="autocomplete-item-alias">${alias}</div>
|
|
1166
|
+
<div class="autocomplete-item-name">${escapeHtml(a.name)}</div>
|
|
1167
|
+
</div>
|
|
1168
|
+
</div>`;
|
|
1169
|
+
}).join('');
|
|
1170
|
+
el.classList.add('visible');
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
function hideAutocomplete() {
|
|
1174
|
+
document.getElementById('autocomplete')?.classList.remove('visible');
|
|
1175
|
+
acFiltered = [];
|
|
1176
|
+
acSelectedIdx = -1;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function selectAutocomplete(idx) {
|
|
1180
|
+
const agent = acFiltered[idx];
|
|
1181
|
+
if (!agent) return;
|
|
1182
|
+
const alias = agent.aliases?.[0] || '@' + agent.id;
|
|
1183
|
+
const input = document.getElementById('chatInput') || document.getElementById('landingInput');
|
|
1184
|
+
if (!input) return;
|
|
1185
|
+
|
|
1186
|
+
// Replace the @partial with the full alias
|
|
1187
|
+
const before = input.value.slice(0, input.selectionStart);
|
|
1188
|
+
const after = input.value.slice(input.selectionStart);
|
|
1189
|
+
const atIdx = before.lastIndexOf('@');
|
|
1190
|
+
input.value = before.slice(0, atIdx) + alias + ' ' + after;
|
|
1191
|
+
input.focus();
|
|
1192
|
+
const newPos = atIdx + alias.length + 1;
|
|
1193
|
+
input.setSelectionRange(newPos, newPos);
|
|
1194
|
+
hideAutocomplete();
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
function handleChatKeydown(event) {
|
|
1198
|
+
const ac = document.getElementById('autocomplete');
|
|
1199
|
+
if (ac?.classList.contains('visible')) {
|
|
1200
|
+
if (event.key === 'ArrowDown') {
|
|
1201
|
+
event.preventDefault();
|
|
1202
|
+
acSelectedIdx = Math.min(acSelectedIdx + 1, acFiltered.length - 1);
|
|
1203
|
+
renderAutocomplete();
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
if (event.key === 'ArrowUp') {
|
|
1207
|
+
event.preventDefault();
|
|
1208
|
+
acSelectedIdx = Math.max(acSelectedIdx - 1, 0);
|
|
1209
|
+
renderAutocomplete();
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
if (event.key === 'Tab' || event.key === 'Enter') {
|
|
1213
|
+
if (acSelectedIdx >= 0) {
|
|
1214
|
+
event.preventDefault();
|
|
1215
|
+
selectAutocomplete(acSelectedIdx);
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
if (event.key === 'Escape') {
|
|
1220
|
+
hideAutocomplete();
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
if (event.key === 'Enter' && !event.shiftKey) {
|
|
1225
|
+
event.preventDefault();
|
|
1226
|
+
sendMessage();
|
|
1227
|
+
}
|
|
1228
|
+
if (event.key === 'Escape' && isThinking) {
|
|
1229
|
+
stopHomeStreaming();
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// ─── Execute via hub agent (streaming) ─────────────────
|
|
1234
|
+
async function executeHub(text, tabId) {
|
|
1235
|
+
const tid = tabId || homeActiveTabId;
|
|
1236
|
+
const ts = getTabState(tid);
|
|
1237
|
+
const isActive = () => tid === homeActiveTabId;
|
|
1238
|
+
|
|
1239
|
+
// Safety: save any leftover streaming text from a prior run
|
|
1240
|
+
if (ts.text) {
|
|
1241
|
+
addMessage('agent', ts.text, tid);
|
|
1242
|
+
ts.text = '';
|
|
1243
|
+
if (isActive()) streamingText = '';
|
|
1244
|
+
}
|
|
1245
|
+
ts.isThinking = true;
|
|
1246
|
+
if (isActive()) isThinking = true;
|
|
1247
|
+
renderMessages();
|
|
1248
|
+
scrollChat();
|
|
1249
|
+
renderHomeTabBar();
|
|
1250
|
+
|
|
1251
|
+
try {
|
|
1252
|
+
const startRes = await fetch(`/api/chat/${hubAgentId}/stream`, {
|
|
1253
|
+
method: 'POST',
|
|
1254
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1255
|
+
body: JSON.stringify({ text, accountOverride }),
|
|
1256
|
+
});
|
|
1257
|
+
const { jobId } = await startRes.json();
|
|
1258
|
+
if (!jobId) throw new Error('No jobId');
|
|
1259
|
+
|
|
1260
|
+
// Track job for stop + raw logs
|
|
1261
|
+
ts.jobId = jobId;
|
|
1262
|
+
if (isActive()) { currentJobId = jobId; }
|
|
1263
|
+
currentRawJobId = jobId;
|
|
1264
|
+
updateHomeInputButtons();
|
|
1265
|
+
if (logsOpen) streamRawLogs(jobId);
|
|
1266
|
+
|
|
1267
|
+
// Stream events using fetch+reader with reconnect (no EventSource)
|
|
1268
|
+
ts.text = '';
|
|
1269
|
+
if (isActive()) streamingText = '';
|
|
1270
|
+
let lastEventId = 0;
|
|
1271
|
+
let isDone = false;
|
|
1272
|
+
let retries = 0;
|
|
1273
|
+
const MAX_RETRIES = 10;
|
|
1274
|
+
|
|
1275
|
+
while (!isDone && retries < MAX_RETRIES) {
|
|
1276
|
+
try {
|
|
1277
|
+
const streamRes = await fetch(`/api/chat/jobs/${jobId}/stream?after=${lastEventId}`);
|
|
1278
|
+
if (!streamRes.ok) {
|
|
1279
|
+
if (streamRes.status === 404) {
|
|
1280
|
+
try {
|
|
1281
|
+
await fetch(`/api/agents/${hubAgentId}/recover`, {
|
|
1282
|
+
method: 'POST',
|
|
1283
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1284
|
+
body: JSON.stringify({ userText: text, response: ts.text }),
|
|
1285
|
+
});
|
|
1286
|
+
} catch { /* best effort */ }
|
|
1287
|
+
if (ts.text) { addMessage('agent', ts.text, tid); ts.text = ''; if (isActive()) streamingText = ''; }
|
|
1288
|
+
isDone = true;
|
|
1289
|
+
break;
|
|
1290
|
+
}
|
|
1291
|
+
retries++;
|
|
1292
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
1293
|
+
continue;
|
|
1294
|
+
}
|
|
1295
|
+
const reader = streamRes.body.getReader();
|
|
1296
|
+
const decoder = new TextDecoder();
|
|
1297
|
+
let buffer = '';
|
|
1298
|
+
retries = 0;
|
|
1299
|
+
|
|
1300
|
+
while (true) {
|
|
1301
|
+
const { done, value } = await reader.read();
|
|
1302
|
+
if (done) break;
|
|
1303
|
+
|
|
1304
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1305
|
+
const lines = buffer.split('\n');
|
|
1306
|
+
buffer = lines.pop() || '';
|
|
1307
|
+
|
|
1308
|
+
for (const line of lines) {
|
|
1309
|
+
if (line.startsWith('id: ')) {
|
|
1310
|
+
lastEventId = parseInt(line.slice(4)) + 1;
|
|
1311
|
+
continue;
|
|
1312
|
+
}
|
|
1313
|
+
if (!line.startsWith('data: ')) continue;
|
|
1314
|
+
const data = line.slice(6);
|
|
1315
|
+
if (data === '[DONE]') { isDone = true; continue; }
|
|
1316
|
+
|
|
1317
|
+
try {
|
|
1318
|
+
const event = JSON.parse(data);
|
|
1319
|
+
if (event.type === 'text') {
|
|
1320
|
+
ts.text += event.data;
|
|
1321
|
+
ts.isThinking = false;
|
|
1322
|
+
if (isActive()) { streamingText = ts.text; isThinking = false; }
|
|
1323
|
+
renderMessages();
|
|
1324
|
+
scrollChat();
|
|
1325
|
+
} else if (event.type === 'status' || event.type === 'tool') {
|
|
1326
|
+
if (event.type === 'tool' && event.tool && event.tool.input && Object.keys(event.tool.input).length > 0) {
|
|
1327
|
+
trackFileFromTool(event.tool);
|
|
1328
|
+
}
|
|
1329
|
+
renderMessages();
|
|
1330
|
+
} else if (event.type === 'error') {
|
|
1331
|
+
ts.isThinking = false;
|
|
1332
|
+
if (isActive()) isThinking = false;
|
|
1333
|
+
addMessage('agent', 'Error: ' + event.data, tid);
|
|
1334
|
+
ts.text = '';
|
|
1335
|
+
if (isActive()) streamingText = '';
|
|
1336
|
+
renderMessages();
|
|
1337
|
+
isDone = true;
|
|
1338
|
+
}
|
|
1339
|
+
} catch { /* skip */ }
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
if (!isDone) {
|
|
1344
|
+
retries++;
|
|
1345
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
1346
|
+
}
|
|
1347
|
+
} catch {
|
|
1348
|
+
retries++;
|
|
1349
|
+
if (retries < MAX_RETRIES) {
|
|
1350
|
+
await new Promise(r => setTimeout(r, 1000 * retries));
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
// Finalize
|
|
1356
|
+
ts.isThinking = false;
|
|
1357
|
+
ts.jobId = null;
|
|
1358
|
+
if (isActive()) { isThinking = false; currentJobId = null; }
|
|
1359
|
+
updateHomeInputButtons();
|
|
1360
|
+
renderHomeTabBar();
|
|
1361
|
+
if (ts.text) {
|
|
1362
|
+
speakText(ts.text);
|
|
1363
|
+
addMessage('agent', ts.text, tid);
|
|
1364
|
+
ts.text = '';
|
|
1365
|
+
if (isActive()) streamingText = '';
|
|
1366
|
+
} else if (retries >= MAX_RETRIES) {
|
|
1367
|
+
addMessage('agent', 'Connection lost after multiple retries.', tid);
|
|
1368
|
+
}
|
|
1369
|
+
} catch (err) {
|
|
1370
|
+
ts.isThinking = false;
|
|
1371
|
+
ts.jobId = null;
|
|
1372
|
+
if (isActive()) { isThinking = false; currentJobId = null; }
|
|
1373
|
+
updateHomeInputButtons();
|
|
1374
|
+
renderHomeTabBar();
|
|
1375
|
+
if (ts.text) {
|
|
1376
|
+
addMessage('agent', ts.text, tid);
|
|
1377
|
+
ts.text = '';
|
|
1378
|
+
if (isActive()) streamingText = '';
|
|
1379
|
+
} else {
|
|
1380
|
+
addMessage('agent', 'Connection error: ' + err.message, tid);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
renderMessages();
|
|
1384
|
+
scrollChat();
|
|
1385
|
+
// Drain queue: auto-send next queued message
|
|
1386
|
+
if (ts.queue.length > 0) {
|
|
1387
|
+
const next = ts.queue.shift();
|
|
1388
|
+
if (isActive()) homeQueue = ts.queue;
|
|
1389
|
+
renderHomeQueue();
|
|
1390
|
+
sendMessage(next);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// ─── Messages ──────────────────────────────────────────
|
|
1395
|
+
function addMessage(role, text, tabId) {
|
|
1396
|
+
const tid = tabId || homeActiveTabId;
|
|
1397
|
+
if (!tabMessages[tid]) tabMessages[tid] = [];
|
|
1398
|
+
tabMessages[tid].push({ role, text, time: new Date().toISOString() });
|
|
1399
|
+
if (tid === homeActiveTabId) {
|
|
1400
|
+
messages = tabMessages[tid];
|
|
1401
|
+
}
|
|
1402
|
+
renderMessages();
|
|
1403
|
+
scrollChat();
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
function renderMessages() {
|
|
1407
|
+
const el = document.getElementById('chatMessages');
|
|
1408
|
+
let html = messages.map(m => {
|
|
1409
|
+
const cls = m.role === 'user' ? 'msg-user' : 'msg-agent';
|
|
1410
|
+
const formatted = m.role === 'agent' ? formatMarkdown(m.text) : escapeHtml(m.text);
|
|
1411
|
+
const time = formatTime(m.time);
|
|
1412
|
+
// Parse via attribution: [via:agentId] at the end
|
|
1413
|
+
let viaBadge = '';
|
|
1414
|
+
const viaMatch = m.text?.match(/\[via:(\w[\w-]*)\]/);
|
|
1415
|
+
if (viaMatch && m.role === 'agent') {
|
|
1416
|
+
const viaAgent = agents.find(a => a.id === viaMatch[1]);
|
|
1417
|
+
viaBadge = `<span class="via-badge">via ${viaAgent?.aliases?.[0] || '@'+viaMatch[1]}</span>`;
|
|
1418
|
+
}
|
|
1419
|
+
return `<div class="msg ${cls}">${formatted}<div class="msg-time">${viaBadge}${time}</div></div>`;
|
|
1420
|
+
}).join('');
|
|
1421
|
+
|
|
1422
|
+
// Streaming message
|
|
1423
|
+
if (streamingText) {
|
|
1424
|
+
const formatted = formatMarkdown(streamingText);
|
|
1425
|
+
let viaBadge = '';
|
|
1426
|
+
const viaMatch = streamingText.match(/\[via:(\w[\w-]*)\]/);
|
|
1427
|
+
if (viaMatch) {
|
|
1428
|
+
const viaAgent = agents.find(a => a.id === viaMatch[1]);
|
|
1429
|
+
viaBadge = `<span class="via-badge">via ${viaAgent?.aliases?.[0] || '@'+viaMatch[1]}</span>`;
|
|
1430
|
+
}
|
|
1431
|
+
html += `<div class="msg msg-agent">${formatted}<div class="msg-time">${viaBadge}streaming...</div></div>`;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// Thinking indicator
|
|
1435
|
+
if (isThinking && !streamingText) {
|
|
1436
|
+
html += `<div class="thinking"><div class="thinking-dots"><span></span><span></span><span></span></div>Thinking...</div>`;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
el.innerHTML = html;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
function scrollChat() {
|
|
1443
|
+
setTimeout(() => {
|
|
1444
|
+
const el = document.getElementById('chatMessages');
|
|
1445
|
+
if (el) el.scrollTop = el.scrollHeight;
|
|
1446
|
+
}, 60);
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// ─── Formatting ────────────────────────────────────────
|
|
1450
|
+
function escapeHtml(s) {
|
|
1451
|
+
return (s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
function formatMarkdown(text) {
|
|
1455
|
+
if (!text) return '';
|
|
1456
|
+
// Remove [via:xxx] markers from display text
|
|
1457
|
+
let html = escapeHtml(text.replace(/\[via:\w[\w-]*\]/g, '').trim());
|
|
1458
|
+
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>');
|
|
1459
|
+
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
1460
|
+
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
|
1461
|
+
// Detect file paths and add download/preview buttons (via Canvas module)
|
|
1462
|
+
html = Canvas.injectFileButtons(html, escapeHtml);
|
|
1463
|
+
html = html.replace(/\n/g, '<br>');
|
|
1464
|
+
return html;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
function formatTime(ts) {
|
|
1468
|
+
if (!ts) return '';
|
|
1469
|
+
try {
|
|
1470
|
+
const d = new Date(ts);
|
|
1471
|
+
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
1472
|
+
} catch { return ''; }
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
function formatRelativeTime(ts) {
|
|
1476
|
+
if (!ts) return '';
|
|
1477
|
+
try {
|
|
1478
|
+
const d = new Date(ts);
|
|
1479
|
+
const diff = Date.now() - d;
|
|
1480
|
+
const mins = Math.floor(diff / 60000);
|
|
1481
|
+
if (mins < 1) return 'now';
|
|
1482
|
+
if (mins < 60) return mins + 'm';
|
|
1483
|
+
const hours = Math.floor(mins / 60);
|
|
1484
|
+
if (hours < 24) return hours + 'h';
|
|
1485
|
+
const days = Math.floor(hours / 24);
|
|
1486
|
+
if (days < 7) return days + 'd';
|
|
1487
|
+
return d.toLocaleDateString([], { month: 'short', day: 'numeric' });
|
|
1488
|
+
} catch { return ''; }
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
function autoResize(el) {
|
|
1492
|
+
el.style.height = 'auto';
|
|
1493
|
+
el.style.height = Math.min(el.scrollHeight, 120) + 'px';
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// ─── Account Override ────────────────────────────────────
|
|
1497
|
+
function setAccountOverride(val) {
|
|
1498
|
+
accountOverride = val || undefined;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
// ─── Reset Session ──────────────────────────────────────
|
|
1502
|
+
async function confirmResetSession() {
|
|
1503
|
+
if (!hubAgentId) return;
|
|
1504
|
+
if (!confirm('Reset hub session? This clears conversation history. Use /opcompact first to save important context.')) return;
|
|
1505
|
+
try {
|
|
1506
|
+
await fetch(`/api/agents/${hubAgentId}/session`, { method: 'DELETE' });
|
|
1507
|
+
messages = [];
|
|
1508
|
+
tabMessages[homeActiveTabId] = [];
|
|
1509
|
+
const ts = getTabState(homeActiveTabId);
|
|
1510
|
+
ts.text = '';
|
|
1511
|
+
ts.isThinking = false;
|
|
1512
|
+
ts.jobId = null;
|
|
1513
|
+
ts.queue = [];
|
|
1514
|
+
streamingText = '';
|
|
1515
|
+
isThinking = false;
|
|
1516
|
+
currentJobId = null;
|
|
1517
|
+
homeQueue = [];
|
|
1518
|
+
renderMessages();
|
|
1519
|
+
updateHomeInputButtons();
|
|
1520
|
+
} catch (err) {
|
|
1521
|
+
alert('Failed to reset session: ' + err.message);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// ─── Close docs dropdown on outside click ──────────────
|
|
1526
|
+
document.addEventListener('click', (e) => {
|
|
1527
|
+
document.querySelectorAll('.docs-dropdown').forEach(dd => {
|
|
1528
|
+
if (!dd.contains(e.target)) dd.querySelector('.docs-menu')?.classList.remove('open');
|
|
1529
|
+
});
|
|
1530
|
+
});
|
|
1531
|
+
|
|
1532
|
+
// ─── Init ──────────────────────────────────────────────
|
|
1533
|
+
// ─── Theme Toggle ──────────────────────────────────────
|
|
1534
|
+
function toggleTheme() {
|
|
1535
|
+
const html = document.documentElement;
|
|
1536
|
+
const isLight = html.getAttribute('data-theme') === 'light';
|
|
1537
|
+
html.setAttribute('data-theme', isLight ? '' : 'light');
|
|
1538
|
+
const icon = isLight ? '🌙' : '☀️';
|
|
1539
|
+
const btn1 = document.getElementById('themeBtn');
|
|
1540
|
+
if (btn1) btn1.textContent = icon;
|
|
1541
|
+
localStorage.setItem('theme', isLight ? 'dark' : 'light');
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
// Load saved theme
|
|
1545
|
+
(function() {
|
|
1546
|
+
const saved = localStorage.getItem('theme');
|
|
1547
|
+
if (saved === 'light') {
|
|
1548
|
+
document.documentElement.setAttribute('data-theme', 'light');
|
|
1549
|
+
setTimeout(() => {
|
|
1550
|
+
const btn1 = document.getElementById('themeBtn');
|
|
1551
|
+
if (btn1) btn1.textContent = '☀️';
|
|
1552
|
+
if (btn2) btn2.textContent = '☀️';
|
|
1553
|
+
}, 50);
|
|
1554
|
+
}
|
|
1555
|
+
})();
|
|
1556
|
+
|
|
1557
|
+
// ─── Drawer ────────────────────────────────────────────
|
|
1558
|
+
function toggleDrawer() {
|
|
1559
|
+
const overlay = document.getElementById('drawerOverlay');
|
|
1560
|
+
overlay.classList.toggle('open');
|
|
1561
|
+
if (overlay.classList.contains('open')) renderDrawer();
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
function renderDrawer() {
|
|
1565
|
+
const list = document.getElementById('drawerList');
|
|
1566
|
+
const sorted = agents
|
|
1567
|
+
.filter(a => !a.subAgents)
|
|
1568
|
+
.sort((a, b) => {
|
|
1569
|
+
const aT = a.lastMessage && a.lastMessage !== 'never' ? new Date(a.lastMessage).getTime() : 0;
|
|
1570
|
+
const bT = b.lastMessage && b.lastMessage !== 'never' ? new Date(b.lastMessage).getTime() : 0;
|
|
1571
|
+
return bT - aT;
|
|
1572
|
+
});
|
|
1573
|
+
|
|
1574
|
+
list.innerHTML = sorted.map(a => {
|
|
1575
|
+
const initials = (a.aliases?.[0] || a.id).replace('@','').slice(0,2).toUpperCase();
|
|
1576
|
+
const alias = a.aliases?.[0] || ('@' + a.id);
|
|
1577
|
+
const time = a.lastMessage && a.lastMessage !== 'never' ? formatRelativeTime(a.lastMessage) : '';
|
|
1578
|
+
return `<div class="drawer-agent" onclick="toggleDrawer();mentionAgent('${alias}')">
|
|
1579
|
+
<div class="drawer-agent-avatar">${initials}</div>
|
|
1580
|
+
<div class="drawer-agent-info">
|
|
1581
|
+
<div class="drawer-agent-name">${a.name}</div>
|
|
1582
|
+
<div class="drawer-agent-alias">${alias}</div>
|
|
1583
|
+
</div>
|
|
1584
|
+
<span class="drawer-agent-time">${time}</span>
|
|
1585
|
+
</div>`;
|
|
1586
|
+
}).join('');
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
// ─── File Attachment ───────────────────────────────────
|
|
1590
|
+
let homeFiles = [];
|
|
1591
|
+
|
|
1592
|
+
function toggleHomeFileZone() {
|
|
1593
|
+
const zone = document.getElementById('homeFileDropZone');
|
|
1594
|
+
if (!zone) return;
|
|
1595
|
+
zone.classList.toggle('visible');
|
|
1596
|
+
const btn = document.getElementById('homeClipBtn');
|
|
1597
|
+
if (btn) btn.classList.toggle('has-files', zone.classList.contains('visible') || homeFiles.length > 0);
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
function handleHomeFileDrop(e) {
|
|
1601
|
+
const files = e.dataTransfer?.files;
|
|
1602
|
+
if (files) { for (const f of files) homeFiles.push(f); renderHomeFiles(); }
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
function handleHomeFiles(e) {
|
|
1606
|
+
const files = e.target.files;
|
|
1607
|
+
if (!files) return;
|
|
1608
|
+
for (const f of files) homeFiles.push(f);
|
|
1609
|
+
renderHomeFiles();
|
|
1610
|
+
e.target.value = '';
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
function renderHomeFiles() {
|
|
1614
|
+
const area = document.getElementById('homeFileListArea');
|
|
1615
|
+
const btn = document.getElementById('homeClipBtn');
|
|
1616
|
+
if (!area) return;
|
|
1617
|
+
if (!homeFiles.length) {
|
|
1618
|
+
area.innerHTML = '<span>Drop files here or click to browse</span>';
|
|
1619
|
+
if (btn) btn.classList.remove('has-files');
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
if (btn) btn.classList.add('has-files');
|
|
1623
|
+
area.innerHTML = '<div class="home-file-list">' + homeFiles.map((f, i) => {
|
|
1624
|
+
const size = f.size > 1024*1024 ? (f.size/(1024*1024)).toFixed(1)+'MB' : (f.size/1024).toFixed(0)+'KB';
|
|
1625
|
+
return `<div class="home-file-pill" onclick="event.stopPropagation()">
|
|
1626
|
+
<span class="home-file-pill-name">${escapeHtml(f.name)}</span>
|
|
1627
|
+
<span class="home-file-pill-size">${size}</span>
|
|
1628
|
+
<span class="home-file-pill-remove" onclick="homeFiles.splice(${i},1);renderHomeFiles()">×</span>
|
|
1629
|
+
</div>`;
|
|
1630
|
+
}).join('') + '</div>';
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
// ─── Voice Input ───────────────────────────────────────
|
|
1634
|
+
let homeRecognition = null;
|
|
1635
|
+
let homeIsRecording = false;
|
|
1636
|
+
|
|
1637
|
+
function toggleHomeVoice() {
|
|
1638
|
+
if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
|
|
1639
|
+
alert('Speech recognition not supported. Use Chrome or Edge.');
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
homeIsRecording ? stopHomeVoice() : startHomeVoice();
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
function startHomeVoice() {
|
|
1646
|
+
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
1647
|
+
homeRecognition = new SpeechRecognition();
|
|
1648
|
+
homeRecognition.continuous = true;
|
|
1649
|
+
homeRecognition.interimResults = true;
|
|
1650
|
+
homeRecognition.lang = 'en-US';
|
|
1651
|
+
|
|
1652
|
+
let finalTranscript = '';
|
|
1653
|
+
const input = document.getElementById('chatInput') || document.getElementById('landingInput');
|
|
1654
|
+
const existing = input ? input.value : '';
|
|
1655
|
+
|
|
1656
|
+
homeRecognition.onresult = (event) => {
|
|
1657
|
+
let interim = '';
|
|
1658
|
+
for (let i = event.resultIndex; i < event.results.length; i++) {
|
|
1659
|
+
if (event.results[i].isFinal) finalTranscript += event.results[i][0].transcript + ' ';
|
|
1660
|
+
else interim += event.results[i][0].transcript;
|
|
1661
|
+
}
|
|
1662
|
+
if (input) { input.value = existing + (existing ? ' ' : '') + finalTranscript + interim; autoResize(input); }
|
|
1663
|
+
};
|
|
1664
|
+
|
|
1665
|
+
homeRecognition.onerror = () => stopHomeVoice();
|
|
1666
|
+
homeRecognition.onend = () => { if (homeIsRecording) stopHomeVoice(); };
|
|
1667
|
+
|
|
1668
|
+
homeRecognition.start();
|
|
1669
|
+
homeIsRecording = true;
|
|
1670
|
+
const btn = document.getElementById('homeMicBtn');
|
|
1671
|
+
if (btn) btn.classList.add('recording');
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
function stopHomeVoice() {
|
|
1675
|
+
if (homeRecognition) { homeRecognition.stop(); homeRecognition = null; }
|
|
1676
|
+
homeIsRecording = false;
|
|
1677
|
+
const btn = document.getElementById('homeMicBtn');
|
|
1678
|
+
if (btn) btn.classList.remove('recording');
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// ─── Stop & Queue ─────────────────────────────────────
|
|
1682
|
+
async function stopHomeStreaming() {
|
|
1683
|
+
const ts = getTabState(homeActiveTabId);
|
|
1684
|
+
if (ts.jobId) {
|
|
1685
|
+
try { await fetch(`/api/chat/jobs/${ts.jobId}/stop`, { method: 'POST' }); } catch {}
|
|
1686
|
+
}
|
|
1687
|
+
ts.isThinking = false;
|
|
1688
|
+
ts.jobId = null;
|
|
1689
|
+
isThinking = false;
|
|
1690
|
+
currentJobId = null;
|
|
1691
|
+
if (ts.text) {
|
|
1692
|
+
addMessage('agent', ts.text);
|
|
1693
|
+
ts.text = '';
|
|
1694
|
+
streamingText = '';
|
|
1695
|
+
}
|
|
1696
|
+
renderMessages();
|
|
1697
|
+
scrollChat();
|
|
1698
|
+
updateHomeInputButtons();
|
|
1699
|
+
renderHomeTabBar();
|
|
1700
|
+
// Drain queue
|
|
1701
|
+
if (ts.queue.length > 0) {
|
|
1702
|
+
const next = ts.queue.shift();
|
|
1703
|
+
homeQueue = ts.queue;
|
|
1704
|
+
renderHomeQueue();
|
|
1705
|
+
sendMessage(next);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
function updateHomeInputButtons() {
|
|
1710
|
+
const box = document.querySelector('.chat-input-home-box');
|
|
1711
|
+
if (!box) return;
|
|
1712
|
+
const sendBtn = document.getElementById('homeSendBtn');
|
|
1713
|
+
const existingStop = box.querySelector('.home-stop-btn');
|
|
1714
|
+
const ts = getTabState(homeActiveTabId);
|
|
1715
|
+
|
|
1716
|
+
if (ts.isThinking || ts.jobId) {
|
|
1717
|
+
// Show stop button
|
|
1718
|
+
if (!existingStop) {
|
|
1719
|
+
const stopBtn = document.createElement('button');
|
|
1720
|
+
stopBtn.className = 'home-stop-btn';
|
|
1721
|
+
stopBtn.onclick = stopHomeStreaming;
|
|
1722
|
+
stopBtn.title = 'Stop (Esc)';
|
|
1723
|
+
stopBtn.innerHTML = '\u25A0';
|
|
1724
|
+
if (sendBtn) box.insertBefore(stopBtn, sendBtn);
|
|
1725
|
+
}
|
|
1726
|
+
// Switch send to queue mode
|
|
1727
|
+
if (sendBtn) { sendBtn.classList.add('queue-mode'); sendBtn.textContent = 'Queue'; }
|
|
1728
|
+
} else {
|
|
1729
|
+
// Remove stop button
|
|
1730
|
+
if (existingStop) existingStop.remove();
|
|
1731
|
+
// Restore send button
|
|
1732
|
+
if (sendBtn) { sendBtn.classList.remove('queue-mode'); sendBtn.textContent = '\u2192'; }
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
function renderHomeQueue() {
|
|
1737
|
+
const container = document.getElementById('homeQueueContainer');
|
|
1738
|
+
if (!container) return;
|
|
1739
|
+
if (!homeQueue.length) { container.innerHTML = ''; return; }
|
|
1740
|
+
container.innerHTML = `<div class="home-queue">${homeQueue.map((msg, i) => {
|
|
1741
|
+
const truncated = msg.length > 60 ? msg.slice(0, 60) + '...' : msg;
|
|
1742
|
+
return `<div class="home-queue-item">
|
|
1743
|
+
<span class="q-pos">${i + 1}</span>
|
|
1744
|
+
<span class="q-text" title="${msg.replace(/"/g, '"')}">${truncated.replace(/</g, '<')}</span>
|
|
1745
|
+
<button class="q-btn" onclick="editHomeQueueItem(${i})" title="Edit">✎</button>
|
|
1746
|
+
<button class="q-btn danger" onclick="deleteHomeQueueItem(${i})" title="Delete">✕</button>
|
|
1747
|
+
</div>`;
|
|
1748
|
+
}).join('')}</div>`;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
function editHomeQueueItem(idx) {
|
|
1752
|
+
if (idx < 0 || idx >= homeQueue.length) return;
|
|
1753
|
+
const text = homeQueue.splice(idx, 1)[0];
|
|
1754
|
+
const input = document.getElementById('chatInput');
|
|
1755
|
+
if (input) { input.value = text; input.focus(); autoResize(input); }
|
|
1756
|
+
renderHomeQueue();
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
function deleteHomeQueueItem(idx) {
|
|
1760
|
+
if (idx < 0 || idx >= homeQueue.length) return;
|
|
1761
|
+
homeQueue.splice(idx, 1);
|
|
1762
|
+
renderHomeQueue();
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
// ─── Raw Logs Drawer (claude -p stdout) ───────────────
|
|
1766
|
+
let logsOpen = false;
|
|
1767
|
+
let rawLogsReader = null;
|
|
1768
|
+
let currentRawJobId = null;
|
|
1769
|
+
|
|
1770
|
+
function toggleLogs() {
|
|
1771
|
+
const drawer = document.getElementById('logsDrawer');
|
|
1772
|
+
logsOpen = !logsOpen;
|
|
1773
|
+
drawer.classList.toggle('open', logsOpen);
|
|
1774
|
+
if (logsOpen) connectRawLogs();
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
function clearLogs() {
|
|
1778
|
+
document.getElementById('logsContent').textContent = '';
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
function formatLogLine(data) {
|
|
1782
|
+
if (data.startsWith('[stderr]')) return { icon: '\u26A0', text: data.slice(9), cls: 'log-stderr' };
|
|
1783
|
+
try {
|
|
1784
|
+
let evt = JSON.parse(data);
|
|
1785
|
+
if (evt.type === 'stream_event' && evt.event) evt = evt.event;
|
|
1786
|
+
if (evt.type === 'content_block_delta' && evt.delta?.type === 'text_delta')
|
|
1787
|
+
return { icon: '\uD83D\uDCAC', text: evt.delta.text };
|
|
1788
|
+
if (evt.type === 'content_block_delta' && evt.delta?.type === 'thinking_delta')
|
|
1789
|
+
return { icon: '\uD83E\uDDE0', text: evt.delta.thinking };
|
|
1790
|
+
if (evt.type === 'content_block_start' && evt.content_block?.type === 'tool_use')
|
|
1791
|
+
return { icon: '\uD83D\uDD27', text: evt.content_block.name };
|
|
1792
|
+
if (evt.type === 'assistant' && evt.message?.content) {
|
|
1793
|
+
const tools = evt.message.content.filter(b => b.type === 'tool_use');
|
|
1794
|
+
if (tools.length) {
|
|
1795
|
+
return { icon: '\uD83D\uDCD6', text: tools.map(t => {
|
|
1796
|
+
const path = t.input?.file_path || t.input?.command?.slice(0, 80) || t.input?.pattern || '';
|
|
1797
|
+
return t.name + (path ? ' ' + path : '');
|
|
1798
|
+
}).join(', ') };
|
|
1799
|
+
}
|
|
1800
|
+
const texts = evt.message.content.filter(b => b.type === 'text' && b.text);
|
|
1801
|
+
if (texts.length) return { icon: '\uD83D\uDCAC', text: texts.map(t => t.text).join(' ') };
|
|
1802
|
+
}
|
|
1803
|
+
if (evt.type === 'user' && evt.message?.content?.[0]?.type === 'tool_result')
|
|
1804
|
+
return { icon: '\u2713', text: 'tool_result', cls: 'log-dim' };
|
|
1805
|
+
if (evt.type === 'content_block_delta' && evt.delta?.type === 'input_json_delta') return null;
|
|
1806
|
+
if (evt.type === 'content_block_stop' || evt.type === 'message_stop' || evt.type === 'message_delta') return null;
|
|
1807
|
+
if (evt.type === 'rate_limit_event') return null;
|
|
1808
|
+
if (evt.type === 'content_block_delta' && evt.delta?.type === 'signature_delta') return null;
|
|
1809
|
+
if (evt.type === 'system' && evt.subtype === 'init')
|
|
1810
|
+
return { icon: '\u25CF', text: 'Session started \u2014 ' + (evt.model || '') + ' | ' + (evt.mcp_servers?.length || 0) + ' MCPs' };
|
|
1811
|
+
if (evt.type === 'system') return { icon: '\u2699', text: evt.subtype || 'system' };
|
|
1812
|
+
if (evt.type === 'result')
|
|
1813
|
+
return { icon: '\uD83C\uDFC1', text: 'Done \u2014 ' + (evt.duration_ms ? Math.round(evt.duration_ms/1000) + 's' : '') };
|
|
1814
|
+
if (evt.type === 'message_start') return null;
|
|
1815
|
+
return { icon: '\u00B7', text: evt.type || data.slice(0, 80), cls: 'log-dim' };
|
|
1816
|
+
} catch {
|
|
1817
|
+
return { icon: '\u00B7', text: data.slice(0, 120) };
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
function connectRawLogs() {
|
|
1822
|
+
if (!currentRawJobId) {
|
|
1823
|
+
document.getElementById('logsContent').textContent = 'No active job. Send a message to start streaming logs.\n';
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1826
|
+
streamRawLogs(currentRawJobId);
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
async function streamRawLogs(jobId) {
|
|
1830
|
+
const el = document.getElementById('logsContent');
|
|
1831
|
+
if (!el) return;
|
|
1832
|
+
el.textContent = '';
|
|
1833
|
+
|
|
1834
|
+
try {
|
|
1835
|
+
if (rawLogsReader) { try { rawLogsReader.cancel(); } catch {} }
|
|
1836
|
+
const res = await fetch(`/api/chat/jobs/${jobId}/raw?after=0`);
|
|
1837
|
+
const reader = res.body.getReader();
|
|
1838
|
+
rawLogsReader = reader;
|
|
1839
|
+
const decoder = new TextDecoder();
|
|
1840
|
+
let buffer = '';
|
|
1841
|
+
|
|
1842
|
+
while (true) {
|
|
1843
|
+
const { done, value } = await reader.read();
|
|
1844
|
+
if (done) break;
|
|
1845
|
+
|
|
1846
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1847
|
+
const lines = buffer.split('\n');
|
|
1848
|
+
buffer = lines.pop() || '';
|
|
1849
|
+
|
|
1850
|
+
for (const line of lines) {
|
|
1851
|
+
if (!line.startsWith('data: ')) continue;
|
|
1852
|
+
const data = line.slice(6);
|
|
1853
|
+
if (data === '[DONE]') {
|
|
1854
|
+
el.textContent += '\n--- Job complete ---\n';
|
|
1855
|
+
break;
|
|
1856
|
+
}
|
|
1857
|
+
const fmt = formatLogLine(data);
|
|
1858
|
+
if (fmt) {
|
|
1859
|
+
const span = document.createElement('span');
|
|
1860
|
+
span.className = 'log-line' + (fmt.cls ? ' ' + fmt.cls : '');
|
|
1861
|
+
span.textContent = fmt.icon + ' ' + fmt.text + '\n';
|
|
1862
|
+
el.appendChild(span);
|
|
1863
|
+
}
|
|
1864
|
+
// Auto-scroll
|
|
1865
|
+
const drawer = document.getElementById('logsDrawer');
|
|
1866
|
+
if (drawer) drawer.scrollTop = drawer.scrollHeight;
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
} catch { /* connection closed */ }
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
// ─── Config Overlay (iframe) ─────────────────────────────
|
|
1873
|
+
function openConfigOverlay() {
|
|
1874
|
+
if (!hubAgentId) return;
|
|
1875
|
+
const overlay = document.getElementById('configOverlay');
|
|
1876
|
+
const iframe = document.getElementById('configIframe');
|
|
1877
|
+
iframe.src = `/org?edit=${encodeURIComponent(hubAgentId)}`;
|
|
1878
|
+
overlay.classList.add('show');
|
|
1879
|
+
}
|
|
1880
|
+
function closeConfigOverlay() {
|
|
1881
|
+
const overlay = document.getElementById('configOverlay');
|
|
1882
|
+
const iframe = document.getElementById('configIframe');
|
|
1883
|
+
overlay.classList.remove('show');
|
|
1884
|
+
setTimeout(() => { iframe.src = 'about:blank'; }, 300);
|
|
1885
|
+
}
|
|
1886
|
+
// Listen for modal close from iframe
|
|
1887
|
+
window.addEventListener('message', (e) => {
|
|
1888
|
+
if (e.data?.type === 'configModalClosed') closeConfigOverlay();
|
|
1889
|
+
});
|
|
1890
|
+
|
|
1891
|
+
init();
|
|
1892
|
+
setTimeout(() => document.getElementById('landingInput')?.focus(), 100);
|
|
1893
|
+
</script>
|
|
1894
|
+
|
|
1895
|
+
<!-- Config Overlay -->
|
|
1896
|
+
<div class="config-overlay" id="configOverlay" onclick="closeConfigOverlay()">
|
|
1897
|
+
<div class="config-overlay-inner" onclick="event.stopPropagation()">
|
|
1898
|
+
<button class="config-overlay-close" onclick="closeConfigOverlay()">✕</button>
|
|
1899
|
+
<iframe id="configIframe" src="about:blank"></iframe>
|
|
1900
|
+
</div>
|
|
1901
|
+
</div>
|
|
1902
|
+
<style>
|
|
1903
|
+
.config-overlay{
|
|
1904
|
+
position:fixed;top:0;left:0;right:0;bottom:0;z-index:9999;
|
|
1905
|
+
background:rgba(0,0,0,0.7);backdrop-filter:blur(4px);
|
|
1906
|
+
display:none;align-items:center;justify-content:center;
|
|
1907
|
+
opacity:0;transition:opacity .25s ease;
|
|
1908
|
+
}
|
|
1909
|
+
.config-overlay.show{display:flex;opacity:1}
|
|
1910
|
+
.config-overlay-inner{
|
|
1911
|
+
position:relative;width:90vw;max-width:680px;height:85vh;
|
|
1912
|
+
border-radius:16px;overflow:hidden;
|
|
1913
|
+
border:1px solid var(--border-glow);
|
|
1914
|
+
box-shadow:0 8px 40px rgba(0,0,0,0.5);
|
|
1915
|
+
}
|
|
1916
|
+
.config-overlay-inner iframe{
|
|
1917
|
+
width:100%;height:100%;border:none;background:var(--bg-deep);
|
|
1918
|
+
}
|
|
1919
|
+
.config-overlay-close{
|
|
1920
|
+
position:absolute;top:10px;right:14px;z-index:10;
|
|
1921
|
+
width:32px;height:32px;border-radius:8px;border:1px solid var(--border-dim);
|
|
1922
|
+
background:var(--bg-surface);color:var(--text-secondary);cursor:pointer;
|
|
1923
|
+
font-size:16px;display:flex;align-items:center;justify-content:center;
|
|
1924
|
+
transition:all .2s;
|
|
1925
|
+
}
|
|
1926
|
+
.config-overlay-close:hover{border-color:var(--border-glow);color:var(--text-primary)}
|
|
1927
|
+
</style>
|
|
1928
|
+
<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>
|
|
1929
|
+
</body>
|
|
1930
|
+
</html>
|