@swarmclawai/swarmclaw 0.5.3 → 0.6.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/README.md +53 -9
- package/bin/server-cmd.js +1 -0
- package/bin/swarmclaw.js +76 -16
- package/next.config.ts +11 -1
- package/package.json +5 -2
- package/scripts/postinstall.mjs +18 -0
- package/src/app/api/canvas/[sessionId]/route.ts +31 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +284 -0
- package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
- package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
- package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
- package/src/app/api/chatrooms/[id]/route.ts +84 -0
- package/src/app/api/chatrooms/route.ts +50 -0
- package/src/app/api/connectors/[id]/route.ts +1 -0
- package/src/app/api/connectors/route.ts +2 -1
- package/src/app/api/credentials/route.ts +2 -3
- package/src/app/api/files/open/route.ts +43 -0
- package/src/app/api/knowledge/[id]/route.ts +13 -2
- package/src/app/api/knowledge/route.ts +8 -1
- package/src/app/api/memory/route.ts +8 -0
- package/src/app/api/notifications/route.ts +4 -0
- package/src/app/api/orchestrator/run/route.ts +1 -1
- package/src/app/api/plugins/install/route.ts +2 -2
- package/src/app/api/search/route.ts +53 -1
- package/src/app/api/sessions/[id]/chat/route.ts +2 -0
- package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
- package/src/app/api/sessions/[id]/fork/route.ts +1 -1
- package/src/app/api/sessions/[id]/messages/route.ts +70 -2
- package/src/app/api/sessions/[id]/route.ts +4 -0
- package/src/app/api/sessions/route.ts +3 -3
- package/src/app/api/settings/route.ts +9 -0
- package/src/app/api/setup/check-provider/route.ts +3 -16
- package/src/app/api/skills/[id]/route.ts +6 -0
- package/src/app/api/skills/route.ts +6 -0
- package/src/app/api/tasks/[id]/route.ts +12 -0
- package/src/app/api/tasks/bulk/route.ts +100 -0
- package/src/app/api/tasks/metrics/route.ts +101 -0
- package/src/app/api/tasks/route.ts +18 -2
- package/src/app/api/tts/route.ts +3 -2
- package/src/app/api/tts/stream/route.ts +3 -2
- package/src/app/api/uploads/[filename]/route.ts +19 -34
- package/src/app/api/uploads/route.ts +94 -0
- package/src/app/api/webhooks/[id]/route.ts +15 -1
- package/src/app/globals.css +63 -15
- package/src/app/page.tsx +142 -13
- package/src/cli/index.js +40 -1
- package/src/cli/index.test.js +30 -0
- package/src/cli/spec.js +42 -0
- package/src/components/agents/agent-avatar.tsx +57 -10
- package/src/components/agents/agent-card.tsx +50 -17
- package/src/components/agents/agent-chat-list.tsx +148 -12
- package/src/components/agents/agent-list.tsx +50 -19
- package/src/components/agents/agent-sheet.tsx +120 -65
- package/src/components/agents/inspector-panel.tsx +81 -6
- package/src/components/agents/openclaw-skills-panel.tsx +32 -3
- package/src/components/agents/personality-builder.tsx +42 -14
- package/src/components/agents/soul-library-picker.tsx +89 -0
- package/src/components/auth/access-key-gate.tsx +10 -3
- package/src/components/auth/setup-wizard.tsx +2 -2
- package/src/components/auth/user-picker.tsx +31 -3
- package/src/components/canvas/canvas-panel.tsx +96 -0
- package/src/components/chat/activity-moment.tsx +173 -0
- package/src/components/chat/chat-area.tsx +46 -22
- package/src/components/chat/chat-header.tsx +457 -286
- package/src/components/chat/chat-preview-panel.tsx +1 -2
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/delegation-banner.tsx +371 -0
- package/src/components/chat/file-path-chip.tsx +146 -0
- package/src/components/chat/heartbeat-history-panel.tsx +269 -0
- package/src/components/chat/markdown-utils.ts +9 -0
- package/src/components/chat/message-bubble.tsx +356 -315
- package/src/components/chat/message-list.tsx +230 -8
- package/src/components/chat/streaming-bubble.tsx +104 -47
- package/src/components/chat/suggestions-bar.tsx +1 -1
- package/src/components/chat/thinking-indicator.tsx +72 -10
- package/src/components/chat/tool-call-bubble.tsx +111 -73
- package/src/components/chat/tool-request-banner.tsx +31 -7
- package/src/components/chat/transfer-agent-picker.tsx +63 -0
- package/src/components/chatrooms/agent-hover-card.tsx +124 -0
- package/src/components/chatrooms/chatroom-input.tsx +320 -0
- package/src/components/chatrooms/chatroom-list.tsx +130 -0
- package/src/components/chatrooms/chatroom-message.tsx +432 -0
- package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
- package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
- package/src/components/chatrooms/chatroom-view.tsx +344 -0
- package/src/components/chatrooms/reaction-picker.tsx +273 -0
- package/src/components/connectors/connector-list.tsx +168 -90
- package/src/components/connectors/connector-sheet.tsx +95 -56
- package/src/components/home/home-view.tsx +501 -0
- package/src/components/input/chat-input.tsx +107 -43
- package/src/components/knowledge/knowledge-list.tsx +31 -1
- package/src/components/knowledge/knowledge-sheet.tsx +83 -2
- package/src/components/layout/app-layout.tsx +194 -97
- package/src/components/layout/update-banner.tsx +2 -2
- package/src/components/logs/log-list.tsx +2 -2
- package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
- package/src/components/memory/memory-agent-list.tsx +143 -0
- package/src/components/memory/memory-browser.tsx +205 -0
- package/src/components/memory/memory-card.tsx +34 -7
- package/src/components/memory/memory-detail.tsx +359 -120
- package/src/components/memory/memory-sheet.tsx +157 -23
- package/src/components/plugins/plugin-list.tsx +1 -1
- package/src/components/plugins/plugin-sheet.tsx +1 -1
- package/src/components/projects/project-detail.tsx +509 -0
- package/src/components/projects/project-list.tsx +195 -59
- package/src/components/providers/provider-list.tsx +2 -2
- package/src/components/providers/provider-sheet.tsx +3 -3
- package/src/components/schedules/schedule-card.tsx +1 -1
- package/src/components/schedules/schedule-list.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +259 -126
- package/src/components/secrets/secret-sheet.tsx +47 -24
- package/src/components/secrets/secrets-list.tsx +18 -8
- package/src/components/sessions/new-session-sheet.tsx +33 -65
- package/src/components/sessions/session-card.tsx +45 -14
- package/src/components/sessions/session-list.tsx +35 -18
- package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
- package/src/components/shared/agent-picker-list.tsx +90 -0
- package/src/components/shared/agent-switch-dialog.tsx +156 -0
- package/src/components/shared/attachment-chip.tsx +165 -0
- package/src/components/shared/avatar.tsx +10 -1
- package/src/components/shared/chatroom-picker-list.tsx +61 -0
- package/src/components/shared/check-icon.tsx +12 -0
- package/src/components/shared/confirm-dialog.tsx +1 -1
- package/src/components/shared/connector-platform-icon.tsx +51 -4
- package/src/components/shared/empty-state.tsx +32 -0
- package/src/components/shared/file-preview.tsx +34 -0
- package/src/components/shared/form-styles.ts +2 -0
- package/src/components/shared/icon-button.tsx +16 -2
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
- package/src/components/shared/notification-center.tsx +44 -6
- package/src/components/shared/profile-sheet.tsx +115 -0
- package/src/components/shared/reply-quote.tsx +26 -0
- package/src/components/shared/search-dialog.tsx +31 -15
- package/src/components/shared/section-label.tsx +12 -0
- package/src/components/shared/settings/plugin-manager.tsx +1 -1
- package/src/components/shared/settings/section-embedding.tsx +48 -13
- package/src/components/shared/settings/section-orchestrator.tsx +46 -15
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/section-secrets.tsx +1 -1
- package/src/components/shared/settings/section-storage.tsx +206 -0
- package/src/components/shared/settings/section-theme.tsx +95 -0
- package/src/components/shared/settings/section-user-preferences.tsx +57 -0
- package/src/components/shared/settings/section-voice.tsx +42 -21
- package/src/components/shared/settings/section-web-search.tsx +30 -6
- package/src/components/shared/settings/settings-page.tsx +182 -27
- package/src/components/shared/settings/settings-sheet.tsx +9 -73
- package/src/components/shared/settings/storage-browser.tsx +259 -0
- package/src/components/shared/sheet-footer.tsx +33 -0
- package/src/components/skills/skill-list.tsx +61 -30
- package/src/components/skills/skill-sheet.tsx +81 -2
- package/src/components/tasks/task-board.tsx +448 -26
- package/src/components/tasks/task-card.tsx +59 -9
- package/src/components/tasks/task-column.tsx +62 -3
- package/src/components/tasks/task-list.tsx +12 -4
- package/src/components/tasks/task-sheet.tsx +416 -74
- package/src/components/ui/hover-card.tsx +52 -0
- package/src/components/usage/metrics-dashboard.tsx +90 -6
- package/src/components/usage/usage-list.tsx +1 -1
- package/src/components/webhooks/webhook-sheet.tsx +1 -1
- package/src/hooks/use-continuous-speech.ts +10 -4
- package/src/hooks/use-view-router.ts +69 -19
- package/src/hooks/use-voice-conversation.ts +53 -10
- package/src/hooks/use-ws.ts +4 -2
- package/src/instrumentation.ts +15 -1
- package/src/lib/chat.ts +2 -0
- package/src/lib/memory.ts +3 -0
- package/src/lib/providers/anthropic.ts +13 -7
- package/src/lib/providers/index.ts +1 -0
- package/src/lib/providers/openai.ts +13 -7
- package/src/lib/server/chat-execution.ts +75 -15
- package/src/lib/server/chatroom-helpers.ts +146 -0
- package/src/lib/server/connectors/manager.ts +229 -7
- package/src/lib/server/context-manager.ts +225 -13
- package/src/lib/server/create-notification.ts +14 -2
- package/src/lib/server/daemon-state.ts +157 -10
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service.ts +48 -6
- package/src/lib/server/heartbeat-wake.ts +110 -0
- package/src/lib/server/langgraph-checkpoint.ts +1 -0
- package/src/lib/server/main-agent-loop.ts +1 -1
- package/src/lib/server/memory-consolidation.ts +105 -0
- package/src/lib/server/memory-db.ts +183 -10
- package/src/lib/server/mime.ts +51 -0
- package/src/lib/server/openclaw-gateway.ts +9 -1
- package/src/lib/server/orchestrator-lg.ts +2 -0
- package/src/lib/server/orchestrator.ts +5 -2
- package/src/lib/server/playwright-proxy.mjs +2 -3
- package/src/lib/server/prompt-runtime-context.ts +53 -0
- package/src/lib/server/provider-health.ts +125 -0
- package/src/lib/server/queue.ts +56 -10
- package/src/lib/server/scheduler.ts +8 -0
- package/src/lib/server/session-run-manager.ts +4 -0
- package/src/lib/server/session-tools/canvas.ts +67 -0
- package/src/lib/server/session-tools/chatroom.ts +136 -0
- package/src/lib/server/session-tools/connector.ts +83 -9
- package/src/lib/server/session-tools/context-mgmt.ts +36 -18
- package/src/lib/server/session-tools/crud.ts +21 -0
- package/src/lib/server/session-tools/delegate.ts +68 -4
- package/src/lib/server/session-tools/git.ts +71 -0
- package/src/lib/server/session-tools/http.ts +57 -0
- package/src/lib/server/session-tools/index.ts +10 -0
- package/src/lib/server/session-tools/memory.ts +7 -1
- package/src/lib/server/session-tools/search-providers.ts +16 -8
- package/src/lib/server/session-tools/subagent.ts +106 -0
- package/src/lib/server/session-tools/web.ts +115 -4
- package/src/lib/server/storage.ts +53 -29
- package/src/lib/server/stream-agent-chat.ts +185 -57
- package/src/lib/server/system-events.ts +49 -0
- package/src/lib/server/task-mention.ts +41 -0
- package/src/lib/server/ws-hub.ts +11 -0
- package/src/lib/sessions.ts +10 -0
- package/src/lib/soul-library.ts +103 -0
- package/src/lib/soul-suggestions.ts +109 -0
- package/src/lib/task-dedupe.ts +26 -0
- package/src/lib/tasks.ts +4 -1
- package/src/lib/tool-definitions.ts +2 -0
- package/src/lib/tts.ts +2 -2
- package/src/lib/view-routes.ts +36 -1
- package/src/lib/ws-client.ts +14 -4
- package/src/stores/use-app-store.ts +41 -3
- package/src/stores/use-chat-store.ts +113 -5
- package/src/stores/use-chatroom-store.ts +276 -0
- package/src/types/index.ts +88 -4
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { HoverCard as HoverCardPrimitive } from "radix-ui"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
|
|
8
|
+
function HoverCard({
|
|
9
|
+
openDelay = 300,
|
|
10
|
+
closeDelay = 150,
|
|
11
|
+
...props
|
|
12
|
+
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
|
|
13
|
+
return (
|
|
14
|
+
<HoverCardPrimitive.Root
|
|
15
|
+
data-slot="hover-card"
|
|
16
|
+
openDelay={openDelay}
|
|
17
|
+
closeDelay={closeDelay}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function HoverCardTrigger({
|
|
24
|
+
...props
|
|
25
|
+
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
|
|
26
|
+
return <HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function HoverCardContent({
|
|
30
|
+
className,
|
|
31
|
+
sideOffset = 8,
|
|
32
|
+
children,
|
|
33
|
+
...props
|
|
34
|
+
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
|
|
35
|
+
return (
|
|
36
|
+
<HoverCardPrimitive.Portal>
|
|
37
|
+
<HoverCardPrimitive.Content
|
|
38
|
+
data-slot="hover-card-content"
|
|
39
|
+
sideOffset={sideOffset}
|
|
40
|
+
className={cn(
|
|
41
|
+
"rounded-[12px] w-[260px] z-50 p-3 border border-white/[0.08] shadow-xl backdrop-blur-xl bg-[rgba(16,16,28,0.95)] animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-hover-card-content-transform-origin)",
|
|
42
|
+
className
|
|
43
|
+
)}
|
|
44
|
+
{...props}
|
|
45
|
+
>
|
|
46
|
+
{children}
|
|
47
|
+
</HoverCardPrimitive.Content>
|
|
48
|
+
</HoverCardPrimitive.Portal>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
|
@@ -57,6 +57,13 @@ function formatCost(n: number): string {
|
|
|
57
57
|
return `$${n.toFixed(4)}`
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
function formatDuration(ms: number): string {
|
|
61
|
+
if (!ms) return '—'
|
|
62
|
+
if (ms < 60_000) return `${Math.round(ms / 1000)}s`
|
|
63
|
+
if (ms < 3600_000) return `${Math.round(ms / 60_000)}m`
|
|
64
|
+
return `${(ms / 3600_000).toFixed(1)}h`
|
|
65
|
+
}
|
|
66
|
+
|
|
60
67
|
function formatBucketLabel(bucket: string, range: Range): string {
|
|
61
68
|
if (range === '24h') {
|
|
62
69
|
// "2026-03-01T14" → "14:00"
|
|
@@ -128,7 +135,24 @@ export function MetricsDashboard() {
|
|
|
128
135
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
129
136
|
}, [])
|
|
130
137
|
|
|
138
|
+
// --- Task metrics ---
|
|
139
|
+
const [taskMetrics, setTaskMetrics] = useState<{
|
|
140
|
+
wip: number; completedCount: number; avgCycleMs: number
|
|
141
|
+
velocity: { bucket: string; count: number }[]
|
|
142
|
+
byAgent: { agentName: string; completed: number; failed: number }[]
|
|
143
|
+
} | null>(null)
|
|
144
|
+
|
|
145
|
+
const loadTaskMetrics = useCallback(async () => {
|
|
146
|
+
try {
|
|
147
|
+
const res = await api<typeof taskMetrics>('GET', `/tasks/metrics?range=${range}`)
|
|
148
|
+
setTaskMetrics(res)
|
|
149
|
+
} catch { /* ignore */ }
|
|
150
|
+
}, [range])
|
|
151
|
+
|
|
152
|
+
useEffect(() => { loadTaskMetrics() }, [loadTaskMetrics])
|
|
153
|
+
|
|
131
154
|
useWs('usage', loadData, 30_000)
|
|
155
|
+
useWs('tasks', loadTaskMetrics, 15_000)
|
|
132
156
|
|
|
133
157
|
const completionRate = computeCompletionRate(tasks)
|
|
134
158
|
|
|
@@ -154,20 +178,20 @@ export function MetricsDashboard() {
|
|
|
154
178
|
|
|
155
179
|
const tooltipStyle = {
|
|
156
180
|
contentStyle: {
|
|
157
|
-
background: '
|
|
181
|
+
background: 'var(--color-surface)',
|
|
158
182
|
border: '1px solid rgba(255,255,255,0.08)',
|
|
159
183
|
borderRadius: 8,
|
|
160
184
|
fontSize: 12,
|
|
161
|
-
color: '
|
|
185
|
+
color: 'var(--color-text)',
|
|
162
186
|
},
|
|
163
|
-
itemStyle: { color: '
|
|
164
|
-
labelStyle: { color: '
|
|
187
|
+
itemStyle: { color: 'var(--color-text)' },
|
|
188
|
+
labelStyle: { color: 'var(--color-text-2)' },
|
|
165
189
|
}
|
|
166
190
|
|
|
167
191
|
return (
|
|
168
192
|
<div className="flex-1 flex flex-col h-full overflow-y-auto">
|
|
169
193
|
<div className="px-8 pt-6 pb-4 shrink-0">
|
|
170
|
-
<h1 className="font-display text-[28px] font-
|
|
194
|
+
<h1 className="font-display text-[28px] font-700 tracking-[-0.03em]">Usage</h1>
|
|
171
195
|
<p className="text-[13px] text-text-3 mt-1">Token usage, cost tracking & agent performance</p>
|
|
172
196
|
</div>
|
|
173
197
|
|
|
@@ -192,7 +216,10 @@ export function MetricsDashboard() {
|
|
|
192
216
|
|
|
193
217
|
{loading && !data ? (
|
|
194
218
|
<div className="flex-1 flex items-center justify-center">
|
|
195
|
-
<
|
|
219
|
+
<div className="flex items-center gap-3">
|
|
220
|
+
<span className="w-5 h-5 rounded-full border-2 border-text-3/20 border-t-accent-bright animate-spin" />
|
|
221
|
+
<span className="text-[14px] text-text-3">Loading metrics...</span>
|
|
222
|
+
</div>
|
|
196
223
|
</div>
|
|
197
224
|
) : (
|
|
198
225
|
<div className="px-8 pb-8 space-y-6">
|
|
@@ -276,6 +303,63 @@ export function MetricsDashboard() {
|
|
|
276
303
|
</ChartCard>
|
|
277
304
|
</div>
|
|
278
305
|
|
|
306
|
+
{/* Task KPIs */}
|
|
307
|
+
{taskMetrics && (
|
|
308
|
+
<>
|
|
309
|
+
<h3 className="font-display text-[16px] font-700 text-text mt-2">Task Performance</h3>
|
|
310
|
+
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
|
311
|
+
<StatCard label="Tasks Completed" value={String(taskMetrics.completedCount)} />
|
|
312
|
+
<StatCard label="Avg Cycle Time" value={formatDuration(taskMetrics.avgCycleMs)} />
|
|
313
|
+
<StatCard label="WIP" value={String(taskMetrics.wip)} />
|
|
314
|
+
<StatCard label="Completion Rate" value={`${completionRate}%`} />
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
318
|
+
<ChartCard title="Task Velocity">
|
|
319
|
+
{taskMetrics.velocity.length > 0 ? (
|
|
320
|
+
<ResponsiveContainer width="100%" height={280}>
|
|
321
|
+
<BarChart data={taskMetrics.velocity.map((v) => ({ ...v, label: formatBucketLabel(v.bucket, range) }))} margin={{ top: 5, right: 20, bottom: 5, left: 0 }}>
|
|
322
|
+
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.06)" />
|
|
323
|
+
<XAxis dataKey="label" tick={{ fill: '#888', fontSize: 11 }} axisLine={false} tickLine={false} />
|
|
324
|
+
<YAxis tick={{ fill: '#888', fontSize: 11 }} axisLine={false} tickLine={false} allowDecimals={false} />
|
|
325
|
+
<Tooltip {...tooltipStyle} formatter={(value: number | undefined) => [value ?? 0, 'Completed']} />
|
|
326
|
+
<Bar dataKey="count" fill="#34D399" radius={[4, 4, 0, 0]} />
|
|
327
|
+
</BarChart>
|
|
328
|
+
</ResponsiveContainer>
|
|
329
|
+
) : (
|
|
330
|
+
<EmptyChart />
|
|
331
|
+
)}
|
|
332
|
+
</ChartCard>
|
|
333
|
+
|
|
334
|
+
<ChartCard title="Tasks by Agent">
|
|
335
|
+
{taskMetrics.byAgent.length > 0 ? (
|
|
336
|
+
<ResponsiveContainer width="100%" height={280}>
|
|
337
|
+
<BarChart
|
|
338
|
+
data={taskMetrics.byAgent.slice(0, 8).map((a) => ({
|
|
339
|
+
name: a.agentName.length > 12 ? a.agentName.slice(0, 12) + '…' : a.agentName,
|
|
340
|
+
completed: a.completed,
|
|
341
|
+
failed: a.failed,
|
|
342
|
+
}))}
|
|
343
|
+
margin={{ top: 5, right: 20, bottom: 5, left: 0 }}
|
|
344
|
+
>
|
|
345
|
+
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.06)" />
|
|
346
|
+
<XAxis dataKey="name" tick={{ fill: '#888', fontSize: 11 }} axisLine={false} tickLine={false} />
|
|
347
|
+
<YAxis tick={{ fill: '#888', fontSize: 11 }} axisLine={false} tickLine={false} allowDecimals={false} />
|
|
348
|
+
<Tooltip {...tooltipStyle} />
|
|
349
|
+
<Bar dataKey="completed" fill="#34D399" radius={[4, 4, 0, 0]} stackId="a" name="Completed" />
|
|
350
|
+
<Bar dataKey="failed" fill="#F87171" radius={[4, 4, 0, 0]} stackId="a" name="Failed" />
|
|
351
|
+
<Legend verticalAlign="bottom" iconType="circle" iconSize={8}
|
|
352
|
+
formatter={(value: string) => <span style={{ color: '#a0a0b0', fontSize: 11 }}>{value}</span>} />
|
|
353
|
+
</BarChart>
|
|
354
|
+
</ResponsiveContainer>
|
|
355
|
+
) : (
|
|
356
|
+
<EmptyChart />
|
|
357
|
+
)}
|
|
358
|
+
</ChartCard>
|
|
359
|
+
</div>
|
|
360
|
+
</>
|
|
361
|
+
)}
|
|
362
|
+
|
|
279
363
|
{/* Provider Health */}
|
|
280
364
|
{data?.providerHealth && Object.keys(data.providerHealth).length > 0 && (
|
|
281
365
|
<div>
|
|
@@ -98,7 +98,7 @@ export function UsageList() {
|
|
|
98
98
|
</div>
|
|
99
99
|
<div className="mt-2 h-1 rounded-full bg-white/[0.04] overflow-hidden">
|
|
100
100
|
<div
|
|
101
|
-
className="h-full rounded-full bg-
|
|
101
|
+
className="h-full rounded-full bg-accent-bright/60"
|
|
102
102
|
style={{ width: `${Math.max(pct, 1)}%` }}
|
|
103
103
|
/>
|
|
104
104
|
</div>
|
|
@@ -389,7 +389,7 @@ export function WebhookSheet() {
|
|
|
389
389
|
<button
|
|
390
390
|
onClick={handleSave}
|
|
391
391
|
disabled={saving}
|
|
392
|
-
className="px-8 py-3 rounded-[14px] border-none bg-
|
|
392
|
+
className="px-8 py-3 rounded-[14px] border-none bg-accent-bright text-white text-[14px] font-600 cursor-pointer disabled:opacity-30 transition-all hover:brightness-110"
|
|
393
393
|
style={{ fontFamily: 'inherit' }}
|
|
394
394
|
>
|
|
395
395
|
{saving ? 'Saving...' : editing ? 'Update' : 'Create'}
|
|
@@ -48,7 +48,7 @@ interface UseContinuousSpeechOptions {
|
|
|
48
48
|
|
|
49
49
|
export function useContinuousSpeech(options: UseContinuousSpeechOptions) {
|
|
50
50
|
const { lang, silenceDelayMs = 800, onUtterance } = options
|
|
51
|
-
const [state,
|
|
51
|
+
const [state, _setState] = useState<ContinuousSpeechState>('idle')
|
|
52
52
|
const [transcript, setTranscript] = useState('')
|
|
53
53
|
const [interimText, setInterimText] = useState('')
|
|
54
54
|
|
|
@@ -56,6 +56,12 @@ export function useContinuousSpeech(options: UseContinuousSpeechOptions) {
|
|
|
56
56
|
const silenceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
57
57
|
const activeRef = useRef(false)
|
|
58
58
|
const accumulatedRef = useRef('')
|
|
59
|
+
const stateRef = useRef<ContinuousSpeechState>('idle')
|
|
60
|
+
|
|
61
|
+
const setState = useCallback((next: ContinuousSpeechState) => {
|
|
62
|
+
stateRef.current = next
|
|
63
|
+
_setState(next)
|
|
64
|
+
}, [])
|
|
59
65
|
|
|
60
66
|
const clearSilenceTimer = () => {
|
|
61
67
|
if (silenceTimerRef.current) {
|
|
@@ -122,7 +128,7 @@ export function useContinuousSpeech(options: UseContinuousSpeechOptions) {
|
|
|
122
128
|
|
|
123
129
|
recog.onend = () => {
|
|
124
130
|
// Auto-restart if still active (browser may stop recognition periodically)
|
|
125
|
-
if (activeRef.current &&
|
|
131
|
+
if (activeRef.current && stateRef.current !== 'waitingForResponse') {
|
|
126
132
|
try { recog.start() } catch { /* noop */ }
|
|
127
133
|
}
|
|
128
134
|
}
|
|
@@ -156,7 +162,7 @@ export function useContinuousSpeech(options: UseContinuousSpeechOptions) {
|
|
|
156
162
|
setTranscript('')
|
|
157
163
|
setInterimText('')
|
|
158
164
|
accumulatedRef.current = ''
|
|
159
|
-
}, [])
|
|
165
|
+
}, [setState])
|
|
160
166
|
|
|
161
167
|
const pause = useCallback(() => {
|
|
162
168
|
clearSilenceTimer()
|
|
@@ -172,7 +178,7 @@ export function useContinuousSpeech(options: UseContinuousSpeechOptions) {
|
|
|
172
178
|
setInterimText('')
|
|
173
179
|
setState('listening')
|
|
174
180
|
startRecognition()
|
|
175
|
-
}, [startRecognition])
|
|
181
|
+
}, [startRecognition, setState])
|
|
176
182
|
|
|
177
183
|
const supported = typeof window !== 'undefined' &&
|
|
178
184
|
!!((window as unknown as WindowWithSpeechRecognition).SpeechRecognition || (window as unknown as WindowWithSpeechRecognition).webkitSpeechRecognition)
|
|
@@ -2,48 +2,98 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useRef } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
-
import {
|
|
5
|
+
import { useChatroomStore } from '@/stores/use-chatroom-store'
|
|
6
|
+
import { parsePath, buildPath, DEFAULT_VIEW } from '@/lib/view-routes'
|
|
7
|
+
|
|
8
|
+
/** Map a view to the relevant entity ID from stores */
|
|
9
|
+
function getIdForView(view: string): string | null {
|
|
10
|
+
if (view === 'agents') return useAppStore.getState().currentAgentId
|
|
11
|
+
if (view === 'chatrooms') return useChatroomStore.getState().currentChatroomId
|
|
12
|
+
return null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Apply a parsed route to the stores */
|
|
16
|
+
function applyRoute(view: string, id: string | null) {
|
|
17
|
+
if (view === 'agents') useAppStore.getState().setCurrentAgent(id)
|
|
18
|
+
if (view === 'chatrooms') useChatroomStore.getState().setCurrentChatroom(id)
|
|
19
|
+
}
|
|
6
20
|
|
|
7
21
|
export function useViewRouter() {
|
|
8
22
|
const fromPopstate = useRef(false)
|
|
23
|
+
const suppressPush = useRef(false)
|
|
9
24
|
|
|
10
|
-
// Mount: read pathname → set active view
|
|
25
|
+
// Mount: read pathname → set active view + entity ID
|
|
11
26
|
useEffect(() => {
|
|
12
|
-
const
|
|
13
|
-
if (
|
|
14
|
-
|
|
27
|
+
const parsed = parsePath(window.location.pathname)
|
|
28
|
+
if (parsed) {
|
|
29
|
+
suppressPush.current = true
|
|
30
|
+
useAppStore.getState().setActiveView(parsed.view)
|
|
31
|
+
applyRoute(parsed.view, parsed.id)
|
|
32
|
+
suppressPush.current = false
|
|
15
33
|
} else {
|
|
16
34
|
useAppStore.getState().setActiveView(DEFAULT_VIEW)
|
|
17
|
-
window.history.replaceState(null, '',
|
|
35
|
+
window.history.replaceState(null, '', buildPath(DEFAULT_VIEW))
|
|
18
36
|
}
|
|
19
37
|
}, [])
|
|
20
38
|
|
|
21
|
-
// State→URL: push new path when activeView changes
|
|
39
|
+
// State→URL: push new path when activeView or entity ID changes
|
|
22
40
|
useEffect(() => {
|
|
23
|
-
let
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
41
|
+
let prevView = useAppStore.getState().activeView
|
|
42
|
+
let prevId = getIdForView(prevView)
|
|
43
|
+
|
|
44
|
+
const unsubApp = useAppStore.subscribe((state) => {
|
|
45
|
+
if (suppressPush.current) return
|
|
46
|
+
const nextView = state.activeView
|
|
47
|
+
const nextId = getIdForView(nextView)
|
|
48
|
+
|
|
49
|
+
if (nextView === prevView && nextId === prevId) return
|
|
50
|
+
prevView = nextView
|
|
51
|
+
prevId = nextId
|
|
52
|
+
|
|
53
|
+
if (fromPopstate.current) {
|
|
54
|
+
fromPopstate.current = false
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
const targetPath = buildPath(nextView, nextId)
|
|
58
|
+
if (window.location.pathname !== targetPath) {
|
|
59
|
+
window.history.pushState(null, '', targetPath)
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const unsubChatroom = useChatroomStore.subscribe((state) => {
|
|
64
|
+
if (suppressPush.current) return
|
|
65
|
+
const currentView = useAppStore.getState().activeView
|
|
66
|
+
if (currentView !== 'chatrooms') return
|
|
67
|
+
const nextId = state.currentChatroomId
|
|
68
|
+
if (nextId === prevId) return
|
|
69
|
+
prevId = nextId
|
|
70
|
+
|
|
28
71
|
if (fromPopstate.current) {
|
|
29
72
|
fromPopstate.current = false
|
|
30
73
|
return
|
|
31
74
|
}
|
|
32
|
-
const targetPath =
|
|
33
|
-
if (
|
|
75
|
+
const targetPath = buildPath('chatrooms', nextId)
|
|
76
|
+
if (window.location.pathname !== targetPath) {
|
|
34
77
|
window.history.pushState(null, '', targetPath)
|
|
35
78
|
}
|
|
36
79
|
})
|
|
37
|
-
|
|
80
|
+
|
|
81
|
+
return () => {
|
|
82
|
+
unsubApp()
|
|
83
|
+
unsubChatroom()
|
|
84
|
+
}
|
|
38
85
|
}, [])
|
|
39
86
|
|
|
40
|
-
// Popstate: browser back/forward → update view
|
|
87
|
+
// Popstate: browser back/forward → update view + entity ID
|
|
41
88
|
useEffect(() => {
|
|
42
89
|
const onPopstate = () => {
|
|
43
|
-
const
|
|
44
|
-
if (
|
|
90
|
+
const parsed = parsePath(window.location.pathname)
|
|
91
|
+
if (parsed) {
|
|
45
92
|
fromPopstate.current = true
|
|
46
|
-
|
|
93
|
+
suppressPush.current = true
|
|
94
|
+
useAppStore.getState().setActiveView(parsed.view)
|
|
95
|
+
applyRoute(parsed.view, parsed.id)
|
|
96
|
+
suppressPush.current = false
|
|
47
97
|
}
|
|
48
98
|
}
|
|
49
99
|
window.addEventListener('popstate', onPopstate)
|
|
@@ -1,40 +1,68 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useCallback, useRef, useState } from 'react'
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
4
4
|
import { useContinuousSpeech } from './use-continuous-speech'
|
|
5
5
|
import { SentenceAccumulator, AudioChunkQueue, fetchStreamTts } from '@/lib/tts-stream'
|
|
6
6
|
import { useChatStore } from '@/stores/use-chat-store'
|
|
7
7
|
|
|
8
8
|
export type VoiceConversationState = 'idle' | 'listening' | 'processing' | 'speaking'
|
|
9
9
|
|
|
10
|
+
/** Max time to wait in 'processing' before falling back to listening (30s). */
|
|
11
|
+
const PROCESSING_TIMEOUT_MS = 30_000
|
|
12
|
+
|
|
10
13
|
export function useVoiceConversation() {
|
|
11
|
-
const [active, setActive] = useState(false)
|
|
12
14
|
const [voiceState, setVoiceState] = useState<VoiceConversationState>('idle')
|
|
13
15
|
const accumulatorRef = useRef<SentenceAccumulator | null>(null)
|
|
14
16
|
const queueRef = useRef<AudioChunkQueue | null>(null)
|
|
17
|
+
const activeRef = useRef(false)
|
|
18
|
+
const processingTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
19
|
+
const [resumeNeeded, setResumeNeeded] = useState(0)
|
|
15
20
|
const sendMessage = useChatStore((s) => s.sendMessage)
|
|
16
21
|
|
|
22
|
+
const clearProcessingTimer = () => {
|
|
23
|
+
if (processingTimerRef.current) {
|
|
24
|
+
clearTimeout(processingTimerRef.current)
|
|
25
|
+
processingTimerRef.current = null
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
17
29
|
const speech = useContinuousSpeech({
|
|
18
30
|
onUtterance: useCallback((text: string) => {
|
|
19
31
|
setVoiceState('processing')
|
|
20
|
-
// Send the transcribed text as a chat message
|
|
21
32
|
sendMessage(text)
|
|
33
|
+
// Safety net: if no stream events arrive within timeout, resume listening
|
|
34
|
+
clearProcessingTimer()
|
|
35
|
+
processingTimerRef.current = setTimeout(() => {
|
|
36
|
+
if (activeRef.current) {
|
|
37
|
+
setVoiceState('listening')
|
|
38
|
+
setResumeNeeded((n) => n + 1)
|
|
39
|
+
}
|
|
40
|
+
}, PROCESSING_TIMEOUT_MS)
|
|
22
41
|
}, [sendMessage]),
|
|
23
42
|
})
|
|
24
43
|
|
|
44
|
+
// When resumeNeeded increments, call speech.resume
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (resumeNeeded > 0) speech.resume()
|
|
47
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
48
|
+
}, [resumeNeeded])
|
|
49
|
+
|
|
25
50
|
// Called by the chat store's onStreamEvent callback
|
|
26
51
|
const handleStreamEvent = useCallback((event: { t: string; text?: string }) => {
|
|
27
|
-
if (!
|
|
52
|
+
if (!activeRef.current) return
|
|
28
53
|
|
|
29
54
|
if (event.t === 'd' && event.text) {
|
|
55
|
+
clearProcessingTimer()
|
|
30
56
|
setVoiceState('speaking')
|
|
31
57
|
if (!accumulatorRef.current) {
|
|
32
58
|
const queue = new AudioChunkQueue()
|
|
33
59
|
queueRef.current = queue
|
|
34
60
|
queue.onComplete = () => {
|
|
35
61
|
// Resume listening after TTS playback finishes
|
|
36
|
-
|
|
37
|
-
|
|
62
|
+
if (activeRef.current) {
|
|
63
|
+
setVoiceState('listening')
|
|
64
|
+
speech.resume()
|
|
65
|
+
}
|
|
38
66
|
}
|
|
39
67
|
accumulatorRef.current = new SentenceAccumulator((sentence) => {
|
|
40
68
|
queue.enqueue(fetchStreamTts(sentence))
|
|
@@ -42,16 +70,30 @@ export function useVoiceConversation() {
|
|
|
42
70
|
}
|
|
43
71
|
accumulatorRef.current.push(event.text)
|
|
44
72
|
} else if (event.t === 'done') {
|
|
73
|
+
clearProcessingTimer()
|
|
45
74
|
// Flush remaining text to TTS
|
|
46
75
|
if (accumulatorRef.current) {
|
|
47
76
|
accumulatorRef.current.flush()
|
|
48
77
|
accumulatorRef.current = null
|
|
78
|
+
} else {
|
|
79
|
+
// No text was streamed (empty response or error) — resume listening
|
|
80
|
+
if (activeRef.current) {
|
|
81
|
+
setVoiceState('listening')
|
|
82
|
+
speech.resume()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} else if (event.t === 'err') {
|
|
86
|
+
// Error from the LLM — resume listening instead of staying stuck
|
|
87
|
+
clearProcessingTimer()
|
|
88
|
+
if (activeRef.current) {
|
|
89
|
+
setVoiceState('listening')
|
|
90
|
+
speech.resume()
|
|
49
91
|
}
|
|
50
92
|
}
|
|
51
|
-
}, [
|
|
93
|
+
}, [speech])
|
|
52
94
|
|
|
53
95
|
const start = useCallback(() => {
|
|
54
|
-
|
|
96
|
+
activeRef.current = true
|
|
55
97
|
setVoiceState('listening')
|
|
56
98
|
// Register the stream event handler on the chat store
|
|
57
99
|
useChatStore.setState({ onStreamEvent: handleStreamEvent, voiceConversationActive: true })
|
|
@@ -59,8 +101,9 @@ export function useVoiceConversation() {
|
|
|
59
101
|
}, [speech, handleStreamEvent])
|
|
60
102
|
|
|
61
103
|
const stop = useCallback(() => {
|
|
62
|
-
|
|
104
|
+
activeRef.current = false
|
|
63
105
|
setVoiceState('idle')
|
|
106
|
+
clearProcessingTimer()
|
|
64
107
|
speech.stop()
|
|
65
108
|
queueRef.current?.stop()
|
|
66
109
|
queueRef.current = null
|
|
@@ -69,7 +112,7 @@ export function useVoiceConversation() {
|
|
|
69
112
|
}, [speech])
|
|
70
113
|
|
|
71
114
|
return {
|
|
72
|
-
active,
|
|
115
|
+
active: activeRef.current || voiceState !== 'idle',
|
|
73
116
|
state: voiceState,
|
|
74
117
|
interimText: speech.interimText,
|
|
75
118
|
transcript: speech.transcript,
|
package/src/hooks/use-ws.ts
CHANGED
|
@@ -32,8 +32,10 @@ export function useWs(topic: string, handler: () => void, fallbackMs?: number) {
|
|
|
32
32
|
let fallbackId: ReturnType<typeof setInterval> | null = null
|
|
33
33
|
const cb = () => handlerRef.current()
|
|
34
34
|
|
|
35
|
-
// When page becomes visible again, fire an immediate refresh
|
|
36
|
-
|
|
35
|
+
// When page becomes visible again, fire an immediate refresh —
|
|
36
|
+
// but only for topics that use fallback polling (i.e. data-fetch topics).
|
|
37
|
+
// Event-only topics (like heartbeat pulses) should never fire from this effect.
|
|
38
|
+
if (isActive && fallbackMsRef.current && fallbackMsRef.current > 0) {
|
|
37
39
|
cb()
|
|
38
40
|
}
|
|
39
41
|
|
package/src/instrumentation.ts
CHANGED
|
@@ -2,9 +2,23 @@ export async function register() {
|
|
|
2
2
|
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
|
3
3
|
const { startScheduler } = await import('./lib/server/scheduler')
|
|
4
4
|
const { resumeQueue } = await import('./lib/server/queue')
|
|
5
|
-
const { initWsServer } = await import('./lib/server/ws-hub')
|
|
5
|
+
const { initWsServer, closeWsServer } = await import('./lib/server/ws-hub')
|
|
6
|
+
const { stopDaemon } = await import('./lib/server/daemon-state')
|
|
6
7
|
startScheduler()
|
|
7
8
|
resumeQueue()
|
|
8
9
|
initWsServer()
|
|
10
|
+
|
|
11
|
+
// Graceful shutdown: stop background services and close WS connections
|
|
12
|
+
let shuttingDown = false
|
|
13
|
+
const shutdown = async (signal: string) => {
|
|
14
|
+
if (shuttingDown) return
|
|
15
|
+
shuttingDown = true
|
|
16
|
+
console.log(`[server] ${signal} received, shutting down gracefully...`)
|
|
17
|
+
stopDaemon({ source: signal })
|
|
18
|
+
await closeWsServer()
|
|
19
|
+
process.exit(0)
|
|
20
|
+
}
|
|
21
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'))
|
|
22
|
+
process.on('SIGINT', () => shutdown('SIGINT'))
|
|
9
23
|
}
|
|
10
24
|
}
|
package/src/lib/chat.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { getStoredAccessKey } from './api-client'
|
|
|
4
4
|
interface StreamChatOptions {
|
|
5
5
|
internal?: boolean
|
|
6
6
|
queueMode?: 'followup' | 'steer' | 'collect'
|
|
7
|
+
replyToId?: string
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
export async function streamChat(
|
|
@@ -39,6 +40,7 @@ export async function streamChat(
|
|
|
39
40
|
attachedFiles,
|
|
40
41
|
internal: !!opts?.internal,
|
|
41
42
|
queueMode: opts?.queueMode,
|
|
43
|
+
...(opts?.replyToId ? { replyToId: opts.replyToId } : {}),
|
|
42
44
|
}),
|
|
43
45
|
})
|
|
44
46
|
|
package/src/lib/memory.ts
CHANGED
|
@@ -40,3 +40,6 @@ export const updateMemory = (id: string, data: Partial<MemoryEntry>) =>
|
|
|
40
40
|
|
|
41
41
|
export const deleteMemory = (id: string) =>
|
|
42
42
|
api<string>('DELETE', `/memory/${id}`)
|
|
43
|
+
|
|
44
|
+
export const getMemoryCounts = () =>
|
|
45
|
+
api<Record<string, number>>('GET', '/memory?counts=true')
|
|
@@ -23,9 +23,9 @@ function fileToContentBlocks(filePath: string): any[] {
|
|
|
23
23
|
return [{ type: 'text', text: `[Attached file: ${filePath.split('/').pop()}]` }]
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
export function streamAnthropicChat({ session, message, imagePath, apiKey, systemPrompt, write, active, loadHistory, onUsage }: StreamChatOptions): Promise<string> {
|
|
26
|
+
export function streamAnthropicChat({ session, message, imagePath, imageUrl, apiKey, systemPrompt, write, active, loadHistory, onUsage }: StreamChatOptions): Promise<string> {
|
|
27
27
|
return new Promise((resolve) => {
|
|
28
|
-
const messages = buildMessages(session, message, imagePath, loadHistory)
|
|
28
|
+
const messages = buildMessages(session, message, imagePath, loadHistory, imageUrl)
|
|
29
29
|
const model = session.model || 'claude-sonnet-4-6'
|
|
30
30
|
let usageInput = 0
|
|
31
31
|
let usageOutput = 0
|
|
@@ -122,14 +122,19 @@ export function streamAnthropicChat({ session, message, imagePath, apiKey, syste
|
|
|
122
122
|
})
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
function
|
|
125
|
+
function urlToImageBlock(url: string): { type: string; source: { type: string; url: string } } {
|
|
126
|
+
return { type: 'image', source: { type: 'url', url } }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function buildMessages(session: any, message: string, imagePath: string | undefined, loadHistory: (id: string) => any[], imageUrl?: string) {
|
|
126
130
|
const msgs: Array<{ role: string; content: any }> = []
|
|
127
131
|
|
|
128
132
|
if (loadHistory) {
|
|
129
133
|
const history = loadHistory(session.id).slice(-40)
|
|
130
134
|
for (const m of history) {
|
|
131
|
-
if (m.role === 'user' && m.imagePath) {
|
|
132
|
-
const blocks = fileToContentBlocks(m.imagePath)
|
|
135
|
+
if (m.role === 'user' && (m.imagePath || m.imageUrl)) {
|
|
136
|
+
const blocks = m.imagePath ? fileToContentBlocks(m.imagePath) : []
|
|
137
|
+
if (m.imageUrl) blocks.push(urlToImageBlock(m.imageUrl))
|
|
133
138
|
msgs.push({ role: 'user', content: [...blocks, { type: 'text', text: m.text }] })
|
|
134
139
|
} else {
|
|
135
140
|
msgs.push({ role: m.role, content: m.text })
|
|
@@ -138,8 +143,9 @@ function buildMessages(session: any, message: string, imagePath: string | undefi
|
|
|
138
143
|
}
|
|
139
144
|
|
|
140
145
|
// Current message with optional attachment
|
|
141
|
-
if (imagePath) {
|
|
142
|
-
const blocks = fileToContentBlocks(imagePath)
|
|
146
|
+
if (imagePath || imageUrl) {
|
|
147
|
+
const blocks = imagePath ? fileToContentBlocks(imagePath) : []
|
|
148
|
+
if (imageUrl) blocks.push(urlToImageBlock(imageUrl))
|
|
143
149
|
msgs.push({ role: 'user', content: [...blocks, { type: 'text', text: message }] })
|
|
144
150
|
} else {
|
|
145
151
|
msgs.push({ role: 'user', content: message })
|