create-metaclaw 3.3.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/LICENSE +44 -0
- package/README.md +282 -0
- package/docs/assets/favicon.png +0 -0
- package/docs/assets/metaclaw-banner.svg +86 -0
- package/docs/assets/qis-logo.png +0 -0
- package/docs/assets/yz-favicon.png +0 -0
- package/docs/assets/yz-logo.png +0 -0
- package/docs/index.html +895 -0
- package/installer/assets/favicon.png +0 -0
- package/installer/auto-start.ts +330 -0
- package/installer/brand.ts +115 -0
- package/installer/core-scaffold.ts +448 -0
- package/installer/dashboard-generator.ts +657 -0
- package/installer/detect.ts +129 -0
- package/installer/index.ts +355 -0
- package/installer/module-loader.ts +412 -0
- package/installer/modules/boardroom/boardroom/client.ts.txt +201 -0
- package/installer/modules/boardroom/boardroom/db.ts.txt +322 -0
- package/installer/modules/boardroom/boardroom/meeting-agent.ts.txt +129 -0
- package/installer/modules/boardroom/boardroom/meeting-scheduler.ts.txt +194 -0
- package/installer/modules/boardroom/boardroom/server.ts.txt +473 -0
- package/installer/modules/boardroom/boardroom/start-boardroom.bat.txt +26 -0
- package/installer/modules/boardroom/boardroom/summons.ts.txt +76 -0
- package/installer/modules/boardroom/boardroom/turn-v2.ts.txt +172 -0
- package/installer/modules/boardroom/boardroom/turn.ts.txt +208 -0
- package/installer/modules/boardroom/boardroom/types.ts.txt +100 -0
- package/installer/modules/boardroom/metaclaw-module.json +35 -0
- package/installer/modules/boardroom/scripts/meeting-check.bat.txt +38 -0
- package/installer/modules/core/metaclaw-module.json +51 -0
- package/installer/modules/core/src/db.ts.txt +277 -0
- package/installer/modules/core/src/health-check.ts.txt +128 -0
- package/installer/modules/core/src/observability.ts.txt +20 -0
- package/installer/modules/core/src/safety.ts.txt +26 -0
- package/installer/modules/core/src/scan-capabilities.ts.txt +196 -0
- package/installer/modules/core/src/self-improve.ts.txt +48 -0
- package/installer/modules/core/src/self-update.ts.txt +345 -0
- package/installer/modules/core/src/sync-context.ts.txt +133 -0
- package/installer/modules/core/src/tasks.ts.txt +159 -0
- package/installer/modules/custom/metaclaw-module.json +15 -0
- package/installer/modules/custom/src/agent-custom.ts.txt +100 -0
- package/installer/modules/dashboard/metaclaw-module.json +23 -0
- package/installer/modules/dashboard/scripts/build-dashboard.cjs.txt +51 -0
- package/installer/modules/dashboard/src/update-dashboard.ts.txt +126 -0
- package/installer/modules/outreach/metaclaw-module.json +29 -0
- package/installer/modules/outreach/src/agent-outreach.ts.txt +193 -0
- package/installer/modules/outreach/src/inbox-agent.ts.txt +283 -0
- package/installer/modules/outreach/src/morning-report.ts.txt +124 -0
- package/installer/modules/research/metaclaw-module.json +15 -0
- package/installer/modules/research/src/agent-research.ts.txt +127 -0
- package/installer/modules/scheduler/metaclaw-module.json +27 -0
- package/installer/modules/scheduler/scripts/agent-cycle.bat.txt +85 -0
- package/installer/modules/scheduler/scripts/detect-session.bat.txt +41 -0
- package/installer/modules/scheduler/scripts/launch.bat.txt +120 -0
- package/installer/modules/scheduler/src/cron-manager.ts.txt +273 -0
- package/installer/modules/social/metaclaw-module.json +15 -0
- package/installer/modules/social/src/agent-social.ts.txt +110 -0
- package/installer/modules/support/metaclaw-module.json +15 -0
- package/installer/modules/support/src/agent-support.ts.txt +60 -0
- package/installer/modules/swarm/metaclaw-module.json +25 -0
- package/installer/modules/swarm/swarm/dht-client.ts.txt +376 -0
- package/installer/modules/swarm/swarm/relay-server.ts.txt +348 -0
- package/installer/modules/swarm/swarm/swarm-client.ts.txt +303 -0
- package/installer/modules/swarm/swarm/types.ts.txt +51 -0
- package/installer/modules/voice/metaclaw-module.json +16 -0
- package/installer/questionnaire.ts +277 -0
- package/installer/research.ts +258 -0
- package/installer/scaffold-from-config.ts +270 -0
- package/installer/task-generator.ts +324 -0
- package/installer/templates/agent-custom.ts.txt +100 -0
- package/installer/templates/agent-cycle.bat.txt +19 -0
- package/installer/templates/agent-outreach.ts.txt +193 -0
- package/installer/templates/agent-research.ts.txt +127 -0
- package/installer/templates/agent-social.ts.txt +110 -0
- package/installer/templates/agent-support.ts.txt +60 -0
- package/installer/templates/build-dashboard.cjs.txt +51 -0
- package/installer/templates/cron-manager.ts.txt +273 -0
- package/installer/templates/dashboard.html.txt +450 -0
- package/installer/templates/db.ts.txt +277 -0
- package/installer/templates/detect-session.bat.txt +41 -0
- package/installer/templates/health-check.ts.txt +128 -0
- package/installer/templates/inbox-agent.ts.txt +283 -0
- package/installer/templates/launch.bat.txt +120 -0
- package/installer/templates/morning-report.ts.txt +124 -0
- package/installer/templates/observability.ts.txt +20 -0
- package/installer/templates/safety.ts.txt +26 -0
- package/installer/templates/self-improve.ts.txt +48 -0
- package/installer/templates/self-update.ts.txt +345 -0
- package/installer/templates/state.json.txt +33 -0
- package/installer/templates/system-context.json.txt +33 -0
- package/installer/templates/update-dashboard.ts.txt +126 -0
- package/package.json +31 -0
- 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
|
+
}
|