@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,146 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState, type ReactNode } from 'react'
|
|
4
|
+
|
|
5
|
+
function extractText(node: ReactNode): string {
|
|
6
|
+
if (typeof node === 'string') return node
|
|
7
|
+
if (typeof node === 'number') return String(node)
|
|
8
|
+
if (!node) return ''
|
|
9
|
+
if (Array.isArray(node)) return node.map(extractText).join('')
|
|
10
|
+
if (typeof node === 'object' && 'props' in node) {
|
|
11
|
+
return extractText((node as any).props.children)
|
|
12
|
+
}
|
|
13
|
+
return ''
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
children: ReactNode
|
|
18
|
+
className?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const PREVIEWABLE = new Set(['html', 'htm', 'svg'])
|
|
22
|
+
|
|
23
|
+
export function CodeBlock({ children, className }: Props) {
|
|
24
|
+
const [copied, setCopied] = useState(false)
|
|
25
|
+
const [previewing, setPreviewing] = useState(false)
|
|
26
|
+
const language = className?.replace(/hljs\s*/g, '').replace(/language-/g, '').trim() || ''
|
|
27
|
+
const canPreview = PREVIEWABLE.has(language)
|
|
28
|
+
|
|
29
|
+
const getText = useCallback(() => extractText(children), [children])
|
|
30
|
+
|
|
31
|
+
const handleCopy = useCallback(() => {
|
|
32
|
+
navigator.clipboard.writeText(getText()).then(() => {
|
|
33
|
+
setCopied(true)
|
|
34
|
+
setTimeout(() => setCopied(false), 2000)
|
|
35
|
+
})
|
|
36
|
+
}, [getText])
|
|
37
|
+
|
|
38
|
+
const handlePreview = useCallback(() => {
|
|
39
|
+
setPreviewing((v) => !v)
|
|
40
|
+
}, [])
|
|
41
|
+
|
|
42
|
+
const handleOpenTab = useCallback(() => {
|
|
43
|
+
const text = getText()
|
|
44
|
+
const blob = new Blob([text], { type: language === 'svg' ? 'image/svg+xml' : 'text/html' })
|
|
45
|
+
window.open(URL.createObjectURL(blob), '_blank')
|
|
46
|
+
}, [getText, language])
|
|
47
|
+
|
|
48
|
+
const handleSave = useCallback(() => {
|
|
49
|
+
const text = getText()
|
|
50
|
+
const ext = language || 'txt'
|
|
51
|
+
const blob = new Blob([text], { type: 'text/plain' })
|
|
52
|
+
const a = document.createElement('a')
|
|
53
|
+
a.href = URL.createObjectURL(blob)
|
|
54
|
+
a.download = `code.${ext}`
|
|
55
|
+
a.click()
|
|
56
|
+
}, [getText, language])
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="relative group/code">
|
|
60
|
+
<div className="flex items-center justify-between px-4 py-2 bg-black/30 border-b border-white/[0.03]">
|
|
61
|
+
<span className="text-[10px] font-600 uppercase tracking-[0.08em] text-text-3 font-mono">{language}</span>
|
|
62
|
+
<div className="flex items-center gap-1">
|
|
63
|
+
{canPreview && (
|
|
64
|
+
<>
|
|
65
|
+
<button
|
|
66
|
+
onClick={handlePreview}
|
|
67
|
+
className={`flex items-center gap-1.5 text-[10px] font-600 bg-transparent border-none cursor-pointer
|
|
68
|
+
transition-all duration-200 px-2 py-0.5 rounded-[6px]
|
|
69
|
+
${previewing ? 'text-accent-bright' : 'text-text-3/50 hover:text-text-2 hover:bg-white/[0.04]'}`}
|
|
70
|
+
style={{ fontFamily: 'inherit' }}
|
|
71
|
+
>
|
|
72
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
73
|
+
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
|
74
|
+
<circle cx="12" cy="12" r="3" />
|
|
75
|
+
</svg>
|
|
76
|
+
{previewing ? 'Code' : 'Preview'}
|
|
77
|
+
</button>
|
|
78
|
+
<button
|
|
79
|
+
onClick={handleOpenTab}
|
|
80
|
+
className="flex items-center gap-1.5 text-[10px] font-600 bg-transparent border-none cursor-pointer
|
|
81
|
+
transition-all duration-200 px-2 py-0.5 rounded-[6px] text-text-3/50 hover:text-text-2 hover:bg-white/[0.04]"
|
|
82
|
+
style={{ fontFamily: 'inherit' }}
|
|
83
|
+
title="Open in new tab"
|
|
84
|
+
>
|
|
85
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
86
|
+
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
|
|
87
|
+
<polyline points="15 3 21 3 21 9" />
|
|
88
|
+
<line x1="10" y1="14" x2="21" y2="3" />
|
|
89
|
+
</svg>
|
|
90
|
+
Open
|
|
91
|
+
</button>
|
|
92
|
+
</>
|
|
93
|
+
)}
|
|
94
|
+
<button
|
|
95
|
+
onClick={handleSave}
|
|
96
|
+
className="flex items-center gap-1.5 text-[10px] font-600 bg-transparent border-none cursor-pointer
|
|
97
|
+
transition-all duration-200 px-2 py-0.5 rounded-[6px] text-text-3/50 hover:text-text-2 hover:bg-white/[0.04]"
|
|
98
|
+
style={{ fontFamily: 'inherit' }}
|
|
99
|
+
>
|
|
100
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
101
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
102
|
+
<polyline points="7 10 12 15 17 10" />
|
|
103
|
+
<line x1="12" y1="15" x2="12" y2="3" />
|
|
104
|
+
</svg>
|
|
105
|
+
Save
|
|
106
|
+
</button>
|
|
107
|
+
<button
|
|
108
|
+
onClick={handleCopy}
|
|
109
|
+
className={`flex items-center gap-1.5 text-[10px] font-600 bg-transparent border-none cursor-pointer
|
|
110
|
+
transition-all duration-200 px-2 py-0.5 rounded-[6px]
|
|
111
|
+
${copied
|
|
112
|
+
? 'text-success'
|
|
113
|
+
: 'text-text-3/50 hover:text-text-2 hover:bg-white/[0.04]'}`}
|
|
114
|
+
style={{ fontFamily: 'inherit' }}
|
|
115
|
+
>
|
|
116
|
+
{copied ? (
|
|
117
|
+
<>
|
|
118
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><polyline points="20 6 9 17 4 12" /></svg>
|
|
119
|
+
Copied
|
|
120
|
+
</>
|
|
121
|
+
) : (
|
|
122
|
+
<>
|
|
123
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
124
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
|
125
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
126
|
+
</svg>
|
|
127
|
+
Copy
|
|
128
|
+
</>
|
|
129
|
+
)}
|
|
130
|
+
</button>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
{canPreview && previewing ? (
|
|
134
|
+
<iframe
|
|
135
|
+
srcDoc={getText()}
|
|
136
|
+
sandbox="allow-scripts"
|
|
137
|
+
className="w-full border-none bg-white rounded-b-[8px]"
|
|
138
|
+
style={{ minHeight: 300, maxHeight: 600 }}
|
|
139
|
+
title="Code preview"
|
|
140
|
+
/>
|
|
141
|
+
) : (
|
|
142
|
+
<code className={className}>{children}</code>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { DevServerStatus } from '@/types'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
status: DevServerStatus | null
|
|
7
|
+
onStop: () => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function DevServerBar({ status, onStop }: Props) {
|
|
11
|
+
if (!status) return null
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className="flex items-center gap-2.5 px-4 py-2 bg-success/[0.04] border-b border-white/[0.04] shrink-0">
|
|
15
|
+
<span className="w-[5px] h-[5px] rounded-full bg-success shrink-0"
|
|
16
|
+
style={{ animation: 'pulse 2s ease infinite' }} />
|
|
17
|
+
{status.url ? (
|
|
18
|
+
<a
|
|
19
|
+
href={status.url}
|
|
20
|
+
target="_blank"
|
|
21
|
+
rel="noreferrer"
|
|
22
|
+
className="text-success font-mono text-[11px] flex-1 no-underline hover:underline"
|
|
23
|
+
>
|
|
24
|
+
{status.url}
|
|
25
|
+
</a>
|
|
26
|
+
) : (
|
|
27
|
+
<span className="text-success font-mono text-[11px] flex-1">Starting...</span>
|
|
28
|
+
)}
|
|
29
|
+
<button
|
|
30
|
+
onClick={onStop}
|
|
31
|
+
className="px-2.5 py-1 rounded-[8px] border border-danger/15 bg-transparent
|
|
32
|
+
text-danger text-[11px] font-600 cursor-pointer hover:bg-danger-soft transition-all duration-200"
|
|
33
|
+
style={{ fontFamily: 'inherit' }}
|
|
34
|
+
>
|
|
35
|
+
Stop
|
|
36
|
+
</button>
|
|
37
|
+
</div>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { memo, useState, useCallback, useEffect } from 'react'
|
|
4
|
+
import ReactMarkdown from 'react-markdown'
|
|
5
|
+
import remarkGfm from 'remark-gfm'
|
|
6
|
+
import rehypeHighlight from 'rehype-highlight'
|
|
7
|
+
import type { Message } from '@/types'
|
|
8
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
9
|
+
import { AiAvatar } from '@/components/shared/avatar'
|
|
10
|
+
import { CodeBlock } from './code-block'
|
|
11
|
+
import { ToolCallBubble } from './tool-call-bubble'
|
|
12
|
+
import { ToolRequestBanner } from './tool-request-banner'
|
|
13
|
+
import { api } from '@/lib/api-client'
|
|
14
|
+
|
|
15
|
+
const FILE_PATH_RE = /^(\/[\w./-]+\.\w{1,10})$/
|
|
16
|
+
const DIR_PATH_RE = /^(\/[\w./-]+)\/?$/
|
|
17
|
+
const PREVIEWABLE_EXT = /\.(html?|svg|css|js|jsx|ts|tsx|json|md|txt|py|sh)$/i
|
|
18
|
+
const SERVEABLE_EXT = /\.(html?|svg|css|js|jsx|ts|tsx)$/i
|
|
19
|
+
|
|
20
|
+
function FilePathChip({ filePath }: { filePath: string }) {
|
|
21
|
+
const canPreview = PREVIEWABLE_EXT.test(filePath)
|
|
22
|
+
const canServe = SERVEABLE_EXT.test(filePath)
|
|
23
|
+
const serveUrl = `/api/files/serve?path=${encodeURIComponent(filePath)}`
|
|
24
|
+
|
|
25
|
+
const [serverState, setServerState] = useState<{
|
|
26
|
+
running: boolean; url?: string; loading: boolean; type?: string; framework?: string
|
|
27
|
+
}>({ running: false, loading: false })
|
|
28
|
+
|
|
29
|
+
// Check if a server is already running for this path on mount
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!canServe) return
|
|
32
|
+
api<{ running: boolean; url?: string; type?: string }>('POST', '/preview-server', { action: 'status', path: filePath })
|
|
33
|
+
.then((res) => { if (res.running) setServerState({ running: true, url: res.url, type: res.type, loading: false }) })
|
|
34
|
+
.catch((err) => console.error('Dev server check failed:', err))
|
|
35
|
+
}, [filePath, canServe])
|
|
36
|
+
|
|
37
|
+
const handleStartServer = async () => {
|
|
38
|
+
setServerState((s) => ({ ...s, loading: true }))
|
|
39
|
+
try {
|
|
40
|
+
const res = await api<{ running: boolean; url?: string; type?: string; framework?: string }>('POST', '/preview-server', { action: 'start', path: filePath })
|
|
41
|
+
setServerState({ running: res.running, url: res.url, type: res.type, framework: res.framework, loading: false })
|
|
42
|
+
} catch {
|
|
43
|
+
setServerState((s) => ({ ...s, loading: false }))
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const handleStopServer = async () => {
|
|
48
|
+
setServerState((s) => ({ ...s, loading: true }))
|
|
49
|
+
try {
|
|
50
|
+
await api('POST', '/preview-server', { action: 'stop', path: filePath })
|
|
51
|
+
setServerState({ running: false, loading: false })
|
|
52
|
+
} catch {
|
|
53
|
+
setServerState((s) => ({ ...s, loading: false }))
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const frameworkLabel = serverState.framework
|
|
58
|
+
? serverState.framework.charAt(0).toUpperCase() + serverState.framework.slice(1)
|
|
59
|
+
: null
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<span className="inline-flex items-center gap-1.5 px-2 py-1 rounded-[8px] bg-white/[0.06] border border-white/[0.08] font-mono text-[13px]">
|
|
63
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-text-3/50 shrink-0">
|
|
64
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
65
|
+
<polyline points="14 2 14 8 20 8" />
|
|
66
|
+
</svg>
|
|
67
|
+
<span className="text-sky-400">{filePath}</span>
|
|
68
|
+
{canPreview && !serverState.running && (
|
|
69
|
+
<a
|
|
70
|
+
href={serveUrl}
|
|
71
|
+
target="_blank"
|
|
72
|
+
rel="noopener noreferrer"
|
|
73
|
+
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-[4px] bg-white/[0.06] hover:bg-white/[0.10] text-[10px] font-600 text-text-3 hover:text-text-2 no-underline transition-colors cursor-pointer"
|
|
74
|
+
title="Open file"
|
|
75
|
+
>
|
|
76
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
77
|
+
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
|
|
78
|
+
<polyline points="15 3 21 3 21 9" />
|
|
79
|
+
<line x1="10" y1="14" x2="21" y2="3" />
|
|
80
|
+
</svg>
|
|
81
|
+
Open
|
|
82
|
+
</a>
|
|
83
|
+
)}
|
|
84
|
+
{canServe && !serverState.running && (
|
|
85
|
+
<button
|
|
86
|
+
onClick={handleStartServer}
|
|
87
|
+
disabled={serverState.loading}
|
|
88
|
+
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-[4px] bg-emerald-500/15 hover:bg-emerald-500/25 text-emerald-400 text-[10px] font-600 border-none cursor-pointer transition-colors disabled:opacity-50"
|
|
89
|
+
title="Start preview server — auto-detects npm projects (React, Next, Vite, etc.) and runs the dev command"
|
|
90
|
+
>
|
|
91
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor">
|
|
92
|
+
<polygon points="5 3 19 12 5 21 5 3" />
|
|
93
|
+
</svg>
|
|
94
|
+
{serverState.loading ? 'Starting...' : 'Serve'}
|
|
95
|
+
</button>
|
|
96
|
+
)}
|
|
97
|
+
{canServe && serverState.running && (
|
|
98
|
+
<>
|
|
99
|
+
{frameworkLabel && (
|
|
100
|
+
<span className="px-1.5 py-0.5 rounded-[4px] bg-indigo-500/15 text-indigo-300 text-[9px] font-700 uppercase tracking-wider">
|
|
101
|
+
{frameworkLabel}
|
|
102
|
+
</span>
|
|
103
|
+
)}
|
|
104
|
+
{serverState.type === 'npm' && (
|
|
105
|
+
<span className="px-1.5 py-0.5 rounded-[4px] bg-amber-500/15 text-amber-300 text-[9px] font-700 uppercase tracking-wider">
|
|
106
|
+
npm
|
|
107
|
+
</span>
|
|
108
|
+
)}
|
|
109
|
+
<a
|
|
110
|
+
href={serverState.url}
|
|
111
|
+
target="_blank"
|
|
112
|
+
rel="noopener noreferrer"
|
|
113
|
+
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-[4px] bg-emerald-500/15 hover:bg-emerald-500/25 text-emerald-400 text-[10px] font-600 no-underline transition-colors"
|
|
114
|
+
title="Open preview server"
|
|
115
|
+
>
|
|
116
|
+
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400" style={{ animation: 'pulse 2s ease infinite' }} />
|
|
117
|
+
{serverState.url}
|
|
118
|
+
</a>
|
|
119
|
+
<button
|
|
120
|
+
onClick={handleStopServer}
|
|
121
|
+
disabled={serverState.loading}
|
|
122
|
+
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-[4px] bg-red-500/15 hover:bg-red-500/25 text-red-400 text-[10px] font-600 border-none cursor-pointer transition-colors disabled:opacity-50"
|
|
123
|
+
title="Stop preview server"
|
|
124
|
+
>
|
|
125
|
+
<svg width="8" height="8" viewBox="0 0 24 24" fill="currentColor">
|
|
126
|
+
<rect x="4" y="4" width="16" height="16" rx="2" />
|
|
127
|
+
</svg>
|
|
128
|
+
Stop
|
|
129
|
+
</button>
|
|
130
|
+
</>
|
|
131
|
+
)}
|
|
132
|
+
</span>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function fmtTime(ts: number): string {
|
|
137
|
+
return new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function relativeTime(ts: number): string {
|
|
141
|
+
const now = Date.now()
|
|
142
|
+
const diff = now - ts
|
|
143
|
+
if (diff < 60_000) return 'just now'
|
|
144
|
+
if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`
|
|
145
|
+
if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`
|
|
146
|
+
const d = new Date(ts)
|
|
147
|
+
const today = new Date()
|
|
148
|
+
if (d.toDateString() === today.toDateString()) return fmtTime(ts)
|
|
149
|
+
if (diff < 604_800_000) return d.toLocaleDateString([], { weekday: 'short', hour: '2-digit', minute: '2-digit' })
|
|
150
|
+
return d.toLocaleDateString([], { month: 'short', day: 'numeric' })
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function heartbeatSummary(text: string): string {
|
|
154
|
+
const clean = (text || '')
|
|
155
|
+
.replace(/\bHEARTBEAT_OK\b/gi, '')
|
|
156
|
+
.replace(/\*\*(.*?)\*\*/g, '$1')
|
|
157
|
+
.replace(/\*(.*?)\*/g, '$1')
|
|
158
|
+
.replace(/`([^`]+)`/g, '$1')
|
|
159
|
+
.replace(/\[(.*?)\]\([^)]+\)/g, '$1')
|
|
160
|
+
.replace(/\bHeartbeat Response\s*:\s*/gi, '')
|
|
161
|
+
.replace(/\bCurrent (State|Status)\s*:\s*/gi, '')
|
|
162
|
+
.replace(/\bRecent Progress\s*:\s*/gi, '')
|
|
163
|
+
.replace(/\bNext (Step|Immediate Step)\s*:\s*/gi, '')
|
|
164
|
+
.replace(/\bStatus\s*:\s*/gi, '')
|
|
165
|
+
.replace(/\s+/g, ' ')
|
|
166
|
+
.trim()
|
|
167
|
+
if (!clean) return 'No new status update.'
|
|
168
|
+
return clean.length > 180 ? `${clean.slice(0, 180)}...` : clean
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
interface Props {
|
|
172
|
+
message: Message
|
|
173
|
+
assistantName?: string
|
|
174
|
+
isLast?: boolean
|
|
175
|
+
onRetry?: () => void
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export const MessageBubble = memo(function MessageBubble({ message, assistantName, isLast, onRetry }: Props) {
|
|
179
|
+
const isUser = message.role === 'user'
|
|
180
|
+
const isHeartbeat = !isUser && (message.kind === 'heartbeat' || /^\s*HEARTBEAT_OK\b/i.test(message.text || ''))
|
|
181
|
+
const currentUser = useAppStore((s) => s.currentUser)
|
|
182
|
+
const [copied, setCopied] = useState(false)
|
|
183
|
+
const [heartbeatExpanded, setHeartbeatExpanded] = useState(false)
|
|
184
|
+
const [toolEventsExpanded, setToolEventsExpanded] = useState(false)
|
|
185
|
+
const toolEvents = message.toolEvents || []
|
|
186
|
+
const hasToolEvents = !isUser && toolEvents.length > 0
|
|
187
|
+
const visibleToolEvents = toolEventsExpanded ? [...toolEvents].reverse() : toolEvents.slice(-1)
|
|
188
|
+
|
|
189
|
+
const handleCopy = useCallback(() => {
|
|
190
|
+
navigator.clipboard.writeText(message.text).then(() => {
|
|
191
|
+
setCopied(true)
|
|
192
|
+
setTimeout(() => setCopied(false), 2000)
|
|
193
|
+
})
|
|
194
|
+
}, [message.text])
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<div
|
|
198
|
+
className={`group ${isUser ? 'flex flex-col items-end' : 'flex flex-col items-start'}`}
|
|
199
|
+
style={{ animation: `${isUser ? 'msg-in-right' : 'msg-in-left'} 0.35s cubic-bezier(0.16, 1, 0.3, 1)` }}
|
|
200
|
+
>
|
|
201
|
+
{/* Sender label + timestamp */}
|
|
202
|
+
<div className={`flex items-center gap-2.5 mb-2 px-1 ${isUser ? 'flex-row-reverse' : ''}`}>
|
|
203
|
+
{!isUser && <AiAvatar size="sm" />}
|
|
204
|
+
<span className={`text-[12px] font-600 ${isUser ? 'text-accent-bright/70' : 'text-text-3'}`}>
|
|
205
|
+
{isUser ? (currentUser ? currentUser.charAt(0).toUpperCase() + currentUser.slice(1) : 'You') : (assistantName || 'Claude')}
|
|
206
|
+
</span>
|
|
207
|
+
<span className="text-[11px] text-text-3/70 font-mono" title={message.time ? new Date(message.time).toLocaleString() : ''}>
|
|
208
|
+
{message.time ? relativeTime(message.time) : ''}
|
|
209
|
+
</span>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
{/* Tool call events (assistant messages only) */}
|
|
213
|
+
{hasToolEvents && (
|
|
214
|
+
<div className="max-w-[85%] md:max-w-[72%] flex flex-col gap-2 mb-2">
|
|
215
|
+
{toolEvents.length > 1 && (
|
|
216
|
+
<button
|
|
217
|
+
type="button"
|
|
218
|
+
onClick={() => setToolEventsExpanded((v) => !v)}
|
|
219
|
+
className="self-start px-2.5 py-1 rounded-[8px] bg-white/[0.04] hover:bg-white/[0.07] text-[11px] text-text-3 border border-white/[0.06] cursor-pointer transition-colors"
|
|
220
|
+
>
|
|
221
|
+
{toolEventsExpanded ? 'Show latest only' : `Show all tool calls (${toolEvents.length})`}
|
|
222
|
+
</button>
|
|
223
|
+
)}
|
|
224
|
+
<div className={`${toolEventsExpanded ? 'max-h-[320px] overflow-y-auto pr-1 flex flex-col gap-2' : 'flex flex-col gap-2'}`}>
|
|
225
|
+
{visibleToolEvents.map((event, i) => (
|
|
226
|
+
<ToolCallBubble
|
|
227
|
+
key={`${message.time}-tool-${toolEventsExpanded ? `all-${i}` : `latest-${toolEvents.length - 1}`}`}
|
|
228
|
+
event={{
|
|
229
|
+
id: `${message.time}-${toolEventsExpanded ? i : toolEvents.length - 1}`,
|
|
230
|
+
name: event.name,
|
|
231
|
+
input: event.input,
|
|
232
|
+
output: event.output,
|
|
233
|
+
status: event.error ? 'error' : 'done',
|
|
234
|
+
}}
|
|
235
|
+
/>
|
|
236
|
+
))}
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
)}
|
|
240
|
+
|
|
241
|
+
{/* Message bubble */}
|
|
242
|
+
<div className={`max-w-[85%] md:max-w-[72%] ${isUser ? 'bubble-user px-5 py-3.5' : isHeartbeat ? 'bubble-ai px-4 py-3' : 'bubble-ai px-5 py-3.5'}`}>
|
|
243
|
+
{(message.imagePath || message.imageUrl) && (() => {
|
|
244
|
+
const url = message.imageUrl || `/api/uploads/${message.imagePath?.split('/').pop()}`
|
|
245
|
+
const rawName = message.imagePath?.split('/').pop() || message.imageUrl?.split('/').pop() || 'file'
|
|
246
|
+
const filename = rawName.replace(/^[a-f0-9]+-/, '').split('?')[0]
|
|
247
|
+
const isImage = /\.(png|jpg|jpeg|gif|webp|svg|bmp|ico)$/i.test(filename)
|
|
248
|
+
if (isImage) {
|
|
249
|
+
return (
|
|
250
|
+
<img src={url} alt="Attached" className="max-w-[240px] rounded-[12px] mb-3 border border-white/10"
|
|
251
|
+
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }} />
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
const isPreviewable = /\.(html?|svg)$/i.test(filename)
|
|
255
|
+
return (
|
|
256
|
+
<div className="flex items-center gap-3 px-4 py-3 mb-3 rounded-[12px] border border-white/10 bg-white/[0.03]">
|
|
257
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-text-3 shrink-0">
|
|
258
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
259
|
+
<polyline points="14 2 14 8 20 8" />
|
|
260
|
+
</svg>
|
|
261
|
+
<span className="text-[13px] text-text-2 font-500 truncate flex-1">{filename}</span>
|
|
262
|
+
{isPreviewable && (
|
|
263
|
+
<a href={url} target="_blank" rel="noopener noreferrer"
|
|
264
|
+
className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] bg-accent-soft hover:bg-accent-soft/80 text-accent-bright text-[11px] font-600 no-underline transition-colors shrink-0"
|
|
265
|
+
title="Preview in new tab">
|
|
266
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
267
|
+
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
|
268
|
+
<circle cx="12" cy="12" r="3" />
|
|
269
|
+
</svg>
|
|
270
|
+
Preview
|
|
271
|
+
</a>
|
|
272
|
+
)}
|
|
273
|
+
<a href={url} download={filename}
|
|
274
|
+
className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] bg-white/[0.06] hover:bg-white/[0.10] text-text-3 text-[11px] font-600 no-underline transition-colors shrink-0">
|
|
275
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
276
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
277
|
+
<polyline points="7 10 12 15 17 10" />
|
|
278
|
+
<line x1="12" y1="15" x2="12" y2="3" />
|
|
279
|
+
</svg>
|
|
280
|
+
Download
|
|
281
|
+
</a>
|
|
282
|
+
</div>
|
|
283
|
+
)
|
|
284
|
+
})()}
|
|
285
|
+
|
|
286
|
+
{isHeartbeat ? (
|
|
287
|
+
<div className="flex flex-col gap-2">
|
|
288
|
+
<button
|
|
289
|
+
type="button"
|
|
290
|
+
onClick={() => setHeartbeatExpanded((v) => !v)}
|
|
291
|
+
className="w-full rounded-[12px] px-3.5 py-3 border border-white/[0.10] bg-white/[0.02] text-left hover:bg-white/[0.04] transition-colors cursor-pointer"
|
|
292
|
+
>
|
|
293
|
+
<div className="flex items-center justify-between gap-3">
|
|
294
|
+
<div className="flex items-center gap-2">
|
|
295
|
+
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
|
|
296
|
+
<span className="text-[11px] uppercase tracking-[0.08em] text-text-2 font-600">Heartbeat</span>
|
|
297
|
+
</div>
|
|
298
|
+
<span className="text-[11px] text-text-3">{heartbeatExpanded ? 'Collapse' : 'Expand'}</span>
|
|
299
|
+
</div>
|
|
300
|
+
<p className="text-[13px] text-text-2/90 leading-[1.5] mt-1.5">{heartbeatSummary(message.text)}</p>
|
|
301
|
+
</button>
|
|
302
|
+
{heartbeatExpanded && (
|
|
303
|
+
<div className="msg-content text-[14px] leading-[1.7] text-text break-words px-3 py-2 rounded-[10px] border border-white/[0.08] bg-black/20">
|
|
304
|
+
<ReactMarkdown
|
|
305
|
+
remarkPlugins={[remarkGfm]}
|
|
306
|
+
rehypePlugins={[rehypeHighlight]}
|
|
307
|
+
components={{
|
|
308
|
+
pre({ children }) {
|
|
309
|
+
return <pre>{children}</pre>
|
|
310
|
+
},
|
|
311
|
+
code({ className, children }) {
|
|
312
|
+
const isBlock = className?.startsWith('language-') || className?.startsWith('hljs')
|
|
313
|
+
if (isBlock) return <CodeBlock className={className}>{children}</CodeBlock>
|
|
314
|
+
return <code className={className}>{children}</code>
|
|
315
|
+
},
|
|
316
|
+
}}
|
|
317
|
+
>
|
|
318
|
+
{message.text}
|
|
319
|
+
</ReactMarkdown>
|
|
320
|
+
</div>
|
|
321
|
+
)}
|
|
322
|
+
</div>
|
|
323
|
+
) : (
|
|
324
|
+
<div className={`msg-content text-[15px] break-words ${isUser ? 'leading-[1.6] text-white/95' : 'leading-[1.7] text-text'}`}>
|
|
325
|
+
<ReactMarkdown
|
|
326
|
+
remarkPlugins={[remarkGfm]}
|
|
327
|
+
rehypePlugins={[rehypeHighlight]}
|
|
328
|
+
components={{
|
|
329
|
+
pre({ children }) {
|
|
330
|
+
return <pre>{children}</pre>
|
|
331
|
+
},
|
|
332
|
+
code({ className, children }) {
|
|
333
|
+
const isBlock = className?.startsWith('language-') || className?.startsWith('hljs')
|
|
334
|
+
if (isBlock) {
|
|
335
|
+
return <CodeBlock className={className}>{children}</CodeBlock>
|
|
336
|
+
}
|
|
337
|
+
// Detect file/dir paths in inline code and make them interactive
|
|
338
|
+
const text = typeof children === 'string' ? children : ''
|
|
339
|
+
if (text && (FILE_PATH_RE.test(text) || (DIR_PATH_RE.test(text) && text.split('/').length > 2))) {
|
|
340
|
+
return <FilePathChip filePath={text.replace(/\/$/, '')} />
|
|
341
|
+
}
|
|
342
|
+
return <code className={className}>{children}</code>
|
|
343
|
+
},
|
|
344
|
+
img({ src, alt }) {
|
|
345
|
+
if (!src || typeof src !== 'string') return null
|
|
346
|
+
const isVideo = /\.(mp4|webm|mov|avi)$/i.test(src)
|
|
347
|
+
if (isVideo) {
|
|
348
|
+
return (
|
|
349
|
+
<video src={src} controls className="max-w-full rounded-[10px] border border-white/10 my-2" />
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
return (
|
|
353
|
+
<a href={src} download target="_blank" rel="noopener noreferrer" className="block my-2">
|
|
354
|
+
<img src={src} alt={alt || 'File'} className="max-w-full rounded-[10px] border border-white/10 hover:border-white/25 transition-colors cursor-pointer" onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }} />
|
|
355
|
+
</a>
|
|
356
|
+
)
|
|
357
|
+
},
|
|
358
|
+
a({ href, children }) {
|
|
359
|
+
if (!href) return <>{children}</>
|
|
360
|
+
// Internal app links: #task:<id> and #schedule:<id>
|
|
361
|
+
const taskMatch = href.match(/^#task:(.+)$/)
|
|
362
|
+
if (taskMatch) {
|
|
363
|
+
return (
|
|
364
|
+
<button
|
|
365
|
+
type="button"
|
|
366
|
+
onClick={async () => {
|
|
367
|
+
const store = useAppStore.getState()
|
|
368
|
+
await store.loadTasks(true)
|
|
369
|
+
store.setEditingTaskId(taskMatch[1])
|
|
370
|
+
store.setTaskSheetOpen(true)
|
|
371
|
+
}}
|
|
372
|
+
className="inline-flex items-center gap-1 text-purple-400 hover:text-purple-300 underline cursor-pointer bg-transparent border-none p-0 font-inherit text-inherit"
|
|
373
|
+
>
|
|
374
|
+
{children}
|
|
375
|
+
</button>
|
|
376
|
+
)
|
|
377
|
+
}
|
|
378
|
+
const schedMatch = href.match(/^#schedule:(.+)$/)
|
|
379
|
+
if (schedMatch) {
|
|
380
|
+
return (
|
|
381
|
+
<button
|
|
382
|
+
type="button"
|
|
383
|
+
onClick={async () => {
|
|
384
|
+
const store = useAppStore.getState()
|
|
385
|
+
await store.loadSchedules()
|
|
386
|
+
store.setEditingScheduleId(schedMatch[1])
|
|
387
|
+
store.setScheduleSheetOpen(true)
|
|
388
|
+
}}
|
|
389
|
+
className="inline-flex items-center gap-1 text-amber-400 hover:text-amber-300 underline cursor-pointer bg-transparent border-none p-0 font-inherit text-inherit"
|
|
390
|
+
>
|
|
391
|
+
{children}
|
|
392
|
+
</button>
|
|
393
|
+
)
|
|
394
|
+
}
|
|
395
|
+
const isUpload = href.startsWith('/api/uploads/')
|
|
396
|
+
if (isUpload) {
|
|
397
|
+
const uploadIsHtml = /\.(html?|svg)$/i.test(href.split('?')[0])
|
|
398
|
+
return (
|
|
399
|
+
<span className="inline-flex items-center gap-1.5">
|
|
400
|
+
<a href={href} download className="inline-flex items-center gap-1.5 text-sky-400 hover:text-sky-300 underline">
|
|
401
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="shrink-0">
|
|
402
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
403
|
+
<polyline points="7 10 12 15 17 10" />
|
|
404
|
+
<line x1="12" y1="15" x2="12" y2="3" />
|
|
405
|
+
</svg>
|
|
406
|
+
{children}
|
|
407
|
+
</a>
|
|
408
|
+
{uploadIsHtml && (
|
|
409
|
+
<a href={href} target="_blank" rel="noopener noreferrer"
|
|
410
|
+
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-[4px] bg-accent-soft hover:bg-accent-soft/80 text-accent-bright text-[10px] font-600 no-underline transition-colors"
|
|
411
|
+
title="Preview in new tab">
|
|
412
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
413
|
+
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
|
414
|
+
<circle cx="12" cy="12" r="3" />
|
|
415
|
+
</svg>
|
|
416
|
+
Preview
|
|
417
|
+
</a>
|
|
418
|
+
)}
|
|
419
|
+
</span>
|
|
420
|
+
)
|
|
421
|
+
}
|
|
422
|
+
// YouTube embed
|
|
423
|
+
const ytMatch = href.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/)
|
|
424
|
+
if (ytMatch) {
|
|
425
|
+
return (
|
|
426
|
+
<div className="my-2">
|
|
427
|
+
<iframe
|
|
428
|
+
src={`https://www.youtube-nocookie.com/embed/${ytMatch[1]}`}
|
|
429
|
+
className="w-full aspect-video rounded-[10px] border border-white/10"
|
|
430
|
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
431
|
+
allowFullScreen
|
|
432
|
+
title="YouTube video"
|
|
433
|
+
/>
|
|
434
|
+
</div>
|
|
435
|
+
)
|
|
436
|
+
}
|
|
437
|
+
return <a href={href} target="_blank" rel="noopener noreferrer">{children}</a>
|
|
438
|
+
},
|
|
439
|
+
}}
|
|
440
|
+
>
|
|
441
|
+
{message.text}
|
|
442
|
+
</ReactMarkdown>
|
|
443
|
+
</div>
|
|
444
|
+
)}
|
|
445
|
+
</div>
|
|
446
|
+
|
|
447
|
+
{/* Tool access request banners */}
|
|
448
|
+
{!isUser && <ToolRequestBanner
|
|
449
|
+
text={message.text || ''}
|
|
450
|
+
toolOutputs={toolEvents.map((e) => e.output || '').filter(Boolean)}
|
|
451
|
+
/>}
|
|
452
|
+
|
|
453
|
+
{/* Action buttons */}
|
|
454
|
+
<div className={`flex items-center gap-1 mt-1.5 px-1 opacity-0 group-hover:opacity-100 transition-opacity duration-200 ${isUser ? 'justify-end' : ''}`}>
|
|
455
|
+
<button
|
|
456
|
+
onClick={handleCopy}
|
|
457
|
+
aria-label="Copy message"
|
|
458
|
+
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] border-none bg-transparent
|
|
459
|
+
text-[11px] font-500 text-text-3 cursor-pointer hover:text-text-2 hover:bg-white/[0.04] transition-all"
|
|
460
|
+
style={{ fontFamily: 'inherit' }}
|
|
461
|
+
>
|
|
462
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
463
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
|
464
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
465
|
+
</svg>
|
|
466
|
+
{copied ? 'Copied' : 'Copy'}
|
|
467
|
+
</button>
|
|
468
|
+
{!isUser && isLast && onRetry && (
|
|
469
|
+
<button
|
|
470
|
+
onClick={onRetry}
|
|
471
|
+
aria-label="Retry message"
|
|
472
|
+
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] border-none bg-transparent
|
|
473
|
+
text-[11px] font-500 text-text-3 cursor-pointer hover:text-text-2 hover:bg-white/[0.04] transition-all"
|
|
474
|
+
style={{ fontFamily: 'inherit' }}
|
|
475
|
+
>
|
|
476
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
477
|
+
<polyline points="23 4 23 10 17 10" />
|
|
478
|
+
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
|
|
479
|
+
</svg>
|
|
480
|
+
Retry
|
|
481
|
+
</button>
|
|
482
|
+
)}
|
|
483
|
+
</div>
|
|
484
|
+
</div>
|
|
485
|
+
)
|
|
486
|
+
})
|