agent-nuvira 2.7.0 → 2.7.2
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/dist/agents/orchestrator.d.ts.map +1 -1
- package/dist/agents/orchestrator.js +17 -3
- package/dist/agents/orchestrator.js.map +1 -1
- package/dist/app.js +26 -0
- package/dist/cli/chat.d.ts.map +1 -1
- package/dist/cli/chat.js +108 -2
- package/dist/cli/chat.js.map +1 -1
- package/dist/config/auth.js +18 -0
- package/dist/config/jwt.js +24 -0
- package/dist/config/keys.js +14 -0
- package/dist/gateway/chat-store.d.ts +32 -5
- package/dist/gateway/chat-store.d.ts.map +1 -1
- package/dist/gateway/chat-store.js +52 -6
- package/dist/gateway/chat-store.js.map +1 -1
- package/dist/gateway/registry.d.ts.map +1 -1
- package/dist/gateway/registry.js +54 -10
- package/dist/gateway/registry.js.map +1 -1
- package/dist/inference/model-validator.d.ts.map +1 -1
- package/dist/inference/model-validator.js +11 -1
- package/dist/inference/model-validator.js.map +1 -1
- package/dist/inference/tool-call-utils.d.ts +31 -0
- package/dist/inference/tool-call-utils.d.ts.map +1 -1
- package/dist/inference/tool-call-utils.js +41 -0
- package/dist/inference/tool-call-utils.js.map +1 -1
- package/dist/learning/reasoning-trace.d.ts +5 -3
- package/dist/learning/reasoning-trace.d.ts.map +1 -1
- package/dist/learning/reasoning-trace.js +2 -2
- package/dist/learning/reasoning-trace.js.map +1 -1
- package/dist/middleware/auth.js +30 -0
- package/dist/nlu/conversation-gate.js +1 -1
- package/dist/nlu/conversation-gate.js.map +1 -1
- package/dist/nlu/entities.d.ts.map +1 -1
- package/dist/nlu/entities.js +12 -0
- package/dist/nlu/entities.js.map +1 -1
- package/dist/nlu/intent.d.ts.map +1 -1
- package/dist/nlu/intent.js +83 -15
- package/dist/nlu/intent.js.map +1 -1
- package/dist/passport.js +34 -0
- package/dist/routes/auth.js +42 -0
- package/dist/routes/user.js +31 -0
- package/dist/server.js +17 -0
- package/dist/tools/pipeline-tool.d.ts.map +1 -1
- package/dist/tools/pipeline-tool.js +14 -0
- package/dist/tools/pipeline-tool.js.map +1 -1
- package/dist/tools/release-sync.d.ts +52 -16
- package/dist/tools/release-sync.d.ts.map +1 -1
- package/dist/tools/release-sync.js +143 -79
- package/dist/tools/release-sync.js.map +1 -1
- package/dist/web-dashboard/src/App.js +85 -0
- package/dist/web-dashboard/src/admin-auth.test.js +186 -0
- package/dist/web-dashboard/src/ansi.js +23 -0
- package/dist/web-dashboard/src/ansi.test.js +31 -0
- package/dist/web-dashboard/src/api-admin-auth.test.js +172 -0
- package/dist/web-dashboard/src/api-admin.test.js +65 -0
- package/dist/web-dashboard/src/api-hub.test.js +117 -0
- package/dist/web-dashboard/src/api.js +1421 -0
- package/dist/web-dashboard/src/api.test.js +51 -0
- package/dist/web-dashboard/src/artifacts.js +128 -0
- package/dist/web-dashboard/src/artifacts.test.js +139 -0
- package/dist/web-dashboard/src/components/AdminPanel.js +567 -0
- package/dist/web-dashboard/src/components/AdminPanel.test.js +288 -0
- package/dist/web-dashboard/src/components/AgentHub.js +1580 -0
- package/dist/web-dashboard/src/components/AgentHub.test.js +343 -0
- package/dist/web-dashboard/src/components/BedrockOnboarding.js +320 -0
- package/dist/web-dashboard/src/components/BenchmarkCharts.js +228 -0
- package/dist/web-dashboard/src/components/ChatPage.js +1586 -0
- package/dist/web-dashboard/src/components/ChatPage.test.js +899 -0
- package/dist/web-dashboard/src/components/ContactsPage.js +141 -0
- package/dist/web-dashboard/src/components/CostDashboard.js +105 -0
- package/dist/web-dashboard/src/components/DAGView.js +477 -0
- package/dist/web-dashboard/src/components/DAGView.test.js +147 -0
- package/dist/web-dashboard/src/components/EnvVarEditor.js +138 -0
- package/dist/web-dashboard/src/components/EvalsPage.js +73 -0
- package/dist/web-dashboard/src/components/EvalsPage.test.js +120 -0
- package/dist/web-dashboard/src/components/ExecutionHistory.js +201 -0
- package/dist/web-dashboard/src/components/GatewayPage.js +40 -0
- package/dist/web-dashboard/src/components/GatewayPage.test.js +74 -0
- package/dist/web-dashboard/src/components/HealthPanel.js +68 -0
- package/dist/web-dashboard/src/components/HistoryBrowser.js +34 -0
- package/dist/web-dashboard/src/components/Layout.js +50 -0
- package/dist/web-dashboard/src/components/Markdown.js +152 -0
- package/dist/web-dashboard/src/components/Markdown.test.js +126 -0
- package/dist/web-dashboard/src/components/MarkdownZeroDep.js +237 -0
- package/dist/web-dashboard/src/components/MemoryPanel.js +81 -0
- package/dist/web-dashboard/src/components/ModelTimeline.js +298 -0
- package/dist/web-dashboard/src/components/ModelsPanel.js +1484 -0
- package/dist/web-dashboard/src/components/ModelsPanel.test.js +460 -0
- package/dist/web-dashboard/src/components/Overview.js +123 -0
- package/dist/web-dashboard/src/components/PhaseTimeline.js +359 -0
- package/dist/web-dashboard/src/components/PhaseTimeline.test.js +234 -0
- package/dist/web-dashboard/src/components/PlatformConfigSection.js +384 -0
- package/dist/web-dashboard/src/components/PlatformConfigSection.test.js +106 -0
- package/dist/web-dashboard/src/components/PlatformsPage.js +250 -0
- package/dist/web-dashboard/src/components/QuotaPanel.js +159 -0
- package/dist/web-dashboard/src/components/QuotaPanel.test.js +85 -0
- package/dist/web-dashboard/src/components/RequestsPanel.js +229 -0
- package/dist/web-dashboard/src/components/RequestsPanel.test.js +103 -0
- package/dist/web-dashboard/src/components/RoutingInsightsPanel.js +938 -0
- package/dist/web-dashboard/src/components/RoutingInsightsPanel.test.js +339 -0
- package/dist/web-dashboard/src/components/RoutingWalkthrough.js +408 -0
- package/dist/web-dashboard/src/components/RoutingWalkthrough.test.js +209 -0
- package/dist/web-dashboard/src/components/TaskConsole.js +119 -0
- package/dist/web-dashboard/src/components/TaskConsole.test.js +123 -0
- package/dist/web-dashboard/src/components/TasksPage.js +256 -0
- package/dist/web-dashboard/src/components/TasksPage.test.js +103 -0
- package/dist/web-dashboard/src/components/TracePanel.js +306 -0
- package/dist/web-dashboard/src/components/WhatsAppPanel.js +195 -0
- package/dist/web-dashboard/src/components/WhatsAppPanel.test.js +122 -0
- package/dist/web-dashboard/src/jsonOrNull.js +37 -0
- package/dist/web-dashboard/src/main.js +15 -0
- package/dist/web-dashboard/src/mask.js +11 -0
- package/dist/web-dashboard/vite.config.js +23 -0
- package/dist/web-dashboard/vitest.config.js +15 -0
- package/package.json +5 -3
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = TracePanel;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
const api_1 = require("../api");
|
|
6
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
7
|
+
const PROVIDER_ICONS = {
|
|
8
|
+
local: '💻', groq: '🟢', nim: '🔶', gemini: '🔷', openrouter: '🟣',
|
|
9
|
+
};
|
|
10
|
+
const AGENT_ICONS = {
|
|
11
|
+
planner: '🗺️', writer: '✍️', reviewer: '🔎', tester: '🧪', debugger: '🐛',
|
|
12
|
+
runner: '🏃', 'context-gatherer': '📂', memory: '🧠', 'self-improver': '📈',
|
|
13
|
+
};
|
|
14
|
+
function providerIcon(provider) {
|
|
15
|
+
return PROVIDER_ICONS[provider] || '🔌';
|
|
16
|
+
}
|
|
17
|
+
function agentIcon(agent) {
|
|
18
|
+
return AGENT_ICONS[agent] || '🤖';
|
|
19
|
+
}
|
|
20
|
+
function timeAgo(ts) {
|
|
21
|
+
const diff = Date.now() - ts;
|
|
22
|
+
if (diff < 60_000)
|
|
23
|
+
return 'just now';
|
|
24
|
+
if (diff < 3_600_000)
|
|
25
|
+
return `${Math.floor(diff / 60_000)}m ago`;
|
|
26
|
+
if (diff < 86_400_000)
|
|
27
|
+
return `${Math.floor(diff / 3_600_000)}h ago`;
|
|
28
|
+
return `${new Date(ts).toLocaleDateString()}`;
|
|
29
|
+
}
|
|
30
|
+
function fmtDuration(ms) {
|
|
31
|
+
if (ms === undefined)
|
|
32
|
+
return '—';
|
|
33
|
+
if (ms < 1000)
|
|
34
|
+
return `${ms}ms`;
|
|
35
|
+
if (ms < 60_000)
|
|
36
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
37
|
+
return `${Math.floor(ms / 60_000)}m ${Math.floor((ms % 60_000) / 1000)}s`;
|
|
38
|
+
}
|
|
39
|
+
function fmtTokens(n) {
|
|
40
|
+
if (n === undefined || n === null)
|
|
41
|
+
return '0';
|
|
42
|
+
if (n >= 1_000_000)
|
|
43
|
+
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
44
|
+
if (n >= 1_000)
|
|
45
|
+
return `${(n / 1_000).toFixed(1)}K`;
|
|
46
|
+
return String(n);
|
|
47
|
+
}
|
|
48
|
+
function SectionCard({ icon, title, subtitle, children }) {
|
|
49
|
+
return (<div style={{
|
|
50
|
+
background: '#161b22', borderRadius: 12,
|
|
51
|
+
border: '1px solid #21262d', padding: '18px 20px', marginBottom: 16,
|
|
52
|
+
}}>
|
|
53
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 4 }}>
|
|
54
|
+
<span style={{ fontSize: 20 }}>{icon}</span>
|
|
55
|
+
<h3 style={{ fontSize: 15, fontWeight: 600, color: '#e6edf3', margin: 0 }}>{title}</h3>
|
|
56
|
+
</div>
|
|
57
|
+
{subtitle && <p style={{ fontSize: 12, color: '#8b949e', margin: '2px 0 12px 0' }}>{subtitle}</p>}
|
|
58
|
+
{!subtitle && <div style={{ height: 6 }}/>}
|
|
59
|
+
{children}
|
|
60
|
+
</div>);
|
|
61
|
+
}
|
|
62
|
+
function EmptyNote() {
|
|
63
|
+
return (<div style={{
|
|
64
|
+
background: '#0d1117', border: '1px dashed #30363d', borderRadius: 10,
|
|
65
|
+
padding: '18px 20px', color: '#8b949e', fontSize: 13, textAlign: 'center',
|
|
66
|
+
}}>
|
|
67
|
+
🔍 No reasoning traces yet — every LLM call in a <code style={{ color: '#58a6ff' }}>buff execute</code> pipeline
|
|
68
|
+
is recorded to <code style={{ color: '#58a6ff' }}>reasoning-traces.json</code>. Run a pipeline, then replay
|
|
69
|
+
it here or with <code style={{ color: '#58a6ff' }}>buff trace replay <id></code>.
|
|
70
|
+
</div>);
|
|
71
|
+
}
|
|
72
|
+
function StepRow({ step }) {
|
|
73
|
+
const [open, setOpen] = (0, react_1.useState)(false);
|
|
74
|
+
const statusColor = step.success ? '#3fb950' : '#f85149';
|
|
75
|
+
const statusLabel = step.success ? 'ok' : 'failed';
|
|
76
|
+
return (<div style={{
|
|
77
|
+
background: '#0d1117', border: '1px solid #21262d', borderRadius: 8,
|
|
78
|
+
marginBottom: 8, overflow: 'hidden',
|
|
79
|
+
}}>
|
|
80
|
+
<button onClick={() => setOpen(!open)} style={{
|
|
81
|
+
width: '100%', background: 'none', border: 'none', cursor: 'pointer',
|
|
82
|
+
padding: '10px 14px', textAlign: 'left', display: 'flex',
|
|
83
|
+
alignItems: 'center', gap: 10, color: 'inherit',
|
|
84
|
+
}}>
|
|
85
|
+
<span style={{ width: 26, fontSize: 16 }}>{agentIcon(step.agentType)}</span>
|
|
86
|
+
<span style={{
|
|
87
|
+
width: 130, fontSize: 12, fontWeight: 600, color: '#e6edf3',
|
|
88
|
+
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
|
|
89
|
+
}} title={step.agentType}>
|
|
90
|
+
{step.agentType}
|
|
91
|
+
</span>
|
|
92
|
+
<span style={{
|
|
93
|
+
flex: 1, fontSize: 12, color: '#8b949e', fontFamily: "'SFMono-Regular', Consolas, monospace",
|
|
94
|
+
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
|
|
95
|
+
}} title={`${step.provider}/${step.model}`}>
|
|
96
|
+
{providerIcon(step.provider)} {step.provider}/{step.model}
|
|
97
|
+
</span>
|
|
98
|
+
<span style={{ width: 70, fontSize: 11, color: '#6e7681', textAlign: 'right' }}>
|
|
99
|
+
{fmtDuration(step.latencyMs)}
|
|
100
|
+
</span>
|
|
101
|
+
<span style={{
|
|
102
|
+
width: 86, fontSize: 11, color: '#6e7681', fontFamily: "'SFMono-Regular', Consolas, monospace", textAlign: 'right',
|
|
103
|
+
}}>
|
|
104
|
+
{fmtTokens(step.inputTokens)}→{fmtTokens(step.outputTokens)} tok
|
|
105
|
+
</span>
|
|
106
|
+
<span style={{ width: 46, fontSize: 11, color: statusColor, textAlign: 'right' }}>
|
|
107
|
+
{statusLabel}
|
|
108
|
+
</span>
|
|
109
|
+
<span style={{ width: 22, fontSize: 11, textAlign: 'right' }}>
|
|
110
|
+
{step.escalated ? <span title="Repair escalated to a stronger routed model (v1.60.4)">🚀</span> : ''}
|
|
111
|
+
</span>
|
|
112
|
+
<span style={{ fontSize: 11, color: '#6e7681' }}>{open ? '▾' : '▸'}</span>
|
|
113
|
+
</button>
|
|
114
|
+
|
|
115
|
+
{open && (<div style={{ padding: '0 14px 12px 50px', fontSize: 12 }}>
|
|
116
|
+
{step.taskId && (<div style={{ color: '#6e7681', marginBottom: 4 }}>
|
|
117
|
+
Task: <span style={{ color: '#8b949e', fontFamily: "'SFMono-Regular', Consolas, monospace" }}>{step.taskId}</span>
|
|
118
|
+
</div>)}
|
|
119
|
+
{step.description && (<div style={{ color: '#8b949e', marginBottom: 6 }}>{step.description}</div>)}
|
|
120
|
+
{step.routing && (<div style={{ marginBottom: 6 }}>
|
|
121
|
+
<span style={{
|
|
122
|
+
fontSize: 10, padding: '1px 8px', borderRadius: 10,
|
|
123
|
+
background: step.escalated ? '#3d2c00' : '#1c2128',
|
|
124
|
+
border: step.escalated ? '1px solid #d29922' : '1px solid #58a6ff',
|
|
125
|
+
color: step.escalated ? '#d29922' : '#58a6ff',
|
|
126
|
+
}}>
|
|
127
|
+
{step.escalated ? '🚀 escalated auto → ' : '🤖 auto → '}{step.routing.provider}/{step.routing.model} · score {step.routing.score.toFixed(3)} · {step.routing.complexity}
|
|
128
|
+
</span>
|
|
129
|
+
{step.escalated && (<div style={{ color: '#d29922', marginTop: 4, fontSize: 11 }}>
|
|
130
|
+
Repair escalated to a stronger routed model (next complexity level).
|
|
131
|
+
</div>)}
|
|
132
|
+
{step.routing.explanation && (<div style={{ color: '#6e7681', marginTop: 4, fontSize: 11 }}>
|
|
133
|
+
{step.routing.explanation}
|
|
134
|
+
</div>)}
|
|
135
|
+
</div>)}
|
|
136
|
+
{step.error && (<div style={{ color: '#f85149', marginBottom: 6 }}>⚠️ {step.error.slice(0, 300)}</div>)}
|
|
137
|
+
<div style={{ color: '#6e7681', margin: '6px 0 3px 0' }}>
|
|
138
|
+
Prompt <span style={{ fontFamily: "'SFMono-Regular', Consolas, monospace" }}>#{step.promptDigest}</span> · {step.promptPreview.length}+ chars:
|
|
139
|
+
</div>
|
|
140
|
+
<pre style={{
|
|
141
|
+
background: '#161b22', border: '1px solid #21262d', borderRadius: 6,
|
|
142
|
+
padding: 8, margin: 0, color: '#8b949e', whiteSpace: 'pre-wrap',
|
|
143
|
+
wordBreak: 'break-word', maxHeight: 180, overflowY: 'auto',
|
|
144
|
+
fontFamily: "'SFMono-Regular', Consolas, monospace", fontSize: 11,
|
|
145
|
+
}}>
|
|
146
|
+
{step.promptPreview.slice(0, 900)}
|
|
147
|
+
</pre>
|
|
148
|
+
<div style={{ color: '#6e7681', margin: '8px 0 3px 0' }}>
|
|
149
|
+
Response ({fmtTokens(step.responseLength)} chars):
|
|
150
|
+
</div>
|
|
151
|
+
<pre style={{
|
|
152
|
+
background: '#161b22', border: '1px solid #21262d', borderRadius: 6,
|
|
153
|
+
padding: 8, margin: 0, color: '#c9d1d9', whiteSpace: 'pre-wrap',
|
|
154
|
+
wordBreak: 'break-word', maxHeight: 240, overflowY: 'auto',
|
|
155
|
+
fontFamily: "'SFMono-Regular', Consolas, monospace", fontSize: 11,
|
|
156
|
+
}}>
|
|
157
|
+
{step.responsePreview.slice(0, 1200)}
|
|
158
|
+
</pre>
|
|
159
|
+
</div>)}
|
|
160
|
+
</div>);
|
|
161
|
+
}
|
|
162
|
+
function TraceDetail({ trace }) {
|
|
163
|
+
const [steps, setSteps] = (0, react_1.useState)(trace.steps ?? null);
|
|
164
|
+
const [error, setError] = (0, react_1.useState)(false);
|
|
165
|
+
(0, react_1.useEffect)(() => {
|
|
166
|
+
if (trace.steps) {
|
|
167
|
+
setSteps(trace.steps);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
let cancelled = false;
|
|
171
|
+
api_1.dashboardAPI.fetchTraceDetail(trace.id).then((detail) => {
|
|
172
|
+
if (cancelled)
|
|
173
|
+
return;
|
|
174
|
+
if (detail?.steps)
|
|
175
|
+
setSteps(detail.steps);
|
|
176
|
+
else
|
|
177
|
+
setError(true);
|
|
178
|
+
});
|
|
179
|
+
return () => { cancelled = true; };
|
|
180
|
+
}, [trace.id, trace.steps]);
|
|
181
|
+
const statusIcon = trace.success === true ? '✅' : trace.success === false ? '❌' : '⏳';
|
|
182
|
+
const statusLabel = trace.success === true ? 'success' : trace.success === false ? 'failed' : 'in progress';
|
|
183
|
+
return (<SectionCard icon={statusIcon} title={`${trace.id} — ${statusLabel}`} subtitle={trace.goal}>
|
|
184
|
+
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 12 }}>
|
|
185
|
+
<span style={{ fontSize: 11, padding: '2px 10px', borderRadius: 12, background: '#0d1117', border: '1px solid #30363d', color: '#8b949e' }}>
|
|
186
|
+
🕓 {timeAgo(trace.startedAt)}
|
|
187
|
+
</span>
|
|
188
|
+
<span style={{ fontSize: 11, padding: '2px 10px', borderRadius: 12, background: '#0d1117', border: '1px solid #30363d', color: '#8b949e' }}>
|
|
189
|
+
⏱ {fmtDuration(trace.durationMs)}
|
|
190
|
+
</span>
|
|
191
|
+
<span style={{ fontSize: 11, padding: '2px 10px', borderRadius: 12, background: '#0d1117', border: '1px solid #30363d', color: '#8b949e' }}>
|
|
192
|
+
🔢 {steps?.length ?? '?'} call(s)
|
|
193
|
+
</span>
|
|
194
|
+
{steps && steps.some((s) => s.escalated) && (<span style={{ fontSize: 11, padding: '2px 10px', borderRadius: 12, background: '#3d2c00', border: '1px solid #d29922', color: '#d29922' }}>
|
|
195
|
+
🚀 {steps.filter((s) => s.escalated).length} escalated repair(s)
|
|
196
|
+
</span>)}
|
|
197
|
+
{trace.totalTokens !== undefined && (<span style={{ fontSize: 11, padding: '2px 10px', borderRadius: 12, background: '#0d1117', border: '1px solid #30363d', color: '#8b949e' }}>
|
|
198
|
+
🧮 {fmtTokens(trace.totalTokens)} tok
|
|
199
|
+
</span>)}
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
{error && (<div style={{ color: '#f85149', fontSize: 12, marginBottom: 10 }}>
|
|
203
|
+
Could not load trace steps (trace may have been deleted).
|
|
204
|
+
</div>)}
|
|
205
|
+
{steps === null && !error && (<div style={{ color: '#8b949e', fontSize: 12 }}>Loading steps…</div>)}
|
|
206
|
+
{steps && steps.length === 0 && (<div style={{ color: '#8b949e', fontSize: 12 }}>No LLM calls recorded in this trace.</div>)}
|
|
207
|
+
{steps && steps.map((step) => <StepRow key={step.seq} step={step}/>)}
|
|
208
|
+
</SectionCard>);
|
|
209
|
+
}
|
|
210
|
+
function TraceList({ traces }) {
|
|
211
|
+
const [selected, setSelected] = (0, react_1.useState)(null);
|
|
212
|
+
if (selected) {
|
|
213
|
+
return (<>
|
|
214
|
+
<button onClick={() => setSelected(null)} style={{
|
|
215
|
+
background: '#21262d', border: '1px solid #30363d', color: '#e6edf3',
|
|
216
|
+
borderRadius: 8, padding: '6px 14px', fontSize: 12, cursor: 'pointer',
|
|
217
|
+
marginBottom: 12,
|
|
218
|
+
}}>
|
|
219
|
+
← Back to traces
|
|
220
|
+
</button>
|
|
221
|
+
<TraceDetail trace={selected}/>
|
|
222
|
+
</>);
|
|
223
|
+
}
|
|
224
|
+
return (<div>
|
|
225
|
+
{traces.map((trace) => {
|
|
226
|
+
const icon = trace.success === true ? '✅' : trace.success === false ? '❌' : '⏳';
|
|
227
|
+
const agents = trace.steps ? [...new Set(trace.steps.map((s) => s.agentType))].join(', ') : '';
|
|
228
|
+
return (<button key={trace.id} onClick={() => setSelected(trace)} style={{
|
|
229
|
+
width: '100%', background: '#0d1117', border: '1px solid #21262d',
|
|
230
|
+
borderRadius: 10, padding: '12px 16px', marginBottom: 8,
|
|
231
|
+
cursor: 'pointer', textAlign: 'left', color: 'inherit',
|
|
232
|
+
transition: 'border-color 0.2s, box-shadow 0.2s',
|
|
233
|
+
}} onMouseEnter={(e) => {
|
|
234
|
+
e.currentTarget.style.borderColor = '#58a6ff';
|
|
235
|
+
e.currentTarget.style.boxShadow = '0 2px 10px #58a6ff22';
|
|
236
|
+
}} onMouseLeave={(e) => {
|
|
237
|
+
e.currentTarget.style.borderColor = '#21262d';
|
|
238
|
+
e.currentTarget.style.boxShadow = 'none';
|
|
239
|
+
}}>
|
|
240
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
|
|
241
|
+
<span>{icon}</span>
|
|
242
|
+
<span style={{
|
|
243
|
+
fontSize: 11, color: '#58a6ff', fontFamily: "'SFMono-Regular', Consolas, monospace",
|
|
244
|
+
}}>
|
|
245
|
+
{trace.id}
|
|
246
|
+
</span>
|
|
247
|
+
<span style={{ fontSize: 11, color: '#6e7681', marginLeft: 'auto' }}>
|
|
248
|
+
{timeAgo(trace.startedAt)}
|
|
249
|
+
</span>
|
|
250
|
+
</div>
|
|
251
|
+
<div style={{ fontSize: 13, color: '#e6edf3', marginBottom: 6 }}>{trace.goal}</div>
|
|
252
|
+
<div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', fontSize: 11, color: '#8b949e' }}>
|
|
253
|
+
<span>🔢 {trace.stepCount ?? 0} call(s)</span>
|
|
254
|
+
<span>⏱ {fmtDuration(trace.durationMs)}</span>
|
|
255
|
+
{trace.failedSteps ? <span style={{ color: '#f85149' }}>❌ {trace.failedSteps} failed</span> : <span style={{ color: '#3fb950' }}>✓ all ok</span>}
|
|
256
|
+
{trace.totalTokens !== undefined && <span>🧮 {fmtTokens(trace.totalTokens)} tok</span>}
|
|
257
|
+
{agents && <span>🤖 {agents.slice(0, 60)}</span>}
|
|
258
|
+
</div>
|
|
259
|
+
</button>);
|
|
260
|
+
})}
|
|
261
|
+
</div>);
|
|
262
|
+
}
|
|
263
|
+
function TracePanel() {
|
|
264
|
+
const [traces, setTraces] = (0, react_1.useState)(null);
|
|
265
|
+
const [loadError, setLoadError] = (0, react_1.useState)(false);
|
|
266
|
+
const load = (0, react_1.useCallback)(() => {
|
|
267
|
+
api_1.dashboardAPI.fetchTraces().then((t) => {
|
|
268
|
+
if (t) {
|
|
269
|
+
setTraces(t);
|
|
270
|
+
setLoadError(false);
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
// Server unreachable or empty payload — surface a clear error state
|
|
274
|
+
// instead of showing the spinner forever.
|
|
275
|
+
setLoadError(true);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}, []);
|
|
279
|
+
(0, react_1.useEffect)(() => {
|
|
280
|
+
load();
|
|
281
|
+
const interval = setInterval(load, 15_000);
|
|
282
|
+
return () => clearInterval(interval);
|
|
283
|
+
}, [load]);
|
|
284
|
+
return (<div className="panel">
|
|
285
|
+
<div style={{ marginBottom: 16 }}>
|
|
286
|
+
<h2 style={{ margin: 0, fontSize: 18, color: '#e6edf3' }}>
|
|
287
|
+
🔍 Reasoning Traces
|
|
288
|
+
</h2>
|
|
289
|
+
<p style={{ margin: '4px 0 0 0', fontSize: 12, color: '#8b949e' }}>
|
|
290
|
+
Every LLM call in each pipeline — agent × model × prompt digest × response × tokens × latency × routing snapshot (assessment P0).
|
|
291
|
+
</p>
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
{traces === null && !loadError && (<SectionCard icon="⏳" title="Loading…">
|
|
295
|
+
<div style={{ color: '#8b949e', fontSize: 13 }}>Fetching reasoning traces…</div>
|
|
296
|
+
</SectionCard>)}
|
|
297
|
+
{loadError && traces === null && (<SectionCard icon="⚠️" title="Could not reach the dashboard server">
|
|
298
|
+
<div style={{ color: '#8b949e', fontSize: 13 }}>
|
|
299
|
+
The traces endpoint is unavailable right now — retrying automatically. Run{' '}
|
|
300
|
+
<code style={{ color: '#58a6ff' }}>buff trace list</code> in the terminal to inspect traces directly.
|
|
301
|
+
</div>
|
|
302
|
+
</SectionCard>)}
|
|
303
|
+
{traces !== null && traces.length === 0 && <EmptyNote />}
|
|
304
|
+
{traces !== null && traces.length > 0 && <TraceList traces={traces}/>}
|
|
305
|
+
</div>);
|
|
306
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* WhatsAppPanel — P2 in-page WhatsApp bridge pairing (GUI parity with
|
|
4
|
+
* `buff whatsapp pair`).
|
|
5
|
+
*
|
|
6
|
+
* Lives in the Agent Hub → Channels tab. Shows the bridge pairing state,
|
|
7
|
+
* starts a pairing session (QR mode or phone-number code mode), streams the
|
|
8
|
+
* live QR into an <img> (the QR auto-refreshes as WhatsApp rotates it), shows
|
|
9
|
+
* the 8-char code for phone mode, cancels, and un-pairs. Everything rides the
|
|
10
|
+
* real BaileysBridge in the dashboard process via /api/whatsapp — the same
|
|
11
|
+
* engine the CLI's `buff whatsapp pair` uses, so a QR scanned here is the
|
|
12
|
+
* same session the gateway uses.
|
|
13
|
+
*
|
|
14
|
+
* Pair/cancel/unpair are write actions → gated behind the admin session +
|
|
15
|
+
* routing.operate (admin or operator), like the other Channels-tab actions.
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.default = WhatsAppPanel;
|
|
19
|
+
const react_1 = require("react");
|
|
20
|
+
const api_1 = require("../api");
|
|
21
|
+
const mask_1 = require("../mask");
|
|
22
|
+
const STATE_LABEL = {
|
|
23
|
+
idle: 'Not paired',
|
|
24
|
+
pairing: '⏳ Pairing…',
|
|
25
|
+
paired: '✅ Paired',
|
|
26
|
+
failed: '❌ Pairing failed',
|
|
27
|
+
cancelled: '⏹ Cancelled',
|
|
28
|
+
error: '💥 Pairing error',
|
|
29
|
+
};
|
|
30
|
+
function WhatsAppPanel({ authed, canWrite, sessionExpired, reveal = false }) {
|
|
31
|
+
const [status, setStatus] = (0, react_1.useState)(null);
|
|
32
|
+
const [contacts, setContacts] = (0, react_1.useState)({});
|
|
33
|
+
const [phone, setPhone] = (0, react_1.useState)('');
|
|
34
|
+
const [busy, setBusy] = (0, react_1.useState)(false);
|
|
35
|
+
const [msg, setMsg] = (0, react_1.useState)(null);
|
|
36
|
+
const subRef = (0, react_1.useRef)(null);
|
|
37
|
+
const refresh = (0, react_1.useCallback)(async () => {
|
|
38
|
+
const s = await api_1.dashboardAPI.getWhatsAppStatus();
|
|
39
|
+
if (s) {
|
|
40
|
+
setStatus(s.status);
|
|
41
|
+
setContacts(s.contacts ?? {});
|
|
42
|
+
}
|
|
43
|
+
}, []);
|
|
44
|
+
// Fetch once + subscribe to live events (QR data URLs, codes, status) via
|
|
45
|
+
// SSE — the QR refreshes automatically as WhatsApp rotates it.
|
|
46
|
+
(0, react_1.useEffect)(() => {
|
|
47
|
+
void refresh();
|
|
48
|
+
if (!authed)
|
|
49
|
+
return;
|
|
50
|
+
// EventSource may be missing (jsdom/SSR-ish environments) — the panel
|
|
51
|
+
// still shows the last-fetched status, it just won't live-update.
|
|
52
|
+
if (typeof EventSource === 'undefined')
|
|
53
|
+
return;
|
|
54
|
+
const off = api_1.dashboardAPI.subscribeWhatsApp({
|
|
55
|
+
onQr: (qr) => setStatus((s) => (s ? { ...s, qr, state: 'pairing' } : s)),
|
|
56
|
+
onCode: (code) => setStatus((s) => (s ? { ...s, pairingCode: code } : s)),
|
|
57
|
+
onStatus: (s) => {
|
|
58
|
+
setStatus(s);
|
|
59
|
+
if (s.state === 'paired' || s.state === 'failed' || s.state === 'error' || s.state === 'cancelled') {
|
|
60
|
+
setBusy(false);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
subRef.current = off;
|
|
65
|
+
return () => {
|
|
66
|
+
off();
|
|
67
|
+
subRef.current = null;
|
|
68
|
+
};
|
|
69
|
+
}, [authed, refresh]);
|
|
70
|
+
const runWrite = async (action) => {
|
|
71
|
+
setBusy(true);
|
|
72
|
+
setMsg(null);
|
|
73
|
+
const r = await action();
|
|
74
|
+
if (!r.ok && r.error && /Not authenticated|Session expired/i.test(r.error)) {
|
|
75
|
+
sessionExpired();
|
|
76
|
+
setBusy(false);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (r.ok) {
|
|
80
|
+
setMsg({ kind: 'ok', text: 'Done.' });
|
|
81
|
+
await refresh();
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
setMsg({ kind: 'err', text: r.error || 'Action failed.' });
|
|
85
|
+
}
|
|
86
|
+
setBusy(false);
|
|
87
|
+
};
|
|
88
|
+
const startPair = (withPhone) => {
|
|
89
|
+
setMsg(null);
|
|
90
|
+
void runWrite(() => api_1.dashboardAPI.startWhatsAppPair(withPhone ? phone.trim() : undefined)).then(() => setPhone(''));
|
|
91
|
+
};
|
|
92
|
+
const cancelPair = () => {
|
|
93
|
+
setMsg(null);
|
|
94
|
+
void runWrite(() => api_1.dashboardAPI.cancelWhatsAppPair());
|
|
95
|
+
};
|
|
96
|
+
const unpair = () => {
|
|
97
|
+
if (!window.confirm('Remove the paired WhatsApp session from this machine? You will need to scan a QR to pair again.'))
|
|
98
|
+
return;
|
|
99
|
+
setMsg(null);
|
|
100
|
+
void runWrite(() => api_1.dashboardAPI.unpairWhatsApp());
|
|
101
|
+
};
|
|
102
|
+
const state = status?.state ?? 'idle';
|
|
103
|
+
const pairing = state === 'pairing';
|
|
104
|
+
return (<div className="wa-panel">
|
|
105
|
+
<div className="wa-panel-head">
|
|
106
|
+
<span className="wa-panel-title">🟢 WhatsApp bridge</span>
|
|
107
|
+
{status ? (<span className={`wa-badge ${status.paired ? 'wa-badge-paired' : pairing ? 'wa-badge-pairing' : ''}`}>
|
|
108
|
+
{STATE_LABEL[state]}
|
|
109
|
+
</span>) : (<span className="admin-hint">loading…</span>)}
|
|
110
|
+
</div>
|
|
111
|
+
<p className="admin-hint">
|
|
112
|
+
The personal Baileys bridge (no Meta Business account) — pair once, and the gateway can send & receive
|
|
113
|
+
WhatsApp. Session: <code>{status?.sessionDir ?? '~/.buff/whatsapp/session'}</code>. Same engine as{' '}
|
|
114
|
+
<code>buff whatsapp pair</code>.
|
|
115
|
+
</p>
|
|
116
|
+
|
|
117
|
+
{pairing ? (<div className="wa-pairing">
|
|
118
|
+
{status?.qr ? (<div className="wa-qr-box">
|
|
119
|
+
<img className="wa-qr-img" src={status.qr} alt="WhatsApp pairing QR code"/>
|
|
120
|
+
<p className="admin-hint">
|
|
121
|
+
Open WhatsApp on the phone → <strong>Linked devices → Link a device → scan</strong>. The QR refreshes
|
|
122
|
+
automatically while this window is open.
|
|
123
|
+
</p>
|
|
124
|
+
</div>) : status?.phone ? (<div className="wa-code-box">
|
|
125
|
+
<p className="admin-hint">
|
|
126
|
+
On the phone with <strong>{status.phone}</strong>: WhatsApp → Linked devices → Link a device →{' '}
|
|
127
|
+
<strong>“Link with phone number instead”</strong> → enter:
|
|
128
|
+
</p>
|
|
129
|
+
<div className="wa-code">{status.pairingCode ?? '… waiting for code'}</div>
|
|
130
|
+
</div>) : (<div className="wa-code-box">
|
|
131
|
+
<p className="admin-hint">Waiting for WhatsApp to hand over a QR… (it usually appears within a few seconds)</p>
|
|
132
|
+
</div>)}
|
|
133
|
+
{status?.phone ? null : (<div className="admin-hint">
|
|
134
|
+
Tip: on a phone, “Link with phone number instead” needs the number format{' '}
|
|
135
|
+
<code>918844433322</code> (country code included) — enter it below instead of scanning.
|
|
136
|
+
</div>)}
|
|
137
|
+
{status?.error ? <div className="admin-row-msg admin-row-msg-err">{status.error}</div> : null}
|
|
138
|
+
{canWrite && authed ? (<button className="admin-refresh-btn" type="button" onClick={cancelPair} disabled={busy}>
|
|
139
|
+
{busy ? '⏳…' : '⏹ Cancel pairing'}
|
|
140
|
+
</button>) : null}
|
|
141
|
+
</div>) : (<div className="wa-actions">
|
|
142
|
+
{canWrite && authed ? (!status?.paired ? (<>
|
|
143
|
+
<div className="wa-action-row">
|
|
144
|
+
<button className="admin-refresh-btn" type="button" onClick={() => startPair(false)} disabled={busy}>
|
|
145
|
+
{busy ? '⏳…' : '🔳 Pair with a QR'}
|
|
146
|
+
</button>
|
|
147
|
+
<span className="admin-hint">Scans from the phone you want to link.</span>
|
|
148
|
+
</div>
|
|
149
|
+
<div className="wa-action-row wa-phone-row">
|
|
150
|
+
<input type="text" className="wa-phone-input" value={phone} onChange={(e) => setPhone(e.target.value)} placeholder="or pair with a number — e.g. 918844433322" disabled={busy} maxLength={16}/>
|
|
151
|
+
<button className="admin-refresh-btn" type="button" onClick={() => startPair(true)} disabled={busy || !phone.trim()}>
|
|
152
|
+
Pair by number
|
|
153
|
+
</button>
|
|
154
|
+
</div>
|
|
155
|
+
</>) : (<div className="wa-action-row">
|
|
156
|
+
<button className="admin-refresh-btn wa-unpair-btn" type="button" onClick={unpair} disabled={busy}>
|
|
157
|
+
{busy ? '⏳…' : '🗑 Unpair (remove session)'}
|
|
158
|
+
</button>
|
|
159
|
+
<span className="admin-hint">Removes creds.json — you will need to scan a QR again.</span>
|
|
160
|
+
</div>)) : null}
|
|
161
|
+
{state === 'failed' || state === 'error' || state === 'cancelled' ? (<div className="admin-row-msg admin-row-msg-err">
|
|
162
|
+
{status?.error || (state === 'cancelled' ? 'Pairing cancelled.' : 'Pairing failed — try again.')}
|
|
163
|
+
</div>) : null}
|
|
164
|
+
{!authed ? (<div className="admin-row-msg">Log in (admin or operator) to pair, cancel, or unpair.</div>) : !canWrite ? (<div className="admin-row-msg">Your role can view pairing status but not change it (requires admin or operator).</div>) : null}
|
|
165
|
+
{msg ? <div className={`admin-row-msg${msg.kind === 'ok' ? '' : ' admin-row-msg-err'}`}>{msg.text}</div> : null}
|
|
166
|
+
</div>)}
|
|
167
|
+
|
|
168
|
+
{/* Send-by-name contacts — the mapping behind `buff whatsapp contact add`.
|
|
169
|
+
These are for SENDING by name only; they do NOT grant inbound access.
|
|
170
|
+
The verified list (Permissions tab / `buff config gateway allow`) is
|
|
171
|
+
what decides who may TRIGGER the agent. */}
|
|
172
|
+
<div className="wa-contacts">
|
|
173
|
+
<div className="wa-contacts-head">
|
|
174
|
+
<span className="wa-panel-title">📇 Send-by-name contacts</span>
|
|
175
|
+
<span className="admin-hint">{Object.keys(contacts).length} mapped</span>
|
|
176
|
+
</div>
|
|
177
|
+
<p className="admin-hint">
|
|
178
|
+
These names are for <strong>sending messages by name</strong> (e.g.{' '}
|
|
179
|
+
<code>buff gateway send whatsapp:Name "…"</code>) — they do{' '}
|
|
180
|
+
<strong>NOT</strong> let these numbers trigger the agent.
|
|
181
|
+
</p>
|
|
182
|
+
<div className="hub-alias-list">
|
|
183
|
+
{Object.keys(contacts).length === 0 ? (<span className="admin-hint">(none — add one with <code>buff whatsapp contact add <Name> <number></code>)</span>) : (Object.entries(contacts).map(([name, number]) => (<div className="hub-alias-row" key={name}>
|
|
184
|
+
<span className="hub-chip">{name}</span>
|
|
185
|
+
<span className="admin-hint">→ {reveal ? number : (0, mask_1.maskSenderId)(number)}</span>
|
|
186
|
+
</div>)))}
|
|
187
|
+
</div>
|
|
188
|
+
<p className="admin-hint" style={{ marginTop: 8 }}>
|
|
189
|
+
To let a number <strong>trigger the agent</strong> (respond to its messages), add it to the{' '}
|
|
190
|
+
<strong>verified list</strong> in the <strong>Permissions tab above</strong>, or run{' '}
|
|
191
|
+
<code>buff config gateway allow whatsapp user <number></code>.
|
|
192
|
+
</p>
|
|
193
|
+
</div>
|
|
194
|
+
</div>);
|
|
195
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* P2 — WhatsAppPanel tests (in-page pairing UI).
|
|
4
|
+
*
|
|
5
|
+
* The panel renders the bridge pairing state, gates pair/cancel/unpair behind
|
|
6
|
+
* the admin session + routing.operate, and shows the live QR / pairing code
|
|
7
|
+
* while a session is active. API calls are mocked — no server, no network.
|
|
8
|
+
*/
|
|
9
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
10
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
11
|
+
};
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
const vitest_1 = require("vitest");
|
|
14
|
+
const react_1 = require("@testing-library/react");
|
|
15
|
+
const WhatsAppPanel_1 = __importDefault(require("./WhatsAppPanel"));
|
|
16
|
+
const api_1 = require("../api");
|
|
17
|
+
const IDLE = {
|
|
18
|
+
state: 'idle',
|
|
19
|
+
paired: false,
|
|
20
|
+
sessionDir: '/tmp/wa-session',
|
|
21
|
+
qr: null,
|
|
22
|
+
qrRaw: null,
|
|
23
|
+
pairingCode: null,
|
|
24
|
+
phone: null,
|
|
25
|
+
error: null,
|
|
26
|
+
startedAt: null,
|
|
27
|
+
};
|
|
28
|
+
const PAIRED = { ...IDLE, state: 'paired', paired: true };
|
|
29
|
+
const PAIRING_QR = {
|
|
30
|
+
...IDLE,
|
|
31
|
+
state: 'pairing',
|
|
32
|
+
qr: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUg==',
|
|
33
|
+
qrRaw: '2@payload',
|
|
34
|
+
startedAt: Date.now(),
|
|
35
|
+
};
|
|
36
|
+
function mockStatus(status, contacts = {}) {
|
|
37
|
+
vitest_1.vi.spyOn(api_1.dashboardAPI, 'getWhatsAppStatus').mockResolvedValue({ status, contacts });
|
|
38
|
+
vitest_1.vi.spyOn(api_1.dashboardAPI, 'subscribeWhatsApp').mockReturnValue(() => { });
|
|
39
|
+
}
|
|
40
|
+
(0, vitest_1.afterEach)(() => {
|
|
41
|
+
(0, react_1.cleanup)();
|
|
42
|
+
vitest_1.vi.restoreAllMocks();
|
|
43
|
+
(0, api_1.setAdminToken)(null);
|
|
44
|
+
});
|
|
45
|
+
(0, vitest_1.describe)('WhatsAppPanel', () => {
|
|
46
|
+
(0, vitest_1.it)('shows the idle state with a pair button for an admin', async () => {
|
|
47
|
+
mockStatus(IDLE);
|
|
48
|
+
(0, react_1.render)(<WhatsAppPanel_1.default authed canWrite sessionExpired={() => { }}/>);
|
|
49
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(react_1.screen.getByText('Not paired')).toBeTruthy());
|
|
50
|
+
(0, vitest_1.expect)(react_1.screen.getByRole('button', { name: /Pair with a QR/ })).toBeTruthy();
|
|
51
|
+
(0, vitest_1.expect)(react_1.screen.getByPlaceholderText(/918844433322/)).toBeTruthy();
|
|
52
|
+
// The pair-by-number button is disabled until a number is typed.
|
|
53
|
+
(0, vitest_1.expect)(react_1.screen.getByRole('button', { name: 'Pair by number' })).toHaveProperty('disabled', true);
|
|
54
|
+
});
|
|
55
|
+
(0, vitest_1.it)('gates writes behind the admin session', async () => {
|
|
56
|
+
mockStatus(IDLE);
|
|
57
|
+
(0, react_1.render)(<WhatsAppPanel_1.default authed={false} canWrite={false} sessionExpired={() => { }}/>);
|
|
58
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(react_1.screen.getByText('Not paired')).toBeTruthy());
|
|
59
|
+
(0, vitest_1.expect)(react_1.screen.queryByRole('button', { name: /Pair with a QR/ })).toBeNull();
|
|
60
|
+
(0, vitest_1.expect)(react_1.screen.getByText(/Log in \(admin or operator\)/)).toBeTruthy();
|
|
61
|
+
});
|
|
62
|
+
(0, vitest_1.it)('lets a viewer read status but not change it', async () => {
|
|
63
|
+
mockStatus(PAIRED);
|
|
64
|
+
(0, react_1.render)(<WhatsAppPanel_1.default authed canWrite={false} sessionExpired={() => { }}/>);
|
|
65
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(react_1.screen.getByText('✅ Paired')).toBeTruthy());
|
|
66
|
+
(0, vitest_1.expect)(react_1.screen.getByText(/can view pairing status but not change it/)).toBeTruthy();
|
|
67
|
+
(0, vitest_1.expect)(react_1.screen.queryByRole('button', { name: /Unpair/ })).toBeNull();
|
|
68
|
+
});
|
|
69
|
+
(0, vitest_1.it)('renders the live QR image during a pairing session', async () => {
|
|
70
|
+
mockStatus(PAIRING_QR);
|
|
71
|
+
(0, react_1.render)(<WhatsAppPanel_1.default authed canWrite sessionExpired={() => { }}/>);
|
|
72
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(react_1.screen.getByAltText('WhatsApp pairing QR code')).toBeTruthy());
|
|
73
|
+
const img = react_1.screen.getByAltText('WhatsApp pairing QR code');
|
|
74
|
+
(0, vitest_1.expect)(img.src).toContain('data:image/png;base64');
|
|
75
|
+
(0, vitest_1.expect)(react_1.screen.getByRole('button', { name: /Cancel pairing/ })).toBeTruthy();
|
|
76
|
+
});
|
|
77
|
+
(0, vitest_1.it)('starts a QR pairing on button click', async () => {
|
|
78
|
+
mockStatus(IDLE);
|
|
79
|
+
const start = vitest_1.vi.spyOn(api_1.dashboardAPI, 'startWhatsAppPair').mockResolvedValue({ ok: true });
|
|
80
|
+
(0, react_1.render)(<WhatsAppPanel_1.default authed canWrite sessionExpired={() => { }}/>);
|
|
81
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(react_1.screen.getByRole('button', { name: /Pair with a QR/ })).toBeTruthy());
|
|
82
|
+
react_1.fireEvent.click(react_1.screen.getByRole('button', { name: /Pair with a QR/ }));
|
|
83
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(start).toHaveBeenCalledWith(undefined));
|
|
84
|
+
});
|
|
85
|
+
(0, vitest_1.it)('starts a phone pairing with the typed number', async () => {
|
|
86
|
+
mockStatus(IDLE);
|
|
87
|
+
const start = vitest_1.vi.spyOn(api_1.dashboardAPI, 'startWhatsAppPair').mockResolvedValue({ ok: true });
|
|
88
|
+
(0, react_1.render)(<WhatsAppPanel_1.default authed canWrite sessionExpired={() => { }}/>);
|
|
89
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(react_1.screen.getByPlaceholderText(/918844433322/)).toBeTruthy());
|
|
90
|
+
react_1.fireEvent.change(react_1.screen.getByPlaceholderText(/918844433322/), { target: { value: '918844433322' } });
|
|
91
|
+
const btn = react_1.screen.getByRole('button', { name: 'Pair by number' });
|
|
92
|
+
(0, vitest_1.expect)(btn.disabled).toBe(false);
|
|
93
|
+
react_1.fireEvent.click(btn);
|
|
94
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(start).toHaveBeenCalledWith('918844433322'));
|
|
95
|
+
});
|
|
96
|
+
(0, vitest_1.it)('offers unpair when paired', async () => {
|
|
97
|
+
mockStatus(PAIRED);
|
|
98
|
+
const unpair = vitest_1.vi.spyOn(api_1.dashboardAPI, 'unpairWhatsApp').mockResolvedValue({ ok: true });
|
|
99
|
+
vitest_1.vi.spyOn(window, 'confirm').mockReturnValue(true);
|
|
100
|
+
(0, react_1.render)(<WhatsAppPanel_1.default authed canWrite sessionExpired={() => { }}/>);
|
|
101
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(react_1.screen.getByRole('button', { name: /Unpair/ })).toBeTruthy());
|
|
102
|
+
react_1.fireEvent.click(react_1.screen.getByRole('button', { name: /Unpair/ }));
|
|
103
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(unpair).toHaveBeenCalled());
|
|
104
|
+
});
|
|
105
|
+
(0, vitest_1.it)('lists send-by-name contacts and clarifies they do NOT grant trigger access', async () => {
|
|
106
|
+
mockStatus(PAIRED, { Alex: '919876543210', Ria: '918877766655' });
|
|
107
|
+
(0, react_1.render)(<WhatsAppPanel_1.default authed canWrite sessionExpired={() => { }}/>);
|
|
108
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(react_1.screen.getByText('📇 Send-by-name contacts')).toBeTruthy());
|
|
109
|
+
(0, vitest_1.expect)(react_1.screen.getByText('2 mapped')).toBeTruthy();
|
|
110
|
+
(0, vitest_1.expect)(react_1.screen.getByText('Alex')).toBeTruthy();
|
|
111
|
+
(0, vitest_1.expect)(react_1.screen.getByText('Ria')).toBeTruthy();
|
|
112
|
+
// The clarity note — send-by-name ≠ trigger access.
|
|
113
|
+
(0, vitest_1.expect)(react_1.screen.getByText(/let these numbers trigger the agent/)).toBeTruthy();
|
|
114
|
+
(0, vitest_1.expect)(react_1.screen.getByText(/verified list/)).toBeTruthy();
|
|
115
|
+
(0, vitest_1.expect)(react_1.screen.getByText(/buff config gateway allow whatsapp user/)).toBeTruthy();
|
|
116
|
+
});
|
|
117
|
+
(0, vitest_1.it)('shows an empty state when no contacts are mapped', async () => {
|
|
118
|
+
mockStatus(PAIRED, {});
|
|
119
|
+
(0, react_1.render)(<WhatsAppPanel_1.default authed canWrite sessionExpired={() => { }}/>);
|
|
120
|
+
await (0, react_1.waitFor)(() => (0, vitest_1.expect)(react_1.screen.getByText(/none — add one with/)).toBeTruthy());
|
|
121
|
+
});
|
|
122
|
+
});
|