create-yonderclaw 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.
Files changed (93) hide show
  1. package/LICENSE +44 -0
  2. package/README.md +288 -0
  3. package/bin/create-yonderclaw.mjs +43 -0
  4. package/docs/assets/favicon.png +0 -0
  5. package/docs/assets/metaclaw-banner.svg +86 -0
  6. package/docs/assets/qis-logo.png +0 -0
  7. package/docs/assets/yz-favicon.png +0 -0
  8. package/docs/assets/yz-logo.png +0 -0
  9. package/docs/index.html +1155 -0
  10. package/installer/assets/favicon.png +0 -0
  11. package/installer/auto-start.ts +330 -0
  12. package/installer/brand.ts +115 -0
  13. package/installer/core-scaffold.ts +448 -0
  14. package/installer/dashboard-generator.ts +657 -0
  15. package/installer/detect.ts +129 -0
  16. package/installer/index.ts +355 -0
  17. package/installer/module-loader.ts +412 -0
  18. package/installer/modules/boardroom/boardroom/client.ts.txt +201 -0
  19. package/installer/modules/boardroom/boardroom/db.ts.txt +322 -0
  20. package/installer/modules/boardroom/boardroom/meeting-agent.ts.txt +129 -0
  21. package/installer/modules/boardroom/boardroom/meeting-scheduler.ts.txt +194 -0
  22. package/installer/modules/boardroom/boardroom/server.ts.txt +473 -0
  23. package/installer/modules/boardroom/boardroom/start-boardroom.bat.txt +26 -0
  24. package/installer/modules/boardroom/boardroom/summons.ts.txt +76 -0
  25. package/installer/modules/boardroom/boardroom/turn-v2.ts.txt +172 -0
  26. package/installer/modules/boardroom/boardroom/turn.ts.txt +208 -0
  27. package/installer/modules/boardroom/boardroom/types.ts.txt +100 -0
  28. package/installer/modules/boardroom/metaclaw-module.json +35 -0
  29. package/installer/modules/boardroom/scripts/meeting-check.bat.txt +38 -0
  30. package/installer/modules/core/metaclaw-module.json +51 -0
  31. package/installer/modules/core/src/db.ts.txt +277 -0
  32. package/installer/modules/core/src/health-check.ts.txt +128 -0
  33. package/installer/modules/core/src/observability.ts.txt +20 -0
  34. package/installer/modules/core/src/safety.ts.txt +26 -0
  35. package/installer/modules/core/src/scan-capabilities.ts.txt +196 -0
  36. package/installer/modules/core/src/self-improve.ts.txt +48 -0
  37. package/installer/modules/core/src/self-update.ts.txt +345 -0
  38. package/installer/modules/core/src/sync-context.ts.txt +133 -0
  39. package/installer/modules/core/src/tasks.ts.txt +159 -0
  40. package/installer/modules/custom/metaclaw-module.json +15 -0
  41. package/installer/modules/custom/src/agent-custom.ts.txt +100 -0
  42. package/installer/modules/dashboard/metaclaw-module.json +23 -0
  43. package/installer/modules/dashboard/scripts/build-dashboard.cjs.txt +51 -0
  44. package/installer/modules/dashboard/src/update-dashboard.ts.txt +126 -0
  45. package/installer/modules/outreach/metaclaw-module.json +29 -0
  46. package/installer/modules/outreach/src/agent-outreach.ts.txt +193 -0
  47. package/installer/modules/outreach/src/inbox-agent.ts.txt +283 -0
  48. package/installer/modules/outreach/src/morning-report.ts.txt +124 -0
  49. package/installer/modules/research/metaclaw-module.json +15 -0
  50. package/installer/modules/research/src/agent-research.ts.txt +127 -0
  51. package/installer/modules/scheduler/metaclaw-module.json +27 -0
  52. package/installer/modules/scheduler/scripts/agent-cycle.bat.txt +85 -0
  53. package/installer/modules/scheduler/scripts/detect-session.bat.txt +41 -0
  54. package/installer/modules/scheduler/scripts/launch.bat.txt +120 -0
  55. package/installer/modules/scheduler/src/cron-manager.ts.txt +273 -0
  56. package/installer/modules/social/metaclaw-module.json +15 -0
  57. package/installer/modules/social/src/agent-social.ts.txt +110 -0
  58. package/installer/modules/support/metaclaw-module.json +15 -0
  59. package/installer/modules/support/src/agent-support.ts.txt +60 -0
  60. package/installer/modules/swarm/metaclaw-module.json +25 -0
  61. package/installer/modules/swarm/swarm/dht-client.ts.txt +376 -0
  62. package/installer/modules/swarm/swarm/relay-server.ts.txt +348 -0
  63. package/installer/modules/swarm/swarm/swarm-client.ts.txt +303 -0
  64. package/installer/modules/swarm/swarm/types.ts.txt +51 -0
  65. package/installer/modules/voice/metaclaw-module.json +16 -0
  66. package/installer/questionnaire.ts +277 -0
  67. package/installer/research.ts +258 -0
  68. package/installer/scaffold-from-config.ts +270 -0
  69. package/installer/task-generator.ts +324 -0
  70. package/installer/templates/agent-custom.ts.txt +100 -0
  71. package/installer/templates/agent-cycle.bat.txt +19 -0
  72. package/installer/templates/agent-outreach.ts.txt +193 -0
  73. package/installer/templates/agent-research.ts.txt +127 -0
  74. package/installer/templates/agent-social.ts.txt +110 -0
  75. package/installer/templates/agent-support.ts.txt +60 -0
  76. package/installer/templates/build-dashboard.cjs.txt +51 -0
  77. package/installer/templates/cron-manager.ts.txt +273 -0
  78. package/installer/templates/dashboard.html.txt +450 -0
  79. package/installer/templates/db.ts.txt +277 -0
  80. package/installer/templates/detect-session.bat.txt +41 -0
  81. package/installer/templates/health-check.ts.txt +128 -0
  82. package/installer/templates/inbox-agent.ts.txt +283 -0
  83. package/installer/templates/launch.bat.txt +120 -0
  84. package/installer/templates/morning-report.ts.txt +124 -0
  85. package/installer/templates/observability.ts.txt +20 -0
  86. package/installer/templates/safety.ts.txt +26 -0
  87. package/installer/templates/self-improve.ts.txt +48 -0
  88. package/installer/templates/self-update.ts.txt +345 -0
  89. package/installer/templates/state.json.txt +33 -0
  90. package/installer/templates/system-context.json.txt +33 -0
  91. package/installer/templates/update-dashboard.ts.txt +126 -0
  92. package/package.json +31 -0
  93. package/setup.bat +178 -0
@@ -0,0 +1,657 @@
1
+ /**
2
+ * MetaClaw Dashboard Generator
3
+ *
4
+ * Builds a CUSTOM dashboard.html tailored to the user's claw type.
5
+ * Each claw gets different KPIs, stats, and sections based on what matters for their use case.
6
+ * The research phase can also inject custom dashboard config.
7
+ *
8
+ * The self-update module evolves the dashboard over time.
9
+ */
10
+
11
+ import type { ClawConfig } from "./research.js";
12
+
13
+ type DashboardSection = {
14
+ id: string;
15
+ title: string;
16
+ type: "kpi" | "table" | "feed" | "health" | "custom";
17
+ };
18
+
19
+ type DashboardKPI = {
20
+ id: string;
21
+ label: string;
22
+ dataKey: string;
23
+ color: string;
24
+ };
25
+
26
+ type DashboardLayout = {
27
+ kpis: DashboardKPI[];
28
+ sections: DashboardSection[];
29
+ };
30
+
31
+ /**
32
+ * Generate dashboard layout based on claw type.
33
+ */
34
+ function getLayoutForTemplate(template: string, answers: Record<string, unknown>): DashboardLayout {
35
+ const base: DashboardKPI[] = [
36
+ { id: "actions", label: "Actions Today", dataKey: "actions_taken", color: "var(--cyan)" },
37
+ { id: "success", label: "Success Rate", dataKey: "success_rate", color: "var(--green)" },
38
+ { id: "cost", label: "Cost Today", dataKey: "total_cost_usd", color: "var(--purple)" },
39
+ { id: "health", label: "System Health", dataKey: "health_status", color: "var(--green)" },
40
+ ];
41
+
42
+ const baseSections: DashboardSection[] = [
43
+ { id: "health", title: "System Health", type: "health" },
44
+ { id: "activity", title: "Recent Activity", type: "feed" },
45
+ ];
46
+
47
+ switch (template) {
48
+ case "outreach":
49
+ return {
50
+ kpis: [
51
+ { id: "sent", label: "Emails Sent", dataKey: "emails_sent", color: "var(--cyan)" },
52
+ { id: "replies", label: "Replies", dataKey: "replies", color: "var(--green)" },
53
+ { id: "rate", label: "Reply Rate", dataKey: "reply_rate", color: "var(--purple)" },
54
+ { id: "health", label: "Deliverability", dataKey: "health_status", color: "var(--green)" },
55
+ ],
56
+ sections: [
57
+ { id: "health", title: "Deliverability Health", type: "health" },
58
+ { id: "prospects", title: "Prospect Pipeline", type: "table" },
59
+ { id: "activity", title: "Send History", type: "feed" },
60
+ ],
61
+ };
62
+
63
+ case "research":
64
+ return {
65
+ kpis: [
66
+ { id: "reports", label: "Reports Generated", dataKey: "reports_count", color: "var(--cyan)" },
67
+ { id: "sources", label: "Sources Found", dataKey: "sources_count", color: "var(--green)" },
68
+ { id: "cost", label: "Research Cost", dataKey: "total_cost_usd", color: "var(--purple)" },
69
+ { id: "health", label: "System Health", dataKey: "health_status", color: "var(--green)" },
70
+ ],
71
+ sections: [
72
+ { id: "health", title: "System Health", type: "health" },
73
+ { id: "reports", title: "Recent Reports", type: "table" },
74
+ { id: "activity", title: "Research Log", type: "feed" },
75
+ ],
76
+ };
77
+
78
+ case "support":
79
+ return {
80
+ kpis: [
81
+ { id: "tickets", label: "Tickets Handled", dataKey: "tickets_handled", color: "var(--cyan)" },
82
+ { id: "resolved", label: "Auto-Resolved", dataKey: "auto_resolved", color: "var(--green)" },
83
+ { id: "escalated", label: "Escalated", dataKey: "escalated", color: "var(--gold)" },
84
+ { id: "health", label: "Response Time", dataKey: "avg_response_time", color: "var(--green)" },
85
+ ],
86
+ sections: [
87
+ { id: "health", title: "System Health", type: "health" },
88
+ { id: "queue", title: "Active Queue", type: "table" },
89
+ { id: "activity", title: "Ticket Log", type: "feed" },
90
+ ],
91
+ };
92
+
93
+ case "social":
94
+ return {
95
+ kpis: [
96
+ { id: "posts", label: "Posts Created", dataKey: "posts_count", color: "var(--cyan)" },
97
+ { id: "scheduled", label: "Scheduled", dataKey: "scheduled_count", color: "var(--purple)" },
98
+ { id: "engagement", label: "Engagement", dataKey: "engagement_rate", color: "var(--green)" },
99
+ { id: "health", label: "System Health", dataKey: "health_status", color: "var(--green)" },
100
+ ],
101
+ sections: [
102
+ { id: "health", title: "System Health", type: "health" },
103
+ { id: "calendar", title: "Content Calendar", type: "table" },
104
+ { id: "activity", title: "Post History", type: "feed" },
105
+ ],
106
+ };
107
+
108
+ default: // custom
109
+ return { kpis: base, sections: baseSections };
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Generate the full dashboard HTML for a specific claw.
115
+ */
116
+ export function generateDashboard(
117
+ agentName: string,
118
+ clawType: string,
119
+ answers: Record<string, unknown>,
120
+ researchNotes?: string
121
+ ): string {
122
+ const layout = getLayoutForTemplate(clawType, answers);
123
+
124
+ const kpiCards = layout.kpis.map(kpi => `
125
+ <div class="kpi">
126
+ <div class="kpi-label">${kpi.label}</div>
127
+ <div class="kpi-value" style="color:${kpi.color}" id="kpi-${kpi.id}">--</div>
128
+ </div>
129
+ `).join("\n");
130
+
131
+ const sectionBlocks = layout.sections.map(section => {
132
+ if (section.type === "health") {
133
+ return `
134
+ <div class="section-header">${section.title}</div>
135
+ <div class="grid grid-stats" id="section-${section.id}">
136
+ <div class="card"><div class="card-header">Circuit Breaker</div><div class="card-value green" id="cb-state">--</div></div>
137
+ <div class="card"><div class="card-header">Error Rate</div><div class="card-value green" id="error-rate">--</div></div>
138
+ <div class="card"><div class="card-header">Rate Limit</div><div class="card-value" id="rate-limit">--</div><div class="progress-bar"><div class="progress-fill cyan" id="rate-bar" style="width:0%"></div></div></div>
139
+ <div class="card"><div class="card-header">Prompt Version</div><div class="card-value gold" id="prompt-ver">--</div><div class="card-sub" id="prompt-score">--</div></div>
140
+ </div>`;
141
+ }
142
+ if (section.type === "feed") {
143
+ return `
144
+ <div class="section-header">${section.title}</div>
145
+ <div class="card" style="margin:0 16px"><div id="feed-${section.id}" class="feed">Waiting for data...</div></div>`;
146
+ }
147
+ if (section.type === "table") {
148
+ return `
149
+ <div class="section-header">${section.title}</div>
150
+ <div class="card" style="margin:0 16px"><div id="table-${section.id}" style="max-height:200px;overflow-y:auto">No data yet</div></div>`;
151
+ }
152
+ return "";
153
+ }).join("\n");
154
+
155
+ return `<!DOCTYPE html>
156
+ <html lang="en">
157
+ <head>
158
+ <meta charset="UTF-8">
159
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
160
+ <meta name="mobile-web-app-capable" content="yes">
161
+ <meta name="apple-mobile-web-app-capable" content="yes">
162
+ <title>${agentName} Command Center</title>
163
+ <link rel="icon" type="image/png" href="favicon.png">
164
+ <link href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@300;400;500;600;700&family=Orbitron:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@300;400;500&display=swap" rel="stylesheet">
165
+ <style>
166
+ :root {
167
+ --cyan: #00BEEA; --cyan-bright: #00D9FF;
168
+ --purple: #8B5CF6; --green: #10B981;
169
+ --gold: #F59E0B; --red: #EF4444;
170
+ --bg: #0A0A0A;
171
+ --border: rgba(0,190,234,0.15);
172
+ --text: #E8F5E9; --text-muted: rgba(232,245,233,0.45);
173
+ }
174
+ * { margin:0; padding:0; box-sizing:border-box; }
175
+ body {
176
+ background: var(--bg); color: var(--text);
177
+ font-family: 'Rajdhani', sans-serif; font-size: 14px;
178
+ background-image:
179
+ radial-gradient(ellipse at 20% 50%, rgba(0,190,234,0.04) 0%, transparent 50%),
180
+ radial-gradient(ellipse at 80% 20%, rgba(0,190,234,0.02) 0%, transparent 50%);
181
+ }
182
+ h1,h2,.label { font-family: 'Orbitron', sans-serif; }
183
+ .mono { font-family: 'JetBrains Mono', monospace; font-size: 12px; }
184
+
185
+ .header {
186
+ padding: 14px 20px; border-bottom: 1px solid var(--border);
187
+ display: flex; justify-content: space-between; align-items: center;
188
+ background: rgba(10,10,10,0.9); backdrop-filter: blur(10px);
189
+ position: sticky; top: 0; z-index: 100;
190
+ }
191
+ .header h1 {
192
+ font-size: 16px; letter-spacing: 2px;
193
+ background: linear-gradient(180deg, #fff, var(--cyan));
194
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent;
195
+ }
196
+ .dot { display:inline-block; width:8px; height:8px; border-radius:50%; margin-right:6px; animation:pulse 2s infinite; }
197
+ .dot.g { background:var(--green); box-shadow:0 0 6px var(--green); }
198
+ .dot.r { background:var(--red); box-shadow:0 0 6px var(--red); }
199
+ @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.5} }
200
+
201
+ .grid-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; padding: 16px; }
202
+ .card {
203
+ background: rgba(0,0,0,0.5); border: 1px solid var(--border);
204
+ border-radius: 8px; padding: 14px; transition: border-color 0.3s;
205
+ }
206
+ .card:hover { border-color: rgba(0,190,234,0.4); }
207
+ .card-header { font-family:'Orbitron',sans-serif; font-size:10px; letter-spacing:1.5px; color:var(--text-muted); text-transform:uppercase; margin-bottom:8px; }
208
+ .card-value { font-family:'Orbitron',sans-serif; font-size:28px; font-weight:700; line-height:1; }
209
+ .card-sub { font-size:12px; color:var(--text-muted); margin-top:4px; }
210
+ .cyan { color: var(--cyan); } .green { color: var(--green); }
211
+ .purple { color: var(--purple); } .gold { color: var(--gold); } .red { color: var(--red); }
212
+
213
+ .section-header {
214
+ font-family:'Orbitron',sans-serif; font-size:12px; letter-spacing:2px;
215
+ color:var(--cyan); text-transform:uppercase;
216
+ padding:12px 20px 4px; border-bottom:1px solid rgba(0,190,234,0.1); margin-top:8px;
217
+ }
218
+
219
+ .kpi-strip { display:flex; gap:2px; padding:8px 16px; background:rgba(10,10,10,0.7); }
220
+ .kpi { flex:1; text-align:center; padding:8px 4px; background:rgba(0,0,0,0.3); border:1px solid rgba(0,190,234,0.08); border-radius:4px; }
221
+ .kpi-label { font-family:'Orbitron',sans-serif; font-size:8px; letter-spacing:1px; color:var(--text-muted); text-transform:uppercase; }
222
+ .kpi-value { font-family:'Orbitron',sans-serif; font-size:22px; font-weight:700; line-height:1.2; }
223
+
224
+ .progress-bar { height:6px; background:rgba(255,255,255,0.05); border-radius:3px; overflow:hidden; margin-top:6px; }
225
+ .progress-fill { height:100%; border-radius:3px; transition:width 0.5s; }
226
+ .progress-fill.cyan { background:linear-gradient(90deg, var(--cyan), var(--cyan-bright)); }
227
+
228
+ .feed { max-height:200px; overflow-y:auto; font-family:'JetBrains Mono',monospace; font-size:11px; line-height:1.8; }
229
+ .feed::-webkit-scrollbar { width:3px; }
230
+ .feed::-webkit-scrollbar-thumb { background:var(--border); }
231
+
232
+ .footer { padding:16px; text-align:center; color:var(--text-muted); font-size:10px; font-family:'JetBrains Mono',monospace; }
233
+
234
+ @media (max-width:900px) { .grid-stats { grid-template-columns: repeat(2, 1fr); } }
235
+ </style>
236
+ </head>
237
+ <body>
238
+
239
+ <div class="header">
240
+ <div style="display:flex;align-items:center;gap:10px">
241
+ <img src="favicon.png" alt="YZ" style="height:28px;opacity:0.85">
242
+ <div>
243
+ <h1>${agentName.toUpperCase()}</h1>
244
+ <div class="mono" style="color:var(--text-muted);font-size:10px">${clawType} agent — MetaClaw v3.3 — Yonder Zenith LLC</div>
245
+ </div>
246
+ </div>
247
+ <div style="text-align:right">
248
+ <div id="statusDot"><span class="dot g"></span><span class="mono">ONLINE</span></div>
249
+ <div class="mono" style="color:var(--text-muted);font-size:10px" id="lastUpdate">--</div>
250
+ </div>
251
+ </div>
252
+
253
+ <div class="kpi-strip">
254
+ ${kpiCards}
255
+ </div>
256
+
257
+ ${sectionBlocks}
258
+
259
+ <!-- Voice Control Panel -->
260
+ <div id="voice-panel" style="
261
+ position:fixed; bottom:20px; right:20px; z-index:200;
262
+ display:flex; flex-direction:column; align-items:flex-end; gap:8px;
263
+ ">
264
+ <!-- Mode toggle -->
265
+ <div id="voice-mode-toggle" style="
266
+ display:flex; align-items:center; gap:6px; padding:4px 10px;
267
+ background:rgba(0,0,0,0.7); border:1px solid var(--border); border-radius:12px;
268
+ font-family:'JetBrains Mono',monospace; font-size:10px; color:var(--text-muted); cursor:pointer;
269
+ user-select:none; backdrop-filter:blur(8px);
270
+ " onclick="toggleVoiceMode()">
271
+ <span id="mode-label">PUSH TO TALK</span>
272
+ <div id="mode-switch" style="
273
+ width:28px; height:14px; border-radius:7px; background:rgba(255,255,255,0.1);
274
+ position:relative; transition:background 0.3s;
275
+ "><div id="mode-dot" style="
276
+ width:10px; height:10px; border-radius:50%; background:var(--text-muted);
277
+ position:absolute; top:2px; left:2px; transition:all 0.3s;
278
+ "></div></div>
279
+ </div>
280
+
281
+ <!-- Mic button -->
282
+ <div style="display:flex; align-items:center; gap:10px;">
283
+ <div id="voice-hint" style="
284
+ font-family:'JetBrains Mono',monospace; font-size:10px; color:var(--text-muted);
285
+ padding:4px 8px; background:rgba(0,0,0,0.5); border-radius:6px;
286
+ border:1px solid var(--border); white-space:nowrap;
287
+ ">Hold SPACE or click to talk</div>
288
+ <button id="mic-btn" onmousedown="micDown()" onmouseup="micUp()" ontouchstart="micDown()" ontouchend="micUp()" style="
289
+ width:52px; height:52px; border-radius:50%; border:2px solid var(--cyan);
290
+ background:rgba(0,190,234,0.08); cursor:pointer; display:flex;
291
+ align-items:center; justify-content:center; transition:all 0.2s;
292
+ backdrop-filter:blur(8px); position:relative;
293
+ ">
294
+ <svg id="mic-icon" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="var(--cyan)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
295
+ <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
296
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
297
+ <line x1="12" y1="19" x2="12" y2="23"/>
298
+ <line x1="8" y1="23" x2="16" y2="23"/>
299
+ </svg>
300
+ <!-- Recording indicator ring -->
301
+ <div id="mic-ring" style="
302
+ position:absolute; top:-4px; left:-4px; right:-4px; bottom:-4px;
303
+ border-radius:50%; border:2px solid var(--red); opacity:0;
304
+ transition:opacity 0.2s; animation:none;
305
+ "></div>
306
+ </button>
307
+ </div>
308
+
309
+ <!-- Status indicator -->
310
+ <div id="voice-status" style="
311
+ font-family:'JetBrains Mono',monospace; font-size:9px; color:var(--text-muted);
312
+ text-align:right; min-height:14px;
313
+ "></div>
314
+ </div>
315
+
316
+ <style>
317
+ @keyframes mic-pulse { 0%,100%{transform:scale(1);opacity:0.7} 50%{transform:scale(1.15);opacity:1} }
318
+ #mic-btn.recording { border-color:var(--red); background:rgba(239,68,68,0.15); }
319
+ #mic-btn.recording #mic-icon { stroke:var(--red); }
320
+ #mic-btn.recording #mic-ring { opacity:1; animation:mic-pulse 1.2s infinite; }
321
+ #mic-btn:hover { background:rgba(0,190,234,0.15); }
322
+ #mic-btn.speaking { border-color:var(--green); background:rgba(16,185,129,0.1); }
323
+ #mic-btn.speaking #mic-icon { stroke:var(--green); }
324
+ </style>
325
+
326
+ <div class="footer">${agentName} Command Center — MetaClaw v3.3 — Yonder Zenith LLC</div>
327
+
328
+ <!-- Inline data placeholders (baked by build-dashboard.cjs for offline mode) -->
329
+ <script id="inline-dashboard" type="application/json">__DASHBOARD_JSON__</script>
330
+ <script id="inline-state" type="application/json">__STATE_JSON__</script>
331
+ <script id="inline-context" type="application/json">__CONTEXT_JSON__</script>
332
+
333
+ <script>
334
+ const DATA_URL = 'data/dashboard.json';
335
+
336
+ // Try inline data first (file:// compatible), fall back to fetch (server mode)
337
+ function tryInline(id) {
338
+ try {
339
+ const el = document.getElementById(id);
340
+ if (el && el.textContent && !el.textContent.startsWith('__'))
341
+ return JSON.parse(el.textContent);
342
+ } catch {}
343
+ return null;
344
+ }
345
+
346
+ async function loadDashboard() {
347
+ try {
348
+ const d = tryInline('inline-dashboard') || await (await fetch(DATA_URL + '?t=' + Date.now())).json();
349
+ if (!d) return;
350
+
351
+ document.getElementById('lastUpdate').textContent = d.generated_at || '--';
352
+
353
+ const m = d.today_metrics || {};
354
+ const cb = d.circuit_breaker || {};
355
+
356
+ // KPIs — map data to IDs
357
+ const kpiMap = {
358
+ 'actions_taken': m.actions_taken || 0,
359
+ 'success_rate': m.actions_taken > 0 ? ((m.actions_succeeded||0)/m.actions_taken*100).toFixed(0)+'%' : '100%',
360
+ 'total_cost_usd': '$'+(m.total_cost_usd||0).toFixed(2),
361
+ 'health_status': (cb.state||'closed').toUpperCase(),
362
+ 'emails_sent': m.actions_taken || 0,
363
+ 'replies': m.actions_succeeded || 0,
364
+ 'reply_rate': m.actions_taken > 0 ? ((m.actions_succeeded||0)/m.actions_taken*100).toFixed(0)+'%' : '--',
365
+ 'reports_count': m.actions_succeeded || 0,
366
+ 'sources_count': m.actions_taken || 0,
367
+ 'tickets_handled': m.actions_taken || 0,
368
+ 'auto_resolved': m.actions_succeeded || 0,
369
+ 'escalated': m.actions_failed || 0,
370
+ 'avg_response_time': '--',
371
+ 'posts_count': m.actions_succeeded || 0,
372
+ 'scheduled_count': m.actions_taken || 0,
373
+ 'engagement_rate': '--',
374
+ };
375
+
376
+ document.querySelectorAll('.kpi-value[id^="kpi-"]').forEach(el => {
377
+ const key = el.id.replace('kpi-','');
378
+ // Find matching data key from the layout
379
+ const val = Object.entries(kpiMap).find(([k]) => el.parentElement?.querySelector('.kpi-label')?.textContent?.toLowerCase().includes(k.split('_')[0]));
380
+ });
381
+
382
+ // Direct KPI updates by position
383
+ const kpiEls = document.querySelectorAll('.kpi-value');
384
+ const kpiValues = [${layout.kpis.map(k => `kpiMap['${k.dataKey}']`).join(', ')}];
385
+ kpiEls.forEach((el, i) => { if (kpiValues[i] !== undefined) el.textContent = kpiValues[i]; });
386
+
387
+ // Health section
388
+ const cbEl = document.getElementById('cb-state');
389
+ if (cbEl) { cbEl.textContent = (cb.state||'closed').toUpperCase(); cbEl.className = 'card-value '+(cb.state==='open'?'red':'green'); }
390
+ const erEl = document.getElementById('error-rate');
391
+ if (erEl && m.actions_taken > 0) { const r=((m.actions_failed||0)/m.actions_taken*100).toFixed(1); erEl.textContent=r+'%'; erEl.className='card-value '+(parseFloat(r)>5?'red':'green'); }
392
+ const rlEl = document.getElementById('rate-limit');
393
+ if (rlEl) { rlEl.textContent=(m.actions_taken||0)+'/'+(d.safety_config?.maxActionsPerDay||50); }
394
+ const rbEl = document.getElementById('rate-bar');
395
+ if (rbEl) { rbEl.style.width=((m.actions_taken||0)/(d.safety_config?.maxActionsPerDay||50)*100)+'%'; }
396
+ if (d.prompt_version) {
397
+ const pvEl = document.getElementById('prompt-ver');
398
+ if (pvEl) pvEl.textContent = 'v'+d.prompt_version.version;
399
+ const psEl = document.getElementById('prompt-score');
400
+ if (psEl) psEl.textContent = 'Score: '+(d.prompt_version.avg_score||0).toFixed(1)+'/10 | '+d.prompt_version.total_runs+' runs';
401
+ }
402
+
403
+ // Status
404
+ const sd = document.getElementById('statusDot');
405
+ if (sd) sd.innerHTML = cb.state==='open' ? '<span class="dot r"></span><span class="mono">ALERT</span>' : '<span class="dot g"></span><span class="mono">ONLINE</span>';
406
+
407
+ // Activity feed
408
+ const actions = d.recent_actions || [];
409
+ const feedEl = document.getElementById('feed-activity');
410
+ if (feedEl && actions.length > 0) {
411
+ feedEl.innerHTML = actions.slice(0,15).map(a =>
412
+ '<div style="padding:1px 0;border-bottom:1px solid rgba(255,255,255,0.03)">' +
413
+ '<span style="color:var(--text-muted)">'+(a.created_at||'').slice(11,16)+'</span> ' +
414
+ '<span style="color:'+(a.status==='success'?'var(--green)':'var(--red)')+'">'+a.action_type+'</span> ' +
415
+ '<span style="color:var(--text-muted)">'+((a.target||'').slice(0,30))+'</span></div>'
416
+ ).join('');
417
+ }
418
+
419
+ } catch(e) { /* waiting for data */ }
420
+ }
421
+
422
+ loadDashboard();
423
+ setInterval(loadDashboard, 15000);
424
+
425
+ // ===== VOICE MODULE =====
426
+
427
+ let voiceMode = 'push'; // 'push' or 'always'
428
+ let isRecording = false;
429
+ let isSpeaking = false;
430
+ let recognition = null;
431
+ let voiceQueue = [];
432
+ let lastVoiceQueueLen = 0;
433
+
434
+ // --- Speech Recognition (STT) ---
435
+ function initRecognition() {
436
+ const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
437
+ if (!SR) { setVoiceStatus('Speech recognition not supported'); return null; }
438
+ const r = new SR();
439
+ r.continuous = true;
440
+ r.interimResults = true;
441
+ r.lang = 'en-US';
442
+
443
+ r.onresult = (e) => {
444
+ let final = '';
445
+ let interim = '';
446
+ for (let i = e.resultIndex; i < e.results.length; i++) {
447
+ if (e.results[i].isFinal) final += e.results[i][0].transcript;
448
+ else interim += e.results[i][0].transcript;
449
+ }
450
+ if (interim) setVoiceStatus('Hearing: ' + interim.slice(0, 60) + '...');
451
+ if (final) {
452
+ setVoiceStatus('Sent: ' + final.slice(0, 60));
453
+ sendVoiceInput(final.trim());
454
+ }
455
+ };
456
+
457
+ r.onerror = (e) => {
458
+ if (e.error !== 'no-speech' && e.error !== 'aborted')
459
+ setVoiceStatus('Mic error: ' + e.error);
460
+ };
461
+
462
+ r.onend = () => {
463
+ // Auto-restart in always-on mode
464
+ if (voiceMode === 'always' && !isSpeaking) {
465
+ try { r.start(); } catch {}
466
+ } else {
467
+ stopRecordingUI();
468
+ }
469
+ };
470
+
471
+ return r;
472
+ }
473
+
474
+ function startRecording() {
475
+ if (isRecording || isSpeaking) return;
476
+ if (!recognition) recognition = initRecognition();
477
+ if (!recognition) return;
478
+ try {
479
+ recognition.start();
480
+ isRecording = true;
481
+ startRecordingUI();
482
+ setVoiceStatus('Listening...');
483
+ } catch (e) {
484
+ // Already started
485
+ if (e.message?.includes('already started')) {
486
+ isRecording = true;
487
+ startRecordingUI();
488
+ }
489
+ }
490
+ }
491
+
492
+ function stopRecording() {
493
+ if (!isRecording || voiceMode === 'always') return;
494
+ try { recognition?.stop(); } catch {}
495
+ isRecording = false;
496
+ stopRecordingUI();
497
+ setVoiceStatus('');
498
+ }
499
+
500
+ function micDown() {
501
+ if (voiceMode === 'push') startRecording();
502
+ }
503
+
504
+ function micUp() {
505
+ if (voiceMode === 'push') stopRecording();
506
+ }
507
+
508
+ // --- Voice Mode Toggle ---
509
+ function toggleVoiceMode() {
510
+ if (voiceMode === 'push') {
511
+ voiceMode = 'always';
512
+ document.getElementById('mode-label').textContent = 'ALWAYS ON';
513
+ document.getElementById('mode-dot').style.left = '16px';
514
+ document.getElementById('mode-dot').style.background = 'var(--green)';
515
+ document.getElementById('mode-switch').style.background = 'rgba(16,185,129,0.3)';
516
+ document.getElementById('voice-hint').textContent = 'Listening continuously';
517
+ startRecording();
518
+ } else {
519
+ voiceMode = 'push';
520
+ document.getElementById('mode-label').textContent = 'PUSH TO TALK';
521
+ document.getElementById('mode-dot').style.left = '2px';
522
+ document.getElementById('mode-dot').style.background = 'var(--text-muted)';
523
+ document.getElementById('mode-switch').style.background = 'rgba(255,255,255,0.1)';
524
+ document.getElementById('voice-hint').textContent = 'Hold SPACE or click to talk';
525
+ stopRecording();
526
+ isRecording = false;
527
+ stopRecordingUI();
528
+ }
529
+ }
530
+
531
+ // --- Keyboard: spacebar hold ---
532
+ document.addEventListener('keydown', (e) => {
533
+ if (e.code === 'Space' && voiceMode === 'push' && !e.repeat &&
534
+ !['INPUT','TEXTAREA','SELECT'].includes(e.target.tagName)) {
535
+ e.preventDefault();
536
+ startRecording();
537
+ }
538
+ });
539
+ document.addEventListener('keyup', (e) => {
540
+ if (e.code === 'Space' && voiceMode === 'push') {
541
+ e.preventDefault();
542
+ stopRecording();
543
+ }
544
+ });
545
+
546
+ // --- UI Updates ---
547
+ function startRecordingUI() {
548
+ document.getElementById('mic-btn')?.classList.add('recording');
549
+ }
550
+ function stopRecordingUI() {
551
+ document.getElementById('mic-btn')?.classList.remove('recording');
552
+ }
553
+ function setVoiceStatus(text) {
554
+ const el = document.getElementById('voice-status');
555
+ if (el) el.textContent = text;
556
+ }
557
+
558
+ // --- Send voice input to agent (write to file via local server POST) ---
559
+ async function sendVoiceInput(text) {
560
+ if (!text) return;
561
+ try {
562
+ // Try localhost relay first
563
+ await fetch('http://localhost:8080/voice-input', {
564
+ method: 'POST',
565
+ headers: { 'Content-Type': 'application/json' },
566
+ body: JSON.stringify({ text, timestamp: new Date().toISOString() }),
567
+ });
568
+ } catch {
569
+ // Fallback: store in localStorage for agent to pick up
570
+ const queue = JSON.parse(localStorage.getItem('voice_input_queue') || '[]');
571
+ queue.push({ text, timestamp: new Date().toISOString() });
572
+ localStorage.setItem('voice_input_queue', JSON.stringify(queue));
573
+ }
574
+ }
575
+
576
+ // --- Text-to-Speech (TTS) — agent talks back ---
577
+ let ttsVoice = null;
578
+ function initTTS() {
579
+ const synth = window.speechSynthesis;
580
+ function pickVoice() {
581
+ const voices = synth.getVoices();
582
+ // Prefer Microsoft Jenny Online (Natural) in Edge
583
+ ttsVoice = voices.find(v => v.name.includes('Jenny') && v.name.includes('Online'))
584
+ || voices.find(v => v.name.includes('Online') && v.name.includes('Natural'))
585
+ || voices.find(v => v.lang.startsWith('en'))
586
+ || voices[0];
587
+ }
588
+ if (synth.getVoices().length > 0) pickVoice();
589
+ synth.onvoiceschanged = pickVoice;
590
+ }
591
+
592
+ function speakText(text) {
593
+ if (!text || !window.speechSynthesis) return;
594
+ isSpeaking = true;
595
+ document.getElementById('mic-btn')?.classList.add('speaking');
596
+ setVoiceStatus('Speaking...');
597
+
598
+ // Pause recognition while speaking to avoid feedback
599
+ if (isRecording && voiceMode === 'always') {
600
+ try { recognition?.stop(); } catch {}
601
+ }
602
+
603
+ // Chunk long text into sentences to avoid Chromium stall bug
604
+ const sentences = text.match(/[^.!?]+[.!?]+|[^.!?]+$/g) || [text];
605
+
606
+ let i = 0;
607
+ function speakNext() {
608
+ if (i >= sentences.length) {
609
+ isSpeaking = false;
610
+ document.getElementById('mic-btn')?.classList.remove('speaking');
611
+ setVoiceStatus('');
612
+ // Resume recognition in always-on mode
613
+ if (voiceMode === 'always') {
614
+ try { recognition?.start(); isRecording = true; } catch {}
615
+ }
616
+ return;
617
+ }
618
+ const utt = new SpeechSynthesisUtterance(sentences[i].trim());
619
+ if (ttsVoice) utt.voice = ttsVoice;
620
+ utt.rate = 1.0;
621
+ utt.pitch = 1.0;
622
+ utt.onend = () => { i++; speakNext(); };
623
+ utt.onerror = () => { i++; speakNext(); };
624
+ window.speechSynthesis.speak(utt);
625
+ }
626
+ speakNext();
627
+ }
628
+
629
+ // --- Poll voice_queue from dashboard.json ---
630
+ function checkVoiceQueue(dashData) {
631
+ if (!dashData || !dashData.voice_queue) return;
632
+ const q = dashData.voice_queue;
633
+ if (q.length > lastVoiceQueueLen) {
634
+ // Speak new entries
635
+ for (let i = lastVoiceQueueLen; i < q.length; i++) {
636
+ speakText(q[i].text || q[i]);
637
+ }
638
+ lastVoiceQueueLen = q.length;
639
+ }
640
+ }
641
+
642
+ // Hook into existing dashboard poll
643
+ const _origLoad = loadDashboard;
644
+ loadDashboard = async function() {
645
+ await _origLoad();
646
+ try {
647
+ const d = tryInline('inline-dashboard') || await (await fetch(DATA_URL + '?t=' + Date.now())).json();
648
+ if (d) checkVoiceQueue(d);
649
+ } catch {}
650
+ };
651
+
652
+ // Init
653
+ initTTS();
654
+ </script>
655
+ </body>
656
+ </html>`;
657
+ }