@swarmclawai/swarmclaw 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +577 -0
- package/bin/server-cmd.js +359 -0
- package/bin/swarmclaw.js +29 -0
- package/bin/swarmclaw.mjs +1504 -0
- package/next.config.ts +33 -0
- package/package.json +112 -0
- package/postcss.config.mjs +7 -0
- package/public/branding/swarmclaw-org-avatar.png +0 -0
- package/public/branding/swarmclaw-org-avatar.svg +58 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/screenshots/agents.png +0 -0
- package/public/screenshots/connectors.png +0 -0
- package/public/screenshots/dashboard.png +0 -0
- package/public/screenshots/new-session-openclaw.png +0 -0
- package/public/screenshots/providers.png +0 -0
- package/public/screenshots/schedules.png +0 -0
- package/public/screenshots/tasks.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/app/api/agents/[id]/route.ts +30 -0
- package/src/app/api/agents/[id]/thread/route.ts +66 -0
- package/src/app/api/agents/generate/route.ts +42 -0
- package/src/app/api/agents/route.ts +33 -0
- package/src/app/api/auth/route.ts +25 -0
- package/src/app/api/claude-skills/route.ts +42 -0
- package/src/app/api/clawhub/install/route.ts +39 -0
- package/src/app/api/clawhub/search/route.ts +11 -0
- package/src/app/api/connectors/[id]/route.ts +79 -0
- package/src/app/api/connectors/route.ts +60 -0
- package/src/app/api/credentials/[id]/route.ts +14 -0
- package/src/app/api/credentials/route.ts +31 -0
- package/src/app/api/daemon/health-check/route.ts +11 -0
- package/src/app/api/daemon/route.ts +22 -0
- package/src/app/api/dirs/pick/route.ts +60 -0
- package/src/app/api/dirs/route.ts +29 -0
- package/src/app/api/documents/[id]/route.ts +47 -0
- package/src/app/api/documents/route.ts +93 -0
- package/src/app/api/files/serve/route.ts +69 -0
- package/src/app/api/generate/info/route.ts +12 -0
- package/src/app/api/generate/route.ts +106 -0
- package/src/app/api/ip/route.ts +6 -0
- package/src/app/api/knowledge/[id]/route.ts +61 -0
- package/src/app/api/knowledge/route.ts +48 -0
- package/src/app/api/knowledge/upload/route.ts +86 -0
- package/src/app/api/logs/route.ts +65 -0
- package/src/app/api/mcp-servers/[id]/route.ts +32 -0
- package/src/app/api/mcp-servers/[id]/test/route.ts +23 -0
- package/src/app/api/mcp-servers/[id]/tools/route.ts +32 -0
- package/src/app/api/mcp-servers/route.ts +27 -0
- package/src/app/api/memory/[id]/route.ts +126 -0
- package/src/app/api/memory/maintenance/route.ts +63 -0
- package/src/app/api/memory/route.ts +111 -0
- package/src/app/api/memory-images/[filename]/route.ts +36 -0
- package/src/app/api/orchestrator/run/route.ts +43 -0
- package/src/app/api/plugins/install/route.ts +58 -0
- package/src/app/api/plugins/marketplace/route.ts +33 -0
- package/src/app/api/plugins/route.ts +21 -0
- package/src/app/api/preview-server/route.ts +339 -0
- package/src/app/api/providers/[id]/models/route.ts +29 -0
- package/src/app/api/providers/[id]/route.ts +34 -0
- package/src/app/api/providers/configs/route.ts +7 -0
- package/src/app/api/providers/ollama/route.ts +30 -0
- package/src/app/api/providers/openclaw/health/route.ts +23 -0
- package/src/app/api/providers/route.ts +28 -0
- package/src/app/api/runs/[id]/route.ts +9 -0
- package/src/app/api/runs/route.ts +13 -0
- package/src/app/api/schedules/[id]/route.ts +28 -0
- package/src/app/api/schedules/[id]/run/route.ts +104 -0
- package/src/app/api/schedules/route.ts +78 -0
- package/src/app/api/secrets/[id]/route.ts +29 -0
- package/src/app/api/secrets/route.ts +42 -0
- package/src/app/api/sessions/[id]/browser/route.ts +13 -0
- package/src/app/api/sessions/[id]/chat/route.ts +96 -0
- package/src/app/api/sessions/[id]/clear/route.ts +19 -0
- package/src/app/api/sessions/[id]/deploy/route.ts +34 -0
- package/src/app/api/sessions/[id]/devserver/route.ts +69 -0
- package/src/app/api/sessions/[id]/mailbox/route.ts +70 -0
- package/src/app/api/sessions/[id]/main-loop/route.ts +94 -0
- package/src/app/api/sessions/[id]/messages/route.ts +9 -0
- package/src/app/api/sessions/[id]/retry/route.ts +28 -0
- package/src/app/api/sessions/[id]/route.ts +103 -0
- package/src/app/api/sessions/[id]/stop/route.ts +13 -0
- package/src/app/api/sessions/heartbeat/route.ts +26 -0
- package/src/app/api/sessions/route.ts +85 -0
- package/src/app/api/settings/route.ts +58 -0
- package/src/app/api/setup/check-provider/route.ts +326 -0
- package/src/app/api/setup/doctor/route.ts +250 -0
- package/src/app/api/skills/[id]/route.ts +40 -0
- package/src/app/api/skills/import/route.ts +69 -0
- package/src/app/api/skills/route.ts +28 -0
- package/src/app/api/tasks/[id]/route.ts +102 -0
- package/src/app/api/tasks/route.ts +115 -0
- package/src/app/api/tts/route.ts +40 -0
- package/src/app/api/upload/route.ts +18 -0
- package/src/app/api/uploads/[filename]/route.ts +59 -0
- package/src/app/api/usage/route.ts +35 -0
- package/src/app/api/version/route.ts +81 -0
- package/src/app/api/version/update/route.ts +95 -0
- package/src/app/api/webhooks/[id]/history/route.ts +13 -0
- package/src/app/api/webhooks/[id]/route.ts +204 -0
- package/src/app/api/webhooks/route.ts +37 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +370 -0
- package/src/app/layout.tsx +52 -0
- package/src/app/page.tsx +172 -0
- package/src/cli/index.js +1232 -0
- package/src/cli/index.test.js +281 -0
- package/src/cli/index.ts +1158 -0
- package/src/cli/spec.js +284 -0
- package/src/components/agents/agent-card.tsx +219 -0
- package/src/components/agents/agent-chat-list.tsx +165 -0
- package/src/components/agents/agent-list.tsx +110 -0
- package/src/components/agents/agent-sheet.tsx +1220 -0
- package/src/components/auth/access-key-gate.tsx +248 -0
- package/src/components/auth/setup-wizard.tsx +940 -0
- package/src/components/auth/user-picker.tsx +88 -0
- package/src/components/chat/chat-area.tsx +406 -0
- package/src/components/chat/chat-header.tsx +491 -0
- package/src/components/chat/chat-tool-toggles.tsx +161 -0
- package/src/components/chat/code-block.tsx +146 -0
- package/src/components/chat/dev-server-bar.tsx +39 -0
- package/src/components/chat/message-bubble.tsx +486 -0
- package/src/components/chat/message-list.tsx +299 -0
- package/src/components/chat/session-debug-panel.tsx +196 -0
- package/src/components/chat/streaming-bubble.tsx +85 -0
- package/src/components/chat/thinking-indicator.tsx +26 -0
- package/src/components/chat/tool-call-bubble.tsx +438 -0
- package/src/components/chat/tool-request-banner.tsx +103 -0
- package/src/components/connectors/connector-list.tsx +196 -0
- package/src/components/connectors/connector-sheet.tsx +804 -0
- package/src/components/input/chat-input.tsx +235 -0
- package/src/components/knowledge/knowledge-list.tsx +206 -0
- package/src/components/knowledge/knowledge-sheet.tsx +316 -0
- package/src/components/layout/app-layout.tsx +1016 -0
- package/src/components/layout/daemon-indicator.tsx +56 -0
- package/src/components/layout/mobile-header.tsx +31 -0
- package/src/components/layout/network-banner.tsx +17 -0
- package/src/components/layout/update-banner.tsx +130 -0
- package/src/components/logs/log-list.tsx +358 -0
- package/src/components/mcp-servers/mcp-server-list.tsx +122 -0
- package/src/components/mcp-servers/mcp-server-sheet.tsx +243 -0
- package/src/components/memory/memory-card.tsx +63 -0
- package/src/components/memory/memory-detail.tsx +339 -0
- package/src/components/memory/memory-list.tsx +198 -0
- package/src/components/memory/memory-sheet.tsx +70 -0
- package/src/components/plugins/plugin-list.tsx +60 -0
- package/src/components/plugins/plugin-sheet.tsx +311 -0
- package/src/components/providers/provider-list.tsx +96 -0
- package/src/components/providers/provider-sheet.tsx +542 -0
- package/src/components/runs/run-list.tsx +231 -0
- package/src/components/schedules/schedule-card.tsx +63 -0
- package/src/components/schedules/schedule-list.tsx +76 -0
- package/src/components/schedules/schedule-sheet.tsx +336 -0
- package/src/components/secrets/secret-sheet.tsx +180 -0
- package/src/components/secrets/secrets-list.tsx +91 -0
- package/src/components/sessions/new-session-sheet.tsx +478 -0
- package/src/components/sessions/session-card.tsx +144 -0
- package/src/components/sessions/session-list.tsx +202 -0
- package/src/components/shared/ai-gen-block.tsx +77 -0
- package/src/components/shared/avatar.tsx +48 -0
- package/src/components/shared/bottom-sheet.tsx +30 -0
- package/src/components/shared/confirm-dialog.tsx +47 -0
- package/src/components/shared/connector-platform-icon.tsx +113 -0
- package/src/components/shared/dir-browser.tsx +285 -0
- package/src/components/shared/dropdown.tsx +55 -0
- package/src/components/shared/icon-button.tsx +25 -0
- package/src/components/shared/settings/plugin-manager.tsx +207 -0
- package/src/components/shared/settings/section-capability-policy.tsx +93 -0
- package/src/components/shared/settings/section-embedding.tsx +99 -0
- package/src/components/shared/settings/section-heartbeat.tsx +168 -0
- package/src/components/shared/settings/section-memory.tsx +77 -0
- package/src/components/shared/settings/section-orchestrator.tsx +108 -0
- package/src/components/shared/settings/section-providers.tsx +181 -0
- package/src/components/shared/settings/section-runtime-loop.tsx +183 -0
- package/src/components/shared/settings/section-secrets.tsx +132 -0
- package/src/components/shared/settings/section-user-preferences.tsx +24 -0
- package/src/components/shared/settings/section-voice.tsx +53 -0
- package/src/components/shared/settings/settings-sheet.tsx +88 -0
- package/src/components/shared/settings/types.ts +7 -0
- package/src/components/shared/settings/utils.ts +13 -0
- package/src/components/shared/settings-sheet.tsx +1 -0
- package/src/components/shared/skeleton.tsx +19 -0
- package/src/components/shared/usage-badge.tsx +28 -0
- package/src/components/skills/clawhub-browser.tsx +225 -0
- package/src/components/skills/skill-list.tsx +70 -0
- package/src/components/skills/skill-sheet.tsx +254 -0
- package/src/components/tasks/task-board.tsx +96 -0
- package/src/components/tasks/task-card.tsx +179 -0
- package/src/components/tasks/task-column.tsx +73 -0
- package/src/components/tasks/task-list.tsx +118 -0
- package/src/components/tasks/task-sheet.tsx +415 -0
- package/src/components/ui/avatar.tsx +109 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/button.tsx +64 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/dialog.tsx +158 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/scroll-area.tsx +58 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +143 -0
- package/src/components/ui/sonner.tsx +22 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +56 -0
- package/src/components/usage/usage-list.tsx +105 -0
- package/src/components/webhooks/webhook-list.tsx +166 -0
- package/src/components/webhooks/webhook-sheet.tsx +402 -0
- package/src/hooks/use-auto-resize.ts +20 -0
- package/src/hooks/use-media-query.ts +21 -0
- package/src/hooks/use-speech-recognition.ts +83 -0
- package/src/instrumentation.ts +8 -0
- package/src/lib/agents.ts +13 -0
- package/src/lib/api-client.ts +100 -0
- package/src/lib/chat.ts +60 -0
- package/src/lib/memory.ts +42 -0
- package/src/lib/openclaw-endpoint.test.ts +48 -0
- package/src/lib/openclaw-endpoint.ts +67 -0
- package/src/lib/provider-config.ts +13 -0
- package/src/lib/providers/anthropic.ts +135 -0
- package/src/lib/providers/claude-cli.ts +202 -0
- package/src/lib/providers/codex-cli.ts +260 -0
- package/src/lib/providers/index.ts +351 -0
- package/src/lib/providers/ollama.ts +131 -0
- package/src/lib/providers/openai.ts +164 -0
- package/src/lib/providers/openclaw.ts +330 -0
- package/src/lib/providers/opencode-cli.ts +164 -0
- package/src/lib/runtime-loop.ts +15 -0
- package/src/lib/schedule-dedupe.test.ts +84 -0
- package/src/lib/schedule-dedupe.ts +174 -0
- package/src/lib/schedule-name.ts +62 -0
- package/src/lib/schedules.ts +16 -0
- package/src/lib/server/agent-registry.ts +70 -0
- package/src/lib/server/api-routes.test.ts +362 -0
- package/src/lib/server/autonomy-contract.ts +200 -0
- package/src/lib/server/build-llm.ts +155 -0
- package/src/lib/server/capability-router.test.ts +21 -0
- package/src/lib/server/capability-router.ts +172 -0
- package/src/lib/server/chat-execution.ts +894 -0
- package/src/lib/server/clawhub-client.test.ts +161 -0
- package/src/lib/server/clawhub-client.ts +26 -0
- package/src/lib/server/connectors/connector-routing.test.ts +243 -0
- package/src/lib/server/connectors/discord.ts +116 -0
- package/src/lib/server/connectors/googlechat.ts +66 -0
- package/src/lib/server/connectors/manager.ts +559 -0
- package/src/lib/server/connectors/matrix.ts +78 -0
- package/src/lib/server/connectors/media.ts +149 -0
- package/src/lib/server/connectors/openclaw.test.ts +375 -0
- package/src/lib/server/connectors/openclaw.ts +1132 -0
- package/src/lib/server/connectors/signal.ts +183 -0
- package/src/lib/server/connectors/slack.ts +258 -0
- package/src/lib/server/connectors/teams.ts +94 -0
- package/src/lib/server/connectors/telegram.ts +221 -0
- package/src/lib/server/connectors/types.ts +62 -0
- package/src/lib/server/connectors/whatsapp.ts +349 -0
- package/src/lib/server/context-manager.ts +232 -0
- package/src/lib/server/cost.ts +31 -0
- package/src/lib/server/daemon-state.ts +354 -0
- package/src/lib/server/data-dir.ts +3 -0
- package/src/lib/server/embeddings.ts +111 -0
- package/src/lib/server/execution-log.ts +257 -0
- package/src/lib/server/gateway/protocol.test.ts +54 -0
- package/src/lib/server/gateway/protocol.ts +114 -0
- package/src/lib/server/heartbeat-service.ts +366 -0
- package/src/lib/server/knowledge-db.test.ts +441 -0
- package/src/lib/server/logger.ts +47 -0
- package/src/lib/server/main-agent-loop.ts +1017 -0
- package/src/lib/server/mcp-client.test.ts +342 -0
- package/src/lib/server/mcp-client.ts +130 -0
- package/src/lib/server/memory-db.ts +1078 -0
- package/src/lib/server/memory-graph.test.ts +153 -0
- package/src/lib/server/memory-graph.ts +138 -0
- package/src/lib/server/openclaw-health.ts +245 -0
- package/src/lib/server/orchestrator-lg.ts +431 -0
- package/src/lib/server/orchestrator.ts +364 -0
- package/src/lib/server/playwright-proxy.mjs +70 -0
- package/src/lib/server/plugins.ts +229 -0
- package/src/lib/server/process-manager.ts +327 -0
- package/src/lib/server/provider-health.ts +113 -0
- package/src/lib/server/queue.ts +859 -0
- package/src/lib/server/runtime-settings.ts +119 -0
- package/src/lib/server/scheduler.ts +196 -0
- package/src/lib/server/session-mailbox.ts +129 -0
- package/src/lib/server/session-run-manager.ts +512 -0
- package/src/lib/server/session-tools/connector.ts +124 -0
- package/src/lib/server/session-tools/context-mgmt.ts +103 -0
- package/src/lib/server/session-tools/context.ts +114 -0
- package/src/lib/server/session-tools/crud.ts +673 -0
- package/src/lib/server/session-tools/delegate.ts +708 -0
- package/src/lib/server/session-tools/file.ts +264 -0
- package/src/lib/server/session-tools/index.ts +164 -0
- package/src/lib/server/session-tools/memory.ts +230 -0
- package/src/lib/server/session-tools/session-info.ts +422 -0
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +166 -0
- package/src/lib/server/session-tools/shell.ts +171 -0
- package/src/lib/server/session-tools/web.ts +408 -0
- package/src/lib/server/session-tools.ts +9 -0
- package/src/lib/server/skills-normalize.ts +130 -0
- package/src/lib/server/storage-mcp.test.ts +161 -0
- package/src/lib/server/storage.ts +670 -0
- package/src/lib/server/stream-agent-chat.ts +571 -0
- package/src/lib/server/task-reports.ts +122 -0
- package/src/lib/server/task-result.ts +161 -0
- package/src/lib/server/task-validation.test.ts +27 -0
- package/src/lib/server/task-validation.ts +90 -0
- package/src/lib/server/tool-capability-policy.test.ts +58 -0
- package/src/lib/server/tool-capability-policy.ts +262 -0
- package/src/lib/sessions.ts +68 -0
- package/src/lib/tasks.ts +20 -0
- package/src/lib/tts.ts +42 -0
- package/src/lib/upload.ts +10 -0
- package/src/lib/utils.ts +6 -0
- package/src/proxy.ts +43 -0
- package/src/stores/use-app-store.ts +468 -0
- package/src/stores/use-chat-store.ts +323 -0
- package/src/types/index.ts +621 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
4
|
+
import type { Message } from '@/types'
|
|
5
|
+
import { useChatStore } from '@/stores/use-chat-store'
|
|
6
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
7
|
+
import { MessageBubble } from './message-bubble'
|
|
8
|
+
import { StreamingBubble } from './streaming-bubble'
|
|
9
|
+
import { ThinkingIndicator } from './thinking-indicator'
|
|
10
|
+
|
|
11
|
+
function dateSeparator(ts: number): string {
|
|
12
|
+
const d = new Date(ts)
|
|
13
|
+
const today = new Date()
|
|
14
|
+
const yesterday = new Date()
|
|
15
|
+
yesterday.setDate(today.getDate() - 1)
|
|
16
|
+
if (d.toDateString() === today.toDateString()) return 'Today'
|
|
17
|
+
if (d.toDateString() === yesterday.toDateString()) return 'Yesterday'
|
|
18
|
+
return d.toLocaleDateString([], { month: 'short', day: 'numeric' })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface Props {
|
|
22
|
+
messages: Message[]
|
|
23
|
+
streaming: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function MessageList({ messages, streaming }: Props) {
|
|
27
|
+
const scrollRef = useRef<HTMLDivElement>(null)
|
|
28
|
+
const [showScrollToBottom, setShowScrollToBottom] = useState(false)
|
|
29
|
+
const needsSnapRef = useRef(true)
|
|
30
|
+
const prevSessionIdRef = useRef<string | null>(null)
|
|
31
|
+
const streamText = useChatStore((s) => s.streamText)
|
|
32
|
+
const retryLastMessage = useChatStore((s) => s.retryLastMessage)
|
|
33
|
+
const session = useAppStore((s) => {
|
|
34
|
+
const id = s.currentSessionId
|
|
35
|
+
return id ? s.sessions[id] : null
|
|
36
|
+
})
|
|
37
|
+
const agents = useAppStore((s) => s.agents)
|
|
38
|
+
const agent = session?.agentId ? agents[session.agentId] : null
|
|
39
|
+
const appSettings = useAppStore((s) => s.appSettings)
|
|
40
|
+
const assistantName = agent?.name
|
|
41
|
+
|| (session?.provider === 'claude-cli' ? undefined : session?.model || session?.provider)
|
|
42
|
+
|| undefined
|
|
43
|
+
|
|
44
|
+
const showOk = appSettings.heartbeatShowOk ?? false
|
|
45
|
+
const showAlerts = appSettings.heartbeatShowAlerts ?? true
|
|
46
|
+
|
|
47
|
+
// Unread count tracking
|
|
48
|
+
const unreadRef = useRef(0)
|
|
49
|
+
const [unreadCount, setUnreadCount] = useState(0)
|
|
50
|
+
const prevMsgCountRef = useRef(messages.length)
|
|
51
|
+
|
|
52
|
+
// In-thread search
|
|
53
|
+
const [searchOpen, setSearchOpen] = useState(false)
|
|
54
|
+
const [searchQuery, setSearchQuery] = useState('')
|
|
55
|
+
const [searchIdx, setSearchIdx] = useState(0)
|
|
56
|
+
const searchInputRef = useRef<HTMLInputElement>(null)
|
|
57
|
+
|
|
58
|
+
const isHeartbeatMessage = (msg: Message) =>
|
|
59
|
+
msg.role === 'assistant' && (msg.kind === 'heartbeat' || /^\s*HEARTBEAT_OK\b/i.test(msg.text || ''))
|
|
60
|
+
const isHeartbeatOk = (msg: Message) =>
|
|
61
|
+
msg.suppressed === true || (msg.kind === 'heartbeat' && /^\s*HEARTBEAT_OK\b/i.test(msg.text || ''))
|
|
62
|
+
|
|
63
|
+
const displayedMessages: Message[] = []
|
|
64
|
+
for (const msg of messages) {
|
|
65
|
+
const isHeartbeat = isHeartbeatMessage(msg)
|
|
66
|
+
|
|
67
|
+
// Visibility filtering based on settings
|
|
68
|
+
if (isHeartbeat) {
|
|
69
|
+
if (!showAlerts) continue // Hide all heartbeat messages
|
|
70
|
+
if (!showOk && isHeartbeatOk(msg)) continue // Hide OK messages
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const last = displayedMessages[displayedMessages.length - 1]
|
|
74
|
+
const lastIsHeartbeat = !!last && isHeartbeatMessage(last)
|
|
75
|
+
if (isHeartbeat && lastIsHeartbeat) {
|
|
76
|
+
displayedMessages[displayedMessages.length - 1] = msg
|
|
77
|
+
} else {
|
|
78
|
+
displayedMessages.push(msg)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Search matches
|
|
83
|
+
const searchMatches = searchQuery.trim()
|
|
84
|
+
? displayedMessages
|
|
85
|
+
.map((msg, i) => ({ msg, i }))
|
|
86
|
+
.filter(({ msg }) => msg.text.toLowerCase().includes(searchQuery.toLowerCase()))
|
|
87
|
+
: []
|
|
88
|
+
|
|
89
|
+
// Track whether user is at/near bottom so we know whether to auto-scroll on new content
|
|
90
|
+
const wasAtBottomRef = useRef(true)
|
|
91
|
+
|
|
92
|
+
const isNearBottom = useCallback((el: HTMLDivElement) => {
|
|
93
|
+
return el.scrollHeight - el.scrollTop - el.clientHeight < 200
|
|
94
|
+
}, [])
|
|
95
|
+
|
|
96
|
+
const updateScrollState = useCallback(() => {
|
|
97
|
+
const el = scrollRef.current
|
|
98
|
+
if (!el) return
|
|
99
|
+
const nearBottom = isNearBottom(el)
|
|
100
|
+
wasAtBottomRef.current = nearBottom
|
|
101
|
+
setShowScrollToBottom(!nearBottom)
|
|
102
|
+
if (nearBottom && unreadRef.current > 0) {
|
|
103
|
+
unreadRef.current = 0
|
|
104
|
+
setUnreadCount(0)
|
|
105
|
+
}
|
|
106
|
+
}, [isNearBottom])
|
|
107
|
+
|
|
108
|
+
// Track unread messages arriving while scrolled up
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
const newCount = messages.length - prevMsgCountRef.current
|
|
111
|
+
prevMsgCountRef.current = messages.length
|
|
112
|
+
if (newCount > 0 && scrollRef.current && !isNearBottom(scrollRef.current)) {
|
|
113
|
+
unreadRef.current += newCount
|
|
114
|
+
setUnreadCount(unreadRef.current)
|
|
115
|
+
}
|
|
116
|
+
}, [messages.length, isNearBottom])
|
|
117
|
+
|
|
118
|
+
// Detect session switch during render (no extra useEffect, no dep-array mismatch)
|
|
119
|
+
const sessionId = session?.id ?? null
|
|
120
|
+
if (sessionId !== prevSessionIdRef.current) {
|
|
121
|
+
prevSessionIdRef.current = sessionId
|
|
122
|
+
needsSnapRef.current = true
|
|
123
|
+
wasAtBottomRef.current = true
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
const el = scrollRef.current
|
|
128
|
+
if (!el) return
|
|
129
|
+
if (needsSnapRef.current && messages.length > 0) {
|
|
130
|
+
// First render after session switch — snap instantly, no visible scroll
|
|
131
|
+
needsSnapRef.current = false
|
|
132
|
+
el.scrollTop = el.scrollHeight
|
|
133
|
+
setShowScrollToBottom(false)
|
|
134
|
+
wasAtBottomRef.current = true
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
// Auto-scroll if user was at bottom before new content arrived
|
|
138
|
+
if (wasAtBottomRef.current) {
|
|
139
|
+
el.scrollTop = el.scrollHeight
|
|
140
|
+
}
|
|
141
|
+
updateScrollState()
|
|
142
|
+
}, [messages.length, streamText, isNearBottom, updateScrollState])
|
|
143
|
+
|
|
144
|
+
const handleScrollToBottom = useCallback(() => {
|
|
145
|
+
const el = scrollRef.current
|
|
146
|
+
if (!el) return
|
|
147
|
+
el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' })
|
|
148
|
+
setShowScrollToBottom(false)
|
|
149
|
+
unreadRef.current = 0
|
|
150
|
+
setUnreadCount(0)
|
|
151
|
+
}, [])
|
|
152
|
+
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
if (typeof window === 'undefined') return
|
|
155
|
+
const handler = () => handleScrollToBottom()
|
|
156
|
+
window.addEventListener('swarmclaw:scroll-bottom', handler)
|
|
157
|
+
return () => window.removeEventListener('swarmclaw:scroll-bottom', handler)
|
|
158
|
+
}, [handleScrollToBottom])
|
|
159
|
+
|
|
160
|
+
// Ctrl+F search toggle
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
const handler = (e: KeyboardEvent) => {
|
|
163
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
|
|
164
|
+
e.preventDefault()
|
|
165
|
+
setSearchOpen((v) => {
|
|
166
|
+
if (!v) setTimeout(() => searchInputRef.current?.focus(), 50)
|
|
167
|
+
else { setSearchQuery(''); setSearchIdx(0) }
|
|
168
|
+
return !v
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
if (e.key === 'Escape' && searchOpen) {
|
|
172
|
+
setSearchOpen(false)
|
|
173
|
+
setSearchQuery('')
|
|
174
|
+
setSearchIdx(0)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
window.addEventListener('keydown', handler)
|
|
178
|
+
return () => window.removeEventListener('keydown', handler)
|
|
179
|
+
}, [searchOpen])
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<div className="relative flex-1 min-h-0">
|
|
183
|
+
{/* In-thread search bar */}
|
|
184
|
+
{searchOpen && (
|
|
185
|
+
<div className="absolute top-0 left-0 right-0 z-20 flex items-center gap-2 px-6 md:px-12 lg:px-16 py-2 bg-surface/95 backdrop-blur-sm border-b border-white/[0.06]">
|
|
186
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3 shrink-0">
|
|
187
|
+
<circle cx="11" cy="11" r="8" />
|
|
188
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
189
|
+
</svg>
|
|
190
|
+
<input
|
|
191
|
+
ref={searchInputRef}
|
|
192
|
+
type="text"
|
|
193
|
+
value={searchQuery}
|
|
194
|
+
onChange={(e) => { setSearchQuery(e.target.value); setSearchIdx(0) }}
|
|
195
|
+
placeholder="Search in conversation..."
|
|
196
|
+
className="flex-1 bg-transparent text-text text-[13px] outline-none placeholder:text-text-3/50"
|
|
197
|
+
style={{ fontFamily: 'inherit' }}
|
|
198
|
+
onKeyDown={(e) => {
|
|
199
|
+
if (e.key === 'Enter') {
|
|
200
|
+
e.preventDefault()
|
|
201
|
+
if (e.shiftKey) setSearchIdx((v) => Math.max(0, v - 1))
|
|
202
|
+
else setSearchIdx((v) => Math.min(searchMatches.length - 1, v + 1))
|
|
203
|
+
}
|
|
204
|
+
}}
|
|
205
|
+
/>
|
|
206
|
+
{searchQuery && (
|
|
207
|
+
<span className="text-[11px] text-text-3 tabular-nums shrink-0">
|
|
208
|
+
{searchMatches.length > 0 ? `${searchIdx + 1}/${searchMatches.length}` : '0 results'}
|
|
209
|
+
</span>
|
|
210
|
+
)}
|
|
211
|
+
<button
|
|
212
|
+
onClick={() => setSearchIdx((v) => Math.max(0, v - 1))}
|
|
213
|
+
disabled={!searchMatches.length}
|
|
214
|
+
aria-label="Previous match"
|
|
215
|
+
className="p-1 rounded-[6px] text-text-3 hover:text-text-2 hover:bg-white/[0.04] disabled:opacity-30 cursor-pointer border-none bg-transparent transition-colors"
|
|
216
|
+
>
|
|
217
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="m18 15-6-6-6 6" /></svg>
|
|
218
|
+
</button>
|
|
219
|
+
<button
|
|
220
|
+
onClick={() => setSearchIdx((v) => Math.min(searchMatches.length - 1, v + 1))}
|
|
221
|
+
disabled={!searchMatches.length}
|
|
222
|
+
aria-label="Next match"
|
|
223
|
+
className="p-1 rounded-[6px] text-text-3 hover:text-text-2 hover:bg-white/[0.04] disabled:opacity-30 cursor-pointer border-none bg-transparent transition-colors"
|
|
224
|
+
>
|
|
225
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="m6 9 6 6 6-6" /></svg>
|
|
226
|
+
</button>
|
|
227
|
+
<button
|
|
228
|
+
onClick={() => { setSearchOpen(false); setSearchQuery(''); setSearchIdx(0) }}
|
|
229
|
+
aria-label="Close search"
|
|
230
|
+
className="p-1 rounded-[6px] text-text-3 hover:text-text-2 hover:bg-white/[0.04] cursor-pointer border-none bg-transparent transition-colors"
|
|
231
|
+
>
|
|
232
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg>
|
|
233
|
+
</button>
|
|
234
|
+
</div>
|
|
235
|
+
)}
|
|
236
|
+
|
|
237
|
+
<div
|
|
238
|
+
ref={scrollRef}
|
|
239
|
+
onScroll={updateScrollState}
|
|
240
|
+
className="h-full overflow-y-auto px-6 md:px-12 lg:px-16 py-6"
|
|
241
|
+
>
|
|
242
|
+
<div className="flex flex-col gap-6">
|
|
243
|
+
{displayedMessages.map((msg, i) => {
|
|
244
|
+
const isLastAssistant = msg.role === 'assistant' && !streaming
|
|
245
|
+
&& displayedMessages.slice(i + 1).every((m) => m.role !== 'assistant')
|
|
246
|
+
const isSearchMatch = searchQuery && searchMatches.some((m) => m.i === i)
|
|
247
|
+
const isCurrentMatch = searchQuery && searchMatches[searchIdx]?.i === i
|
|
248
|
+
|
|
249
|
+
// Date separator
|
|
250
|
+
const prevMsg = i > 0 ? displayedMessages[i - 1] : null
|
|
251
|
+
const showDateSep = msg.time && (!prevMsg?.time || new Date(msg.time).toDateString() !== new Date(prevMsg.time).toDateString())
|
|
252
|
+
|
|
253
|
+
return (
|
|
254
|
+
<div key={`${msg.time}-${i}`}>
|
|
255
|
+
{showDateSep && (
|
|
256
|
+
<div className="flex items-center gap-4 py-2 mb-2">
|
|
257
|
+
<div className="flex-1 h-px bg-white/[0.06]" />
|
|
258
|
+
<span className="text-[10px] font-600 text-text-3/50 uppercase tracking-[0.1em]">
|
|
259
|
+
{dateSeparator(msg.time)}
|
|
260
|
+
</span>
|
|
261
|
+
<div className="flex-1 h-px bg-white/[0.06]" />
|
|
262
|
+
</div>
|
|
263
|
+
)}
|
|
264
|
+
<div className={isCurrentMatch ? 'ring-1 ring-amber-400/50 rounded-[16px] bg-amber-400/[0.04]' : isSearchMatch ? 'bg-white/[0.02] rounded-[16px]' : ''}>
|
|
265
|
+
<MessageBubble
|
|
266
|
+
message={msg}
|
|
267
|
+
assistantName={assistantName}
|
|
268
|
+
isLast={isLastAssistant}
|
|
269
|
+
onRetry={isLastAssistant ? retryLastMessage : undefined}
|
|
270
|
+
/>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
)
|
|
274
|
+
})}
|
|
275
|
+
{streaming && !streamText && <ThinkingIndicator assistantName={assistantName} />}
|
|
276
|
+
{streaming && streamText && <StreamingBubble text={streamText} assistantName={assistantName} />}
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
{showScrollToBottom && (
|
|
280
|
+
<button
|
|
281
|
+
onClick={handleScrollToBottom}
|
|
282
|
+
className="absolute right-6 md:right-12 lg:right-16 bottom-5 inline-flex items-center gap-2 px-3 py-1.5 rounded-full border border-white/[0.08] bg-[#171a2b]/95 text-text-2 text-[12px] font-600 hover:bg-[#1e2238] transition-colors shadow-lg cursor-pointer"
|
|
283
|
+
title="Scroll to latest messages"
|
|
284
|
+
>
|
|
285
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round">
|
|
286
|
+
<path d="M12 5v14" />
|
|
287
|
+
<path d="m19 12-7 7-7-7" />
|
|
288
|
+
</svg>
|
|
289
|
+
Latest
|
|
290
|
+
{unreadCount > 0 && (
|
|
291
|
+
<span className="inline-flex items-center justify-center min-w-[18px] h-[18px] px-1 rounded-full bg-accent-bright text-white text-[10px] font-700">
|
|
292
|
+
{unreadCount}
|
|
293
|
+
</span>
|
|
294
|
+
)}
|
|
295
|
+
</button>
|
|
296
|
+
)}
|
|
297
|
+
</div>
|
|
298
|
+
)
|
|
299
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react'
|
|
4
|
+
import type { Message } from '@/types'
|
|
5
|
+
import { IconButton } from '@/components/shared/icon-button'
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
messages: Message[]
|
|
9
|
+
open: boolean
|
|
10
|
+
onClose: () => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type EventType = 'user' | 'assistant' | 'delegation' | 'agent_result' | 'system' | 'error' | 'tool_call'
|
|
14
|
+
|
|
15
|
+
interface DebugEvent {
|
|
16
|
+
type: EventType
|
|
17
|
+
label: string
|
|
18
|
+
detail: string
|
|
19
|
+
time: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function classifyMessage(msg: Message): DebugEvent {
|
|
23
|
+
const text = msg.text || ''
|
|
24
|
+
|
|
25
|
+
if (msg.role === 'user') {
|
|
26
|
+
if (text.startsWith('[System]')) {
|
|
27
|
+
return { type: 'system', label: 'System', detail: text.replace('[System] ', ''), time: msg.time }
|
|
28
|
+
}
|
|
29
|
+
if (text.startsWith('[Agent ')) {
|
|
30
|
+
const match = text.match(/\[Agent (.+?) result\]/)
|
|
31
|
+
return { type: 'agent_result', label: `Agent: ${match?.[1] || 'Unknown'}`, detail: text.replace(/\[Agent .+? result\]:?\n?/, ''), time: msg.time }
|
|
32
|
+
}
|
|
33
|
+
if (text.startsWith('[Memory search')) {
|
|
34
|
+
return { type: 'system', label: 'Memory Search', detail: text.replace('[Memory search results]:\n', ''), time: msg.time }
|
|
35
|
+
}
|
|
36
|
+
return { type: 'user', label: 'User', detail: text, time: msg.time }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// assistant
|
|
40
|
+
if (text.startsWith('[Delegating to ')) {
|
|
41
|
+
const match = text.match(/\[Delegating to (.+?)\]/)
|
|
42
|
+
return { type: 'delegation', label: `Delegate: ${match?.[1] || 'Unknown'}`, detail: text.replace(/\[Delegating to .+?\]:?\s?/, ''), time: msg.time }
|
|
43
|
+
}
|
|
44
|
+
if (text.startsWith('[Error]')) {
|
|
45
|
+
return { type: 'error', label: 'Error', detail: text.replace('[Error] ', ''), time: msg.time }
|
|
46
|
+
}
|
|
47
|
+
if (text.startsWith('Starting task:')) {
|
|
48
|
+
return { type: 'system', label: 'Task Start', detail: text, time: msg.time }
|
|
49
|
+
}
|
|
50
|
+
return { type: 'assistant', label: 'Assistant', detail: text, time: msg.time }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const TYPE_COLORS: Record<EventType, string> = {
|
|
54
|
+
user: '#6366F1',
|
|
55
|
+
assistant: '#a0a0b0',
|
|
56
|
+
delegation: '#F59E0B',
|
|
57
|
+
agent_result: '#10B981',
|
|
58
|
+
system: '#6B7280',
|
|
59
|
+
error: '#EF4444',
|
|
60
|
+
tool_call: '#8B5CF6',
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const TYPE_ICONS: Record<EventType, string> = {
|
|
64
|
+
user: 'U',
|
|
65
|
+
assistant: 'AI',
|
|
66
|
+
delegation: 'D',
|
|
67
|
+
agent_result: 'R',
|
|
68
|
+
system: 'S',
|
|
69
|
+
error: '!',
|
|
70
|
+
tool_call: 'T',
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function fmtTime(ts: number) {
|
|
74
|
+
return new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function SessionDebugPanel({ messages, open, onClose }: Props) {
|
|
78
|
+
const [filter, setFilter] = useState<EventType | 'all'>('all')
|
|
79
|
+
const [expandedIdx, setExpandedIdx] = useState<number | null>(null)
|
|
80
|
+
|
|
81
|
+
const events = messages.map(classifyMessage)
|
|
82
|
+
const filtered = filter === 'all' ? events : events.filter((e) => e.type === filter)
|
|
83
|
+
|
|
84
|
+
// Auto-scroll to bottom
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
setExpandedIdx(null)
|
|
87
|
+
}, [messages.length])
|
|
88
|
+
|
|
89
|
+
if (!open) return null
|
|
90
|
+
|
|
91
|
+
const filters: { id: EventType | 'all'; label: string }[] = [
|
|
92
|
+
{ id: 'all', label: 'All' },
|
|
93
|
+
{ id: 'delegation', label: 'Delegations' },
|
|
94
|
+
{ id: 'agent_result', label: 'Results' },
|
|
95
|
+
{ id: 'error', label: 'Errors' },
|
|
96
|
+
{ id: 'system', label: 'System' },
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div className="absolute inset-0 z-30 bg-bg/95 backdrop-blur-xl flex flex-col">
|
|
101
|
+
{/* Header */}
|
|
102
|
+
<div className="flex items-center gap-3 px-5 py-3 border-b border-white/[0.06] shrink-0">
|
|
103
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#6366F1" strokeWidth="2" strokeLinecap="round">
|
|
104
|
+
<path d="M12 20V10" />
|
|
105
|
+
<path d="M18 20V4" />
|
|
106
|
+
<path d="M6 20v-4" />
|
|
107
|
+
</svg>
|
|
108
|
+
<span className="font-display text-[16px] font-600 tracking-[-0.02em] flex-1">Session Debug</span>
|
|
109
|
+
<span className="text-[12px] text-text-3 font-mono">{events.length} events</span>
|
|
110
|
+
<IconButton onClick={onClose} aria-label="Close debug panel">
|
|
111
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
|
|
112
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
113
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
114
|
+
</svg>
|
|
115
|
+
</IconButton>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
{/* Filters */}
|
|
119
|
+
<div className="flex gap-2 px-5 py-3 border-b border-white/[0.04] overflow-x-auto shrink-0">
|
|
120
|
+
{filters.map((f) => (
|
|
121
|
+
<button
|
|
122
|
+
key={f.id}
|
|
123
|
+
onClick={() => setFilter(f.id)}
|
|
124
|
+
className={`px-3 py-1.5 rounded-[8px] text-[11px] font-600 cursor-pointer transition-all border whitespace-nowrap
|
|
125
|
+
${filter === f.id
|
|
126
|
+
? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
|
|
127
|
+
: 'bg-surface border-white/[0.06] text-text-3 hover:text-text-2'}`}
|
|
128
|
+
style={{ fontFamily: 'inherit' }}
|
|
129
|
+
>
|
|
130
|
+
{f.label}
|
|
131
|
+
</button>
|
|
132
|
+
))}
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
{/* Event timeline */}
|
|
136
|
+
<div className="flex-1 overflow-y-auto px-5 py-4">
|
|
137
|
+
<div className="relative">
|
|
138
|
+
{/* Timeline line */}
|
|
139
|
+
<div className="absolute left-[15px] top-0 bottom-0 w-px bg-white/[0.06]" />
|
|
140
|
+
|
|
141
|
+
{filtered.map((event, i) => {
|
|
142
|
+
const color = TYPE_COLORS[event.type]
|
|
143
|
+
const expanded = expandedIdx === i
|
|
144
|
+
return (
|
|
145
|
+
<button
|
|
146
|
+
key={i}
|
|
147
|
+
onClick={() => setExpandedIdx(expanded ? null : i)}
|
|
148
|
+
className="w-full text-left relative pl-10 pb-4 group cursor-pointer"
|
|
149
|
+
>
|
|
150
|
+
{/* Dot */}
|
|
151
|
+
<div
|
|
152
|
+
className="absolute left-[10px] top-1 w-[11px] h-[11px] rounded-full border-2"
|
|
153
|
+
style={{ borderColor: color, backgroundColor: expanded ? color : 'transparent' }}
|
|
154
|
+
/>
|
|
155
|
+
|
|
156
|
+
{/* Content */}
|
|
157
|
+
<div className="flex items-center gap-2 mb-0.5">
|
|
158
|
+
<span className="text-[11px] font-700 uppercase tracking-wider" style={{ color }}>
|
|
159
|
+
{event.label}
|
|
160
|
+
</span>
|
|
161
|
+
<span className="text-[10px] text-text-3/70 font-mono">{fmtTime(event.time)}</span>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<p className={`text-[12px] text-text-3 leading-[1.5] ${expanded ? 'whitespace-pre-wrap' : 'line-clamp-2'}`}>
|
|
165
|
+
{event.detail}
|
|
166
|
+
</p>
|
|
167
|
+
|
|
168
|
+
{!expanded && event.detail.length > 150 && (
|
|
169
|
+
<span className="text-[11px] text-accent-bright/60 mt-1 inline-block">click to expand</span>
|
|
170
|
+
)}
|
|
171
|
+
</button>
|
|
172
|
+
)
|
|
173
|
+
})}
|
|
174
|
+
|
|
175
|
+
{filtered.length === 0 && (
|
|
176
|
+
<p className="text-center text-[13px] text-text-3 py-12">No events matching filter</p>
|
|
177
|
+
)}
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
{/* Stats bar */}
|
|
182
|
+
<div className="flex items-center gap-4 px-5 py-3 border-t border-white/[0.06] shrink-0">
|
|
183
|
+
{(['delegation', 'agent_result', 'error'] as EventType[]).map((type) => {
|
|
184
|
+
const count = events.filter((e) => e.type === type).length
|
|
185
|
+
if (!count) return null
|
|
186
|
+
return (
|
|
187
|
+
<span key={type} className="flex items-center gap-1.5 text-[11px] font-mono" style={{ color: TYPE_COLORS[type] }}>
|
|
188
|
+
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: TYPE_COLORS[type] }} />
|
|
189
|
+
{count} {type === 'delegation' ? 'delegations' : type === 'agent_result' ? 'results' : 'errors'}
|
|
190
|
+
</span>
|
|
191
|
+
)
|
|
192
|
+
})}
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
)
|
|
196
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react'
|
|
4
|
+
import ReactMarkdown from 'react-markdown'
|
|
5
|
+
import remarkGfm from 'remark-gfm'
|
|
6
|
+
import rehypeHighlight from 'rehype-highlight'
|
|
7
|
+
import { AiAvatar } from '@/components/shared/avatar'
|
|
8
|
+
import { CodeBlock } from './code-block'
|
|
9
|
+
import { ToolCallBubble } from './tool-call-bubble'
|
|
10
|
+
import { useChatStore } from '@/stores/use-chat-store'
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
text: string
|
|
14
|
+
assistantName?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function StreamingBubble({ text, assistantName }: Props) {
|
|
18
|
+
const rendered = useMemo(() => text, [text])
|
|
19
|
+
const toolEvents = useChatStore((s) => s.toolEvents)
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div
|
|
23
|
+
className="flex flex-col items-start"
|
|
24
|
+
style={{ animation: 'msg-in-left 0.35s cubic-bezier(0.16, 1, 0.3, 1)' }}
|
|
25
|
+
>
|
|
26
|
+
<div className="flex items-center gap-2.5 mb-2 px-1">
|
|
27
|
+
<AiAvatar size="sm" />
|
|
28
|
+
<span className="text-[12px] font-600 text-text-3">{assistantName || 'Claude'}</span>
|
|
29
|
+
<span className="w-2 h-2 rounded-full bg-accent-bright" style={{ animation: 'pulse 1.5s ease infinite' }} />
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
{/* Tool call events */}
|
|
33
|
+
{toolEvents.length > 0 && (
|
|
34
|
+
<div className="max-w-[85%] md:max-w-[72%] flex flex-col gap-2 mb-2">
|
|
35
|
+
{toolEvents.map((event) => (
|
|
36
|
+
<ToolCallBubble key={event.id} event={event} />
|
|
37
|
+
))}
|
|
38
|
+
</div>
|
|
39
|
+
)}
|
|
40
|
+
|
|
41
|
+
{rendered && (
|
|
42
|
+
<div className="max-w-[85%] md:max-w-[72%] bubble-ai px-5 py-3.5">
|
|
43
|
+
<div className="msg-content streaming-cursor text-[15px] leading-[1.7] break-words text-text">
|
|
44
|
+
<ReactMarkdown
|
|
45
|
+
remarkPlugins={[remarkGfm]}
|
|
46
|
+
rehypePlugins={[rehypeHighlight]}
|
|
47
|
+
components={{
|
|
48
|
+
pre({ children }) {
|
|
49
|
+
return <pre>{children}</pre>
|
|
50
|
+
},
|
|
51
|
+
code({ className, children }) {
|
|
52
|
+
const isBlock = className?.startsWith('language-') || className?.startsWith('hljs')
|
|
53
|
+
if (isBlock) {
|
|
54
|
+
return <CodeBlock className={className}>{children}</CodeBlock>
|
|
55
|
+
}
|
|
56
|
+
return <code className={className}>{children}</code>
|
|
57
|
+
},
|
|
58
|
+
a({ href, children }) {
|
|
59
|
+
if (!href) return <>{children}</>
|
|
60
|
+
const ytMatch = href.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/)
|
|
61
|
+
if (ytMatch) {
|
|
62
|
+
return (
|
|
63
|
+
<div className="my-2">
|
|
64
|
+
<iframe
|
|
65
|
+
src={`https://www.youtube-nocookie.com/embed/${ytMatch[1]}`}
|
|
66
|
+
className="w-full aspect-video rounded-[10px] border border-white/10"
|
|
67
|
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
68
|
+
allowFullScreen
|
|
69
|
+
title="YouTube video"
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
return <a href={href} target="_blank" rel="noopener noreferrer">{children}</a>
|
|
75
|
+
},
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
{rendered}
|
|
79
|
+
</ReactMarkdown>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { AiAvatar } from '@/components/shared/avatar'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
assistantName?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function ThinkingIndicator({ assistantName }: Props) {
|
|
10
|
+
return (
|
|
11
|
+
<div className="flex flex-col items-start"
|
|
12
|
+
style={{ animation: 'msg-in-left 0.35s cubic-bezier(0.16, 1, 0.3, 1)' }}>
|
|
13
|
+
<div className="flex items-center gap-2.5 mb-2 px-1">
|
|
14
|
+
<AiAvatar size="sm" />
|
|
15
|
+
<span className="text-[12px] font-600 text-text-3">{assistantName || 'Claude'}</span>
|
|
16
|
+
</div>
|
|
17
|
+
<div className="bubble-ai px-6 py-5">
|
|
18
|
+
<div className="flex gap-2">
|
|
19
|
+
<span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite' }} />
|
|
20
|
+
<span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite 0.15s' }} />
|
|
21
|
+
<span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite 0.3s' }} />
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
)
|
|
26
|
+
}
|