@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
|
@@ -6,12 +6,16 @@ import { useChatStore } from '@/stores/use-chat-store'
|
|
|
6
6
|
import { useAppStore } from '@/stores/use-app-store'
|
|
7
7
|
import { api } from '@/lib/api-client'
|
|
8
8
|
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
9
|
+
import { ConnectorPlatformIcon, CONNECTOR_PLATFORM_META } from '@/components/shared/connector-platform-icon'
|
|
9
10
|
import { MessageBubble } from './message-bubble'
|
|
10
11
|
import { StreamingBubble } from './streaming-bubble'
|
|
11
12
|
import { ThinkingIndicator } from './thinking-indicator'
|
|
12
13
|
import { SuggestionsBar } from './suggestions-bar'
|
|
13
14
|
import { ExecApprovalCard } from './exec-approval-card'
|
|
15
|
+
import { HeartbeatMoment, ActivityMoment, isNotableTool } from './activity-moment'
|
|
14
16
|
import { useApprovalStore } from '@/stores/use-approval-store'
|
|
17
|
+
import { useWs } from '@/hooks/use-ws'
|
|
18
|
+
import { GatewayDisconnectOverlay, useGatewayStatus } from '@/components/settings/gateway-disconnect-overlay'
|
|
15
19
|
|
|
16
20
|
const INTRO_GREETINGS = [
|
|
17
21
|
'What can I help you with?',
|
|
@@ -55,6 +59,10 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
55
59
|
const retryLastMessage = useChatStore((s) => s.retryLastMessage)
|
|
56
60
|
const editAndResend = useChatStore((s) => s.editAndResend)
|
|
57
61
|
const sendMessage = useChatStore((s) => s.sendMessage)
|
|
62
|
+
const hasMoreMessages = useChatStore((s) => s.hasMoreMessages)
|
|
63
|
+
const loadingMore = useChatStore((s) => s.loadingMore)
|
|
64
|
+
const totalMessages = useChatStore((s) => s.totalMessages)
|
|
65
|
+
const loadMoreMessages = useChatStore((s) => s.loadMoreMessages)
|
|
58
66
|
const forkSession = useAppStore((s) => s.forkSession)
|
|
59
67
|
const session = useAppStore((s) => {
|
|
60
68
|
const id = s.currentSessionId
|
|
@@ -71,6 +79,38 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
71
79
|
const showOk = appSettings.heartbeatShowOk ?? false
|
|
72
80
|
const showAlerts = appSettings.heartbeatShowAlerts ?? true
|
|
73
81
|
|
|
82
|
+
// Gateway disconnect overlay for openclaw agents
|
|
83
|
+
const isOpenClaw = agent?.provider === 'openclaw'
|
|
84
|
+
const gatewayStatus = useGatewayStatus()
|
|
85
|
+
const showGatewayOverlay = isOpenClaw && gatewayStatus === 'disconnected'
|
|
86
|
+
|
|
87
|
+
// Moment overlay for last assistant message (heartbeat or tool events)
|
|
88
|
+
type MomentType = { kind: 'heartbeat' } | { kind: 'tool'; name: string; input: string }
|
|
89
|
+
const [currentMoment, setCurrentMoment] = useState<MomentType | null>(null)
|
|
90
|
+
|
|
91
|
+
const heartbeatTopic = agent?.id ? `heartbeat:agent:${agent.id}` : ''
|
|
92
|
+
useWs(heartbeatTopic, () => {
|
|
93
|
+
setCurrentMoment({ kind: 'heartbeat' })
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
// Detect notable tool events on latest assistant message when messages change
|
|
97
|
+
const prevToolKeyRef = useRef<string | null>(null)
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
const last = messages[messages.length - 1]
|
|
100
|
+
if (!last || last.role !== 'assistant' || !last.toolEvents?.length) return
|
|
101
|
+
const events = last.toolEvents
|
|
102
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
103
|
+
if (isNotableTool(events[i].name)) {
|
|
104
|
+
const key = `${last.time}-${events[i].name}-${i}`
|
|
105
|
+
if (key !== prevToolKeyRef.current) {
|
|
106
|
+
prevToolKeyRef.current = key
|
|
107
|
+
setCurrentMoment({ kind: 'tool', name: events[i].name, input: events[i].input || '' })
|
|
108
|
+
}
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}, [messages])
|
|
113
|
+
|
|
74
114
|
// Unread count tracking
|
|
75
115
|
const unreadRef = useRef(0)
|
|
76
116
|
const [unreadCount, setUnreadCount] = useState(0)
|
|
@@ -79,6 +119,10 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
79
119
|
// Bookmark filter
|
|
80
120
|
const [bookmarkFilter, setBookmarkFilter] = useState(false)
|
|
81
121
|
|
|
122
|
+
// Connector source filter
|
|
123
|
+
const [connectorFilter, setConnectorFilter] = useState<string | null>(null)
|
|
124
|
+
const [connectorFilterCollapsed, setConnectorFilterCollapsed] = useState(false)
|
|
125
|
+
|
|
82
126
|
const toggleBookmark = useCallback(async (index: number) => {
|
|
83
127
|
if (!sessionId) return
|
|
84
128
|
const msg = messages[index]
|
|
@@ -137,10 +181,24 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
137
181
|
}
|
|
138
182
|
}
|
|
139
183
|
|
|
140
|
-
//
|
|
141
|
-
const
|
|
184
|
+
// Collect unique connector sources for filter UI
|
|
185
|
+
const connectorSources = new Map<string, { platform: string; connectorName: string }>()
|
|
186
|
+
for (const msg of displayedMessages) {
|
|
187
|
+
if (msg.source?.connectorId && !connectorSources.has(msg.source.connectorId)) {
|
|
188
|
+
connectorSources.set(msg.source.connectorId, {
|
|
189
|
+
platform: msg.source.platform,
|
|
190
|
+
connectorName: msg.source.connectorName,
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Apply bookmark + connector filter
|
|
196
|
+
let filteredMessages = bookmarkFilter
|
|
142
197
|
? displayedMessages.filter((msg) => msg.bookmarked)
|
|
143
198
|
: displayedMessages
|
|
199
|
+
if (connectorFilter) {
|
|
200
|
+
filteredMessages = filteredMessages.filter((msg) => msg.source?.connectorId === connectorFilter)
|
|
201
|
+
}
|
|
144
202
|
|
|
145
203
|
// Search matches
|
|
146
204
|
const searchMatches = searchQuery.trim()
|
|
@@ -245,6 +303,23 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
245
303
|
return () => window.removeEventListener('swarmclaw:scroll-bottom', handler)
|
|
246
304
|
}, [handleScrollToBottom])
|
|
247
305
|
|
|
306
|
+
// Scroll to a specific message by index (used by search)
|
|
307
|
+
useEffect(() => {
|
|
308
|
+
if (typeof window === 'undefined') return
|
|
309
|
+
const handler = (e: Event) => {
|
|
310
|
+
const idx = (e as CustomEvent).detail?.index
|
|
311
|
+
if (typeof idx !== 'number') return
|
|
312
|
+
const el = scrollRef.current?.querySelector(`[data-message-index="${idx}"]`) as HTMLElement | null
|
|
313
|
+
if (el) {
|
|
314
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
315
|
+
el.classList.add('bg-accent-bright/10')
|
|
316
|
+
setTimeout(() => el.classList.remove('bg-accent-bright/10'), 2000)
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
window.addEventListener('swarmclaw:scroll-to-message', handler)
|
|
320
|
+
return () => window.removeEventListener('swarmclaw:scroll-to-message', handler)
|
|
321
|
+
}, [])
|
|
322
|
+
|
|
248
323
|
// Ctrl+F search toggle
|
|
249
324
|
useEffect(() => {
|
|
250
325
|
const handler = (e: KeyboardEvent) => {
|
|
@@ -267,7 +342,7 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
267
342
|
}, [searchOpen])
|
|
268
343
|
|
|
269
344
|
return (
|
|
270
|
-
<div className="relative flex-1 min-h-0 min-w-0">
|
|
345
|
+
<div className="relative flex-1 min-h-0 min-w-0 flex flex-col overflow-hidden">
|
|
271
346
|
{/* In-thread search bar */}
|
|
272
347
|
{searchOpen && (
|
|
273
348
|
<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]">
|
|
@@ -281,6 +356,7 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
281
356
|
value={searchQuery}
|
|
282
357
|
onChange={(e) => { setSearchQuery(e.target.value); setSearchIdx(0) }}
|
|
283
358
|
placeholder="Search in conversation..."
|
|
359
|
+
aria-label="Search messages"
|
|
284
360
|
className="flex-1 bg-transparent text-text text-[13px] outline-none placeholder:text-text-3/50"
|
|
285
361
|
style={{ fontFamily: 'inherit' }}
|
|
286
362
|
onKeyDown={(e) => {
|
|
@@ -315,7 +391,7 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
315
391
|
<button
|
|
316
392
|
onClick={() => setBookmarkFilter((v) => !v)}
|
|
317
393
|
aria-label={bookmarkFilter ? 'Show all messages' : 'Show bookmarked only'}
|
|
318
|
-
className={`p-1 rounded-[6px] hover:bg-white/[0.04] cursor-pointer border-none bg-transparent transition-colors ${bookmarkFilter ? 'text-
|
|
394
|
+
className={`p-1 rounded-[6px] hover:bg-white/[0.04] cursor-pointer border-none bg-transparent transition-colors ${bookmarkFilter ? 'text-amber-500' : 'text-text-3 hover:text-text-2'}`}
|
|
319
395
|
>
|
|
320
396
|
<svg width="14" height="14" viewBox="0 0 24 24" fill={bookmarkFilter ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
321
397
|
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
|
@@ -331,12 +407,104 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
331
407
|
</div>
|
|
332
408
|
)}
|
|
333
409
|
|
|
410
|
+
{/* Connector source filter — shown when connector messages exist */}
|
|
411
|
+
{connectorSources.size > 0 && (
|
|
412
|
+
<div className="flex items-center gap-1.5 px-6 md:px-12 lg:px-16 py-1.5 border-b border-white/[0.04]">
|
|
413
|
+
<button
|
|
414
|
+
onClick={() => setConnectorFilterCollapsed((c) => !c)}
|
|
415
|
+
className="flex items-center gap-1 text-[10px] text-text-3/50 uppercase tracking-wider font-600 mr-1 bg-transparent border-none cursor-pointer hover:text-text-3/70 transition-colors p-0"
|
|
416
|
+
title={connectorFilterCollapsed ? 'Expand source filter' : 'Collapse source filter'}
|
|
417
|
+
>
|
|
418
|
+
<svg
|
|
419
|
+
width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"
|
|
420
|
+
className="transition-transform duration-200"
|
|
421
|
+
style={{ transform: connectorFilterCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)' }}
|
|
422
|
+
>
|
|
423
|
+
<polyline points="6 9 12 15 18 9" />
|
|
424
|
+
</svg>
|
|
425
|
+
Source
|
|
426
|
+
{connectorFilterCollapsed && connectorFilter && (
|
|
427
|
+
<span className="text-accent-bright/70 normal-case tracking-normal">
|
|
428
|
+
({connectorSources.get(connectorFilter)?.connectorName || connectorFilter})
|
|
429
|
+
</span>
|
|
430
|
+
)}
|
|
431
|
+
</button>
|
|
432
|
+
{!connectorFilterCollapsed && (
|
|
433
|
+
<>
|
|
434
|
+
<button
|
|
435
|
+
onClick={() => setConnectorFilter(null)}
|
|
436
|
+
className={`px-2 py-1 rounded-[6px] text-[11px] font-600 cursor-pointer border-none transition-all ${
|
|
437
|
+
!connectorFilter ? 'bg-accent-soft text-accent-bright' : 'bg-transparent text-text-3 hover:text-text-2 hover:bg-white/[0.04]'
|
|
438
|
+
}`}
|
|
439
|
+
style={{ fontFamily: 'inherit' }}
|
|
440
|
+
>
|
|
441
|
+
All
|
|
442
|
+
</button>
|
|
443
|
+
{Array.from(connectorSources.entries()).map(([cid, info]) => {
|
|
444
|
+
const active = connectorFilter === cid
|
|
445
|
+
const meta = CONNECTOR_PLATFORM_META[info.platform as keyof typeof CONNECTOR_PLATFORM_META]
|
|
446
|
+
return (
|
|
447
|
+
<button
|
|
448
|
+
key={cid}
|
|
449
|
+
onClick={() => setConnectorFilter(active ? null : cid)}
|
|
450
|
+
className={`flex items-center gap-1.5 px-2 py-1 rounded-[6px] text-[11px] font-600 cursor-pointer border-none transition-all ${
|
|
451
|
+
active ? 'bg-accent-soft text-accent-bright' : 'bg-transparent text-text-3 hover:text-text-2 hover:bg-white/[0.04]'
|
|
452
|
+
}`}
|
|
453
|
+
style={{ fontFamily: 'inherit' }}
|
|
454
|
+
>
|
|
455
|
+
<ConnectorPlatformIcon platform={info.platform as keyof typeof CONNECTOR_PLATFORM_META} size={12} />
|
|
456
|
+
{info.connectorName || meta?.label || info.platform}
|
|
457
|
+
</button>
|
|
458
|
+
)
|
|
459
|
+
})}
|
|
460
|
+
</>
|
|
461
|
+
)}
|
|
462
|
+
</div>
|
|
463
|
+
)}
|
|
464
|
+
|
|
334
465
|
<div
|
|
335
466
|
ref={scrollRef}
|
|
336
467
|
onScroll={updateScrollState}
|
|
337
|
-
className="h-
|
|
468
|
+
className="flex-1 min-h-0 overflow-y-auto overflow-x-hidden px-6 md:px-12 lg:px-16 pt-6 pb-10 fade-up"
|
|
338
469
|
>
|
|
339
|
-
<div className="flex flex-col gap-6">
|
|
470
|
+
<div className="flex flex-col gap-6 relative">
|
|
471
|
+
{/* Chat spine — vertical line for assistant messages */}
|
|
472
|
+
<div className="absolute left-[15px] top-0 bottom-0 w-px bg-white/[0.06] pointer-events-none" />
|
|
473
|
+
{hasMoreMessages && (
|
|
474
|
+
<div className="flex justify-center py-3">
|
|
475
|
+
<button
|
|
476
|
+
onClick={async () => {
|
|
477
|
+
const el = scrollRef.current
|
|
478
|
+
const prevHeight = el?.scrollHeight ?? 0
|
|
479
|
+
await loadMoreMessages()
|
|
480
|
+
// Preserve scroll position after prepending
|
|
481
|
+
if (el) {
|
|
482
|
+
requestAnimationFrame(() => {
|
|
483
|
+
el.scrollTop += el.scrollHeight - prevHeight
|
|
484
|
+
})
|
|
485
|
+
}
|
|
486
|
+
}}
|
|
487
|
+
disabled={loadingMore}
|
|
488
|
+
className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full border border-white/[0.08] bg-surface/80 text-text-3 text-[12px] font-600 hover:bg-surface-2 hover:text-text-2 transition-colors cursor-pointer disabled:opacity-50"
|
|
489
|
+
>
|
|
490
|
+
{loadingMore ? (
|
|
491
|
+
<>
|
|
492
|
+
<span className="w-3 h-3 rounded-full border-2 border-text-3/30 border-t-text-2 animate-spin" />
|
|
493
|
+
Loading...
|
|
494
|
+
</>
|
|
495
|
+
) : (
|
|
496
|
+
<>
|
|
497
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round">
|
|
498
|
+
<path d="M12 19V5" />
|
|
499
|
+
<path d="m5 12 7-7 7 7" />
|
|
500
|
+
</svg>
|
|
501
|
+
Load earlier messages
|
|
502
|
+
<span className="text-text-3/50">({totalMessages - messages.length} more)</span>
|
|
503
|
+
</>
|
|
504
|
+
)}
|
|
505
|
+
</button>
|
|
506
|
+
</div>
|
|
507
|
+
)}
|
|
340
508
|
{filteredMessages.length === 0 && !streaming && (
|
|
341
509
|
<div className="flex flex-col items-center justify-center gap-3 py-20 text-center" style={{ animation: 'fadeUp 0.5s cubic-bezier(0.16, 1, 0.3, 1) both' }}>
|
|
342
510
|
<AgentAvatar seed={agent?.avatarSeed || null} name={agent?.name || 'Agent'} size={48} />
|
|
@@ -347,6 +515,41 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
347
515
|
</div>
|
|
348
516
|
)}
|
|
349
517
|
{filteredMessages.map((msg, i) => {
|
|
518
|
+
// Context-clear divider — render a visual separator instead of a bubble
|
|
519
|
+
if (msg.kind === 'context-clear') {
|
|
520
|
+
const originalIndex = messages.indexOf(msg)
|
|
521
|
+
return (
|
|
522
|
+
<div key={`ctx-clear-${msg.time}-${i}`} className="group/ctx flex items-center gap-4 py-3">
|
|
523
|
+
<div className="flex-1 h-px bg-amber-400/20" />
|
|
524
|
+
<span className="flex items-center gap-1.5 text-[10px] font-600 text-amber-400/60 uppercase tracking-[0.1em]">
|
|
525
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="shrink-0">
|
|
526
|
+
<line x1="2" y1="12" x2="22" y2="12" />
|
|
527
|
+
<polyline points="8 8 4 12 8 16" />
|
|
528
|
+
<polyline points="16 8 20 12 16 16" />
|
|
529
|
+
</svg>
|
|
530
|
+
New context
|
|
531
|
+
{msg.time ? ` · ${new Date(msg.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}` : ''}
|
|
532
|
+
</span>
|
|
533
|
+
{sessionId && originalIndex >= 0 && (
|
|
534
|
+
<button
|
|
535
|
+
type="button"
|
|
536
|
+
onClick={async () => {
|
|
537
|
+
try {
|
|
538
|
+
await api('DELETE', `/sessions/${sessionId}/messages`, { messageIndex: originalIndex })
|
|
539
|
+
setMessages(messages.filter((_: Message, idx: number) => idx !== originalIndex))
|
|
540
|
+
} catch { /* best-effort */ }
|
|
541
|
+
}}
|
|
542
|
+
className="opacity-0 group-hover/ctx:opacity-100 text-[10px] font-600 text-amber-400/60 hover:text-amber-400 bg-transparent border-none cursor-pointer transition-all px-1.5 py-0.5 rounded-[4px] hover:bg-amber-400/10"
|
|
543
|
+
title="Undo — restore full context"
|
|
544
|
+
>
|
|
545
|
+
Undo
|
|
546
|
+
</button>
|
|
547
|
+
)}
|
|
548
|
+
<div className="flex-1 h-px bg-amber-400/20" />
|
|
549
|
+
</div>
|
|
550
|
+
)
|
|
551
|
+
}
|
|
552
|
+
|
|
350
553
|
// Find original index in the full messages array for API calls
|
|
351
554
|
const originalIndex = messages.indexOf(msg)
|
|
352
555
|
const isLastAssistant = msg.role === 'assistant' && !streaming
|
|
@@ -358,8 +561,25 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
358
561
|
const prevMsg = i > 0 ? filteredMessages[i - 1] : null
|
|
359
562
|
const showDateSep = msg.time && (!prevMsg?.time || new Date(msg.time).toDateString() !== new Date(prevMsg.time).toDateString())
|
|
360
563
|
|
|
564
|
+
// Moment overlay — only on the last assistant message
|
|
565
|
+
let momentOverlay: React.ReactNode = null
|
|
566
|
+
if (isLastAssistant && currentMoment && !streaming) {
|
|
567
|
+
if (currentMoment.kind === 'heartbeat') {
|
|
568
|
+
momentOverlay = <HeartbeatMoment onDismiss={() => setCurrentMoment(null)} />
|
|
569
|
+
} else {
|
|
570
|
+
momentOverlay = (
|
|
571
|
+
<ActivityMoment
|
|
572
|
+
key={`${currentMoment.name}-${Date.now()}`}
|
|
573
|
+
toolName={currentMoment.name}
|
|
574
|
+
toolInput={currentMoment.input}
|
|
575
|
+
onDismiss={() => setCurrentMoment(null)}
|
|
576
|
+
/>
|
|
577
|
+
)
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
361
581
|
return (
|
|
362
|
-
<div key={`${msg.time}-${i}`}>
|
|
582
|
+
<div key={`${msg.time}-${i}`} data-message-index={i}>
|
|
363
583
|
{showDateSep && (
|
|
364
584
|
<div className="flex items-center gap-4 py-2 mb-2">
|
|
365
585
|
<div className="flex-1 h-px bg-white/[0.06]" />
|
|
@@ -381,6 +601,7 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
381
601
|
onToggleBookmark={toggleBookmark}
|
|
382
602
|
onEditResend={handleEditResend}
|
|
383
603
|
onFork={handleFork}
|
|
604
|
+
momentOverlay={momentOverlay}
|
|
384
605
|
/>
|
|
385
606
|
</div>
|
|
386
607
|
</div>
|
|
@@ -389,11 +610,12 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
389
610
|
<ApprovalCards agentId={agent?.id} />
|
|
390
611
|
{streaming && !displayText && <ThinkingIndicator assistantName={assistantName} agentAvatarSeed={agent?.avatarSeed} agentName={agent?.name} />}
|
|
391
612
|
{streaming && displayText && <StreamingBubble text={displayText} assistantName={assistantName} agentAvatarSeed={agent?.avatarSeed} agentName={agent?.name} />}
|
|
392
|
-
{!streaming && filteredMessages.length > 0 && filteredMessages[filteredMessages.length - 1]?.role === 'assistant' && (
|
|
613
|
+
{appSettings.suggestionsEnabled !== false && !streaming && filteredMessages.length > 0 && filteredMessages[filteredMessages.length - 1]?.role === 'assistant' && (
|
|
393
614
|
<SuggestionsBar lastMessage={filteredMessages[filteredMessages.length - 1]} onSend={sendMessage} />
|
|
394
615
|
)}
|
|
395
616
|
</div>
|
|
396
617
|
</div>
|
|
618
|
+
{showGatewayOverlay && <GatewayDisconnectOverlay />}
|
|
397
619
|
{showScrollToBottom && (
|
|
398
620
|
<button
|
|
399
621
|
onClick={handleScrollToBottom}
|
|
@@ -1,20 +1,38 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useMemo, useState } from 'react'
|
|
4
|
-
import ReactMarkdown from 'react-markdown'
|
|
5
|
-
import remarkGfm from 'remark-gfm'
|
|
6
|
-
import rehypeHighlight from 'rehype-highlight'
|
|
7
4
|
import { AiAvatar } from '@/components/shared/avatar'
|
|
8
5
|
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
6
|
+
import { ToolCallBubble, extractMedia } from './tool-call-bubble'
|
|
7
|
+
import { ActivityMoment, isNotableTool } from './activity-moment'
|
|
11
8
|
import { useChatStore, type ToolEvent } from '@/stores/use-chat-store'
|
|
9
|
+
import { isStructuredMarkdown } from './markdown-utils'
|
|
12
10
|
|
|
13
11
|
function ToolEventsSection({ toolEvents }: { toolEvents: ToolEvent[] }) {
|
|
14
12
|
const [expanded, setExpanded] = useState(false)
|
|
15
13
|
const shouldCollapse = toolEvents.length > 2
|
|
16
14
|
const latestTool = toolEvents[toolEvents.length - 1]
|
|
17
15
|
|
|
16
|
+
// When collapsed, collect deduplicated media from all tool events so files remain visible
|
|
17
|
+
const collapsedMedia = useMemo(() => {
|
|
18
|
+
if (!shouldCollapse || expanded) return null
|
|
19
|
+
const seen = new Set<string>()
|
|
20
|
+
const images: string[] = []
|
|
21
|
+
const videos: string[] = []
|
|
22
|
+
const pdfs: { name: string; url: string }[] = []
|
|
23
|
+
const files: { name: string; url: string }[] = []
|
|
24
|
+
for (const ev of toolEvents) {
|
|
25
|
+
if (!ev.output) continue
|
|
26
|
+
const m = extractMedia(ev.output)
|
|
27
|
+
for (const url of m.images) { if (!seen.has(url)) { seen.add(url); images.push(url) } }
|
|
28
|
+
for (const url of m.videos) { if (!seen.has(url)) { seen.add(url); videos.push(url) } }
|
|
29
|
+
for (const p of m.pdfs) { if (!seen.has(p.url)) { seen.add(p.url); pdfs.push(p) } }
|
|
30
|
+
for (const f of m.files) { if (!seen.has(f.url)) { seen.add(f.url); files.push(f) } }
|
|
31
|
+
}
|
|
32
|
+
if (!images.length && !videos.length && !pdfs.length && !files.length) return null
|
|
33
|
+
return { images, videos, pdfs, files }
|
|
34
|
+
}, [toolEvents, shouldCollapse, expanded])
|
|
35
|
+
|
|
18
36
|
if (shouldCollapse && !expanded) {
|
|
19
37
|
return (
|
|
20
38
|
<div className="max-w-[85%] md:max-w-[72%] flex flex-col gap-2 mb-2">
|
|
@@ -33,6 +51,33 @@ function ToolEventsSection({ toolEvents }: { toolEvents: ToolEvent[] }) {
|
|
|
33
51
|
latest: {latestTool?.name || 'unknown'}
|
|
34
52
|
</span>
|
|
35
53
|
</button>
|
|
54
|
+
{collapsedMedia && (
|
|
55
|
+
<>
|
|
56
|
+
{collapsedMedia.images.map((src, i) => (
|
|
57
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
58
|
+
<img key={`ci-${i}`} src={src} alt={`Screenshot ${i + 1}`} loading="lazy"
|
|
59
|
+
className="max-w-[400px] rounded-[10px] border border-white/10"
|
|
60
|
+
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }} />
|
|
61
|
+
))}
|
|
62
|
+
{collapsedMedia.videos.map((src, i) => (
|
|
63
|
+
<video key={`cv-${i}`} src={src} controls playsInline preload="none" className="max-w-full rounded-[10px] border border-white/10" />
|
|
64
|
+
))}
|
|
65
|
+
{collapsedMedia.pdfs.map((file, i) => (
|
|
66
|
+
<div key={`cp-${i}`} className="rounded-[10px] border border-white/10 overflow-hidden">
|
|
67
|
+
<iframe src={file.url} loading="lazy" className="w-full h-[400px] bg-white" title={file.name} />
|
|
68
|
+
</div>
|
|
69
|
+
))}
|
|
70
|
+
{collapsedMedia.files.map((file, i) => (
|
|
71
|
+
<a key={`cf-${i}`} href={file.url} download className="flex items-center gap-2 px-3 py-2 rounded-[10px] border border-white/10 bg-surface/60 text-[13px] text-text-2 no-underline">
|
|
72
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
73
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
74
|
+
<polyline points="14 2 14 8 20 8" />
|
|
75
|
+
</svg>
|
|
76
|
+
{file.name}
|
|
77
|
+
</a>
|
|
78
|
+
))}
|
|
79
|
+
</>
|
|
80
|
+
)}
|
|
36
81
|
</div>
|
|
37
82
|
)
|
|
38
83
|
}
|
|
@@ -63,18 +108,46 @@ interface Props {
|
|
|
63
108
|
}
|
|
64
109
|
|
|
65
110
|
export function StreamingBubble({ text, assistantName, agentAvatarSeed, agentName }: Props) {
|
|
66
|
-
const rendered = useMemo(() => text, [text])
|
|
67
111
|
const toolEvents = useChatStore((s) => s.toolEvents)
|
|
68
112
|
const streamPhase = useChatStore((s) => s.streamPhase)
|
|
69
113
|
const streamToolName = useChatStore((s) => s.streamToolName)
|
|
114
|
+
const thinkingText = useChatStore((s) => s.thinkingText)
|
|
115
|
+
const wide = useMemo(() => isStructuredMarkdown(text), [text])
|
|
116
|
+
|
|
117
|
+
// Track which activity moments have been dismissed
|
|
118
|
+
const [dismissedIds, setDismissedIds] = useState<Set<string>>(new Set())
|
|
119
|
+
|
|
120
|
+
// Find the latest completed notable tool event that hasn't been dismissed
|
|
121
|
+
let currentMoment: { id: string; name: string; input: string } | null = null
|
|
122
|
+
for (let i = toolEvents.length - 1; i >= 0; i--) {
|
|
123
|
+
const event = toolEvents[i]
|
|
124
|
+
if (event.status === 'done' && isNotableTool(event.name) && !dismissedIds.has(event.id)) {
|
|
125
|
+
currentMoment = { id: event.id, name: event.name, input: event.input }
|
|
126
|
+
break
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const handleDismiss = (momentId: string) => {
|
|
131
|
+
setDismissedIds((prev) => new Set(prev).add(momentId))
|
|
132
|
+
}
|
|
70
133
|
|
|
71
134
|
return (
|
|
72
135
|
<div
|
|
73
|
-
className="flex flex-col items-start"
|
|
136
|
+
className="flex flex-col items-start relative pl-[44px]"
|
|
74
137
|
style={{ animation: 'msg-in-left 0.35s cubic-bezier(0.16, 1, 0.3, 1)' }}
|
|
75
138
|
>
|
|
139
|
+
<div className="absolute left-[4px] top-0 relative">
|
|
140
|
+
{agentName ? <AgentAvatar seed={agentAvatarSeed || null} name={agentName} size={28} /> : <AiAvatar size="sm" mood={streamPhase === 'tool' ? 'tool' : 'thinking'} />}
|
|
141
|
+
{currentMoment && (
|
|
142
|
+
<ActivityMoment
|
|
143
|
+
key={currentMoment.id}
|
|
144
|
+
toolName={currentMoment.name}
|
|
145
|
+
toolInput={currentMoment.input}
|
|
146
|
+
onDismiss={() => handleDismiss(currentMoment!.id)}
|
|
147
|
+
/>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
76
150
|
<div className="flex items-center gap-2.5 mb-2 px-1">
|
|
77
|
-
{agentName ? <AgentAvatar seed={agentAvatarSeed || null} name={agentName} size={36} /> : <AiAvatar size="sm" mood={streamPhase === 'tool' ? 'tool' : 'thinking'} />}
|
|
78
151
|
<span className="text-[12px] font-600 text-text-3">{assistantName || 'Claude'}</span>
|
|
79
152
|
<span className="w-2 h-2 rounded-full bg-accent-bright" style={{ animation: 'pulse 1.5s ease infinite' }} />
|
|
80
153
|
{streamPhase === 'tool' && streamToolName && (
|
|
@@ -82,50 +155,34 @@ export function StreamingBubble({ text, assistantName, agentAvatarSeed, agentNam
|
|
|
82
155
|
)}
|
|
83
156
|
</div>
|
|
84
157
|
|
|
158
|
+
{/* Collapsed thinking section (shown when text has started but thinking exists) */}
|
|
159
|
+
{text && thinkingText && (
|
|
160
|
+
<div className="max-w-[85%] md:max-w-[72%] mb-2">
|
|
161
|
+
<details className="group rounded-[12px] border border-purple-500/15 bg-purple-500/[0.04]">
|
|
162
|
+
<summary className="flex items-center gap-2 px-3.5 py-2.5 cursor-pointer select-none list-none [&::-webkit-details-marker]:hidden">
|
|
163
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-purple-400/60 shrink-0 transition-transform group-open:rotate-90">
|
|
164
|
+
<polyline points="9 18 15 12 9 6" />
|
|
165
|
+
</svg>
|
|
166
|
+
<span className="text-[11px] font-600 text-purple-400/70 uppercase tracking-[0.05em]">Thinking</span>
|
|
167
|
+
</summary>
|
|
168
|
+
<div className="px-3.5 pb-3 pt-1 max-h-[300px] overflow-y-auto">
|
|
169
|
+
<div className="text-[13px] leading-[1.6] text-text-3/70 whitespace-pre-wrap break-words">
|
|
170
|
+
{thinkingText}
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</details>
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
176
|
+
|
|
85
177
|
{/* Tool call events (collapsible when > 2) */}
|
|
86
178
|
{toolEvents.length > 0 && (
|
|
87
179
|
<ToolEventsSection toolEvents={toolEvents} />
|
|
88
180
|
)}
|
|
89
181
|
|
|
90
|
-
{
|
|
91
|
-
<div className=
|
|
92
|
-
<div className="
|
|
93
|
-
|
|
94
|
-
remarkPlugins={[remarkGfm]}
|
|
95
|
-
rehypePlugins={[rehypeHighlight]}
|
|
96
|
-
components={{
|
|
97
|
-
pre({ children }) {
|
|
98
|
-
return <pre>{children}</pre>
|
|
99
|
-
},
|
|
100
|
-
code({ className, children }) {
|
|
101
|
-
const isBlock = className?.startsWith('language-') || className?.startsWith('hljs')
|
|
102
|
-
if (isBlock) {
|
|
103
|
-
return <CodeBlock className={className}>{children}</CodeBlock>
|
|
104
|
-
}
|
|
105
|
-
return <code className={className}>{children}</code>
|
|
106
|
-
},
|
|
107
|
-
a({ href, children }) {
|
|
108
|
-
if (!href) return <>{children}</>
|
|
109
|
-
const ytMatch = href.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/)
|
|
110
|
-
if (ytMatch) {
|
|
111
|
-
return (
|
|
112
|
-
<div className="my-2">
|
|
113
|
-
<iframe
|
|
114
|
-
src={`https://www.youtube-nocookie.com/embed/${ytMatch[1]}`}
|
|
115
|
-
className="w-full aspect-video rounded-[10px] border border-white/10"
|
|
116
|
-
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
117
|
-
allowFullScreen
|
|
118
|
-
title="YouTube video"
|
|
119
|
-
/>
|
|
120
|
-
</div>
|
|
121
|
-
)
|
|
122
|
-
}
|
|
123
|
-
return <a href={href} target="_blank" rel="noopener noreferrer">{children}</a>
|
|
124
|
-
},
|
|
125
|
-
}}
|
|
126
|
-
>
|
|
127
|
-
{rendered}
|
|
128
|
-
</ReactMarkdown>
|
|
182
|
+
{text && (
|
|
183
|
+
<div className={`${wide ? 'max-w-[92%] md:max-w-[85%]' : 'max-w-[85%] md:max-w-[72%]'} bubble-ai px-5 py-3.5`}>
|
|
184
|
+
<div className="streaming-cursor text-[15px] leading-[1.7] break-words text-text whitespace-pre-wrap">
|
|
185
|
+
{text}
|
|
129
186
|
</div>
|
|
130
187
|
</div>
|
|
131
188
|
)}
|
|
@@ -53,7 +53,7 @@ export function SuggestionsBar({ lastMessage, onSend }: Props) {
|
|
|
53
53
|
|
|
54
54
|
return (
|
|
55
55
|
<div
|
|
56
|
-
className="flex flex-wrap gap-2 px-1 pt-2"
|
|
56
|
+
className="flex flex-wrap gap-2 px-1 pt-2 ml-10"
|
|
57
57
|
style={{ animation: 'fade-in 0.3s cubic-bezier(0.16, 1, 0.3, 1)' }}
|
|
58
58
|
>
|
|
59
59
|
{suggestions.map((text) => (
|