@swarmclawai/swarmclaw 0.3.0 → 0.4.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 +20 -11
- package/bin/server-cmd.js +14 -7
- package/bin/swarmclaw.js +3 -1
- package/bin/update-cmd.js +120 -0
- package/next.config.ts +2 -0
- package/package.json +3 -1
- package/src/app/api/agents/[id]/route.ts +3 -0
- package/src/app/api/agents/[id]/thread/route.ts +2 -1
- package/src/app/api/agents/route.ts +5 -1
- package/src/app/api/auth/route.ts +3 -1
- package/src/app/api/claude-skills/route.ts +3 -1
- package/src/app/api/connectors/[id]/route.ts +4 -0
- package/src/app/api/connectors/route.ts +6 -1
- package/src/app/api/credentials/route.ts +3 -1
- package/src/app/api/daemon/route.ts +6 -1
- package/src/app/api/ip/route.ts +3 -1
- package/src/app/api/mcp-servers/route.ts +3 -1
- package/src/app/api/orchestrator/graph/route.ts +25 -0
- package/src/app/api/plugins/marketplace/route.ts +3 -1
- package/src/app/api/plugins/route.ts +3 -1
- package/src/app/api/providers/[id]/route.ts +3 -0
- package/src/app/api/providers/configs/route.ts +3 -1
- package/src/app/api/providers/route.ts +5 -1
- package/src/app/api/schedules/[id]/route.ts +3 -0
- package/src/app/api/schedules/route.ts +6 -1
- package/src/app/api/secrets/route.ts +3 -1
- package/src/app/api/sessions/[id]/chat/route.ts +5 -2
- package/src/app/api/sessions/route.ts +9 -2
- package/src/app/api/settings/route.ts +3 -1
- package/src/app/api/setup/doctor/route.ts +1 -0
- package/src/app/api/setup/openclaw-device/route.ts +3 -1
- package/src/app/api/skills/route.ts +3 -1
- package/src/app/api/tasks/[id]/approve/route.ts +73 -0
- package/src/app/api/tasks/[id]/route.ts +3 -0
- package/src/app/api/tasks/route.ts +3 -0
- package/src/app/api/usage/route.ts +3 -1
- package/src/app/api/version/route.ts +3 -1
- package/src/app/api/webhooks/[id]/route.ts +2 -1
- package/src/app/api/webhooks/route.ts +3 -1
- package/src/app/icon.svg +58 -0
- package/src/app/page.tsx +8 -2
- package/src/cli/index.js +1 -9
- package/src/cli/index.ts +51 -1
- package/src/cli/spec.js +0 -8
- package/src/components/agents/agent-card.tsx +1 -1
- package/src/components/agents/agent-sheet.tsx +63 -80
- package/src/components/chat/chat-area.tsx +44 -30
- package/src/components/chat/chat-tool-toggles.tsx +12 -53
- package/src/components/chat/message-bubble.tsx +110 -42
- package/src/components/chat/tool-call-bubble.tsx +41 -3
- package/src/components/chat/tool-request-banner.tsx +1 -9
- package/src/components/connectors/connector-list.tsx +3 -8
- package/src/components/connectors/connector-sheet.tsx +24 -29
- package/src/components/input/chat-input.tsx +72 -56
- package/src/components/knowledge/knowledge-list.tsx +27 -31
- package/src/components/layout/app-layout.tsx +92 -71
- package/src/components/layout/daemon-indicator.tsx +3 -5
- package/src/components/logs/log-list.tsx +5 -9
- package/src/components/mcp-servers/mcp-server-list.tsx +24 -2
- package/src/components/memory/memory-detail.tsx +1 -1
- package/src/components/plugins/plugin-list.tsx +227 -27
- package/src/components/providers/provider-list.tsx +46 -13
- package/src/components/providers/provider-sheet.tsx +0 -45
- package/src/components/runs/run-list.tsx +6 -15
- package/src/components/schedules/schedule-card.tsx +54 -4
- package/src/components/schedules/schedule-list.tsx +6 -3
- package/src/components/schedules/schedule-sheet.tsx +0 -47
- package/src/components/secrets/secrets-list.tsx +20 -2
- package/src/components/sessions/new-session-sheet.tsx +8 -9
- package/src/components/shared/connector-platform-icon.tsx +22 -20
- package/src/components/shared/model-combobox.tsx +148 -0
- package/src/components/shared/settings/section-heartbeat.tsx +7 -39
- package/src/components/shared/settings/section-orchestrator.tsx +8 -9
- package/src/components/skills/skill-list.tsx +260 -34
- package/src/components/skills/skill-sheet.tsx +0 -45
- package/src/components/tasks/task-board.tsx +3 -6
- package/src/components/tasks/task-card.tsx +43 -1
- package/src/components/tasks/task-list.tsx +3 -5
- package/src/components/tasks/task-sheet.tsx +0 -44
- package/src/components/usage/usage-list.tsx +12 -4
- package/src/hooks/use-ws.ts +66 -0
- package/src/instrumentation.ts +2 -0
- package/src/lib/chat.ts +14 -2
- package/src/lib/providers/anthropic.ts +1 -1
- package/src/lib/providers/index.ts +2 -0
- package/src/lib/providers/ollama.ts +1 -1
- package/src/lib/providers/openai.ts +33 -12
- package/src/lib/server/chat-execution.ts +19 -4
- package/src/lib/server/connectors/manager.ts +9 -3
- package/src/lib/server/context-manager.ts +1 -1
- package/src/lib/server/daemon-state.ts +3 -0
- package/src/lib/server/data-dir.ts +1 -0
- package/src/lib/server/heartbeat-service.ts +67 -3
- package/src/lib/server/langgraph-checkpoint.ts +274 -0
- package/src/lib/server/main-agent-loop.ts +61 -2
- package/src/lib/server/orchestrator-lg.ts +394 -13
- package/src/lib/server/orchestrator.ts +25 -5
- package/src/lib/server/queue.ts +17 -3
- package/src/lib/server/session-run-manager.ts +6 -1
- package/src/lib/server/session-tools/delegate.ts +2 -2
- package/src/lib/server/session-tools/index.ts +2 -0
- package/src/lib/server/session-tools/sandbox.ts +164 -0
- package/src/lib/server/storage-mcp.test.ts +25 -2
- package/src/lib/server/storage.ts +24 -7
- package/src/lib/server/stream-agent-chat.ts +77 -22
- package/src/lib/server/task-validation.test.ts +23 -0
- package/src/lib/server/task-validation.ts +5 -3
- package/src/lib/server/ws-hub.ts +85 -0
- package/src/lib/tool-definitions.ts +42 -0
- package/src/lib/upload.ts +7 -1
- package/src/lib/ws-client.ts +124 -0
- package/src/stores/use-chat-store.ts +33 -13
- package/src/types/index.ts +8 -1
- package/src/app/api/agents/generate/route.ts +0 -42
- package/src/app/api/generate/info/route.ts +0 -12
- package/src/app/api/generate/route.ts +0 -106
- package/src/app/favicon.ico +0 -0
- package/src/components/shared/ai-gen-block.tsx +0 -77
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useMemo } from 'react'
|
|
4
4
|
import type { ToolEvent } from '@/stores/use-chat-store'
|
|
5
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
5
6
|
|
|
6
7
|
const TOOL_COLORS: Record<string, string> = {
|
|
7
8
|
execute_command: '#F59E0B',
|
|
@@ -15,6 +16,7 @@ const TOOL_COLORS: Record<string, string> = {
|
|
|
15
16
|
send_file: '#10B981',
|
|
16
17
|
web_search: '#3B82F6',
|
|
17
18
|
web_fetch: '#3B82F6',
|
|
19
|
+
delegate_to_agent: '#6366F1',
|
|
18
20
|
delegate_to_claude_code: '#6366F1',
|
|
19
21
|
delegate_to_codex_cli: '#0EA5E9',
|
|
20
22
|
delegate_to_opencode_cli: '#14B8A6',
|
|
@@ -63,6 +65,7 @@ export const TOOL_LABELS: Record<string, string> = {
|
|
|
63
65
|
claude_code: 'Claude Code',
|
|
64
66
|
codex_cli: 'Codex CLI',
|
|
65
67
|
opencode_cli: 'OpenCode CLI',
|
|
68
|
+
delegate_to_agent: 'Agent Delegation',
|
|
66
69
|
delegate_to_claude_code: 'Claude Code',
|
|
67
70
|
delegate_to_codex_cli: 'Codex CLI',
|
|
68
71
|
delegate_to_opencode_cli: 'OpenCode CLI',
|
|
@@ -96,6 +99,7 @@ export const TOOL_DESCRIPTIONS: Record<string, string> = {
|
|
|
96
99
|
claude_code: 'Enable delegation to Claude Code CLI',
|
|
97
100
|
codex_cli: 'Enable delegation to OpenAI Codex CLI',
|
|
98
101
|
opencode_cli: 'Enable delegation to OpenCode CLI',
|
|
102
|
+
delegate_to_agent: 'Delegate a task to another agent',
|
|
99
103
|
delegate_to_claude_code: 'Delegate complex coding tasks to Claude Code',
|
|
100
104
|
delegate_to_codex_cli: 'Delegate complex coding tasks to Codex CLI',
|
|
101
105
|
delegate_to_opencode_cli: 'Delegate complex coding tasks to OpenCode CLI',
|
|
@@ -181,6 +185,7 @@ function getInputPreview(name: string, input: string): string {
|
|
|
181
185
|
return ''
|
|
182
186
|
}
|
|
183
187
|
if (name === 'send_file') return parsed.filePath || ''
|
|
188
|
+
if (name === 'delegate_to_agent') return `${parsed.agentName}: ${(parsed.task || '').slice(0, 80)}`
|
|
184
189
|
|
|
185
190
|
if (parsed.command) return parsed.command
|
|
186
191
|
if (parsed.filePath) return parsed.filePath
|
|
@@ -280,6 +285,24 @@ export function ToolCallBubble({ event }: { event: ToolEvent }) {
|
|
|
280
285
|
|
|
281
286
|
const hasMedia = media.images.length > 0 || media.videos.length > 0 || media.pdfs.length > 0 || media.files.length > 0
|
|
282
287
|
|
|
288
|
+
// Parse delegation info for clickable agent link
|
|
289
|
+
const delegationInfo = useMemo(() => {
|
|
290
|
+
if (event.name !== 'delegate_to_agent') return null
|
|
291
|
+
try {
|
|
292
|
+
const parsed = JSON.parse(event.input)
|
|
293
|
+
return { agentName: parsed.agentName || '', agentId: parsed.agentId || '', task: parsed.task || '' }
|
|
294
|
+
} catch { return null }
|
|
295
|
+
}, [event.name, event.input])
|
|
296
|
+
|
|
297
|
+
const handleAgentClick = (e: React.MouseEvent) => {
|
|
298
|
+
e.stopPropagation()
|
|
299
|
+
if (delegationInfo?.agentId) {
|
|
300
|
+
const store = useAppStore.getState()
|
|
301
|
+
store.setActiveView('agents')
|
|
302
|
+
store.setCurrentAgent(delegationInfo.agentId)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
283
306
|
return (
|
|
284
307
|
<div className="w-full text-left">
|
|
285
308
|
<button
|
|
@@ -303,9 +326,24 @@ export function ToolCallBubble({ event }: { event: ToolEvent }) {
|
|
|
303
326
|
<span className="text-[12px] font-700 uppercase tracking-wider shrink-0" style={{ color }}>
|
|
304
327
|
{label}
|
|
305
328
|
</span>
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
329
|
+
{delegationInfo ? (
|
|
330
|
+
<span className="text-[12px] text-text-2 font-mono truncate flex-1">
|
|
331
|
+
<span
|
|
332
|
+
role="link"
|
|
333
|
+
tabIndex={0}
|
|
334
|
+
onClick={handleAgentClick}
|
|
335
|
+
onKeyDown={(e) => e.key === 'Enter' && handleAgentClick(e as any)}
|
|
336
|
+
className="text-accent-bright hover:underline cursor-pointer font-600"
|
|
337
|
+
>
|
|
338
|
+
{delegationInfo.agentName}
|
|
339
|
+
</span>
|
|
340
|
+
{delegationInfo.task && <span className="text-text-3">: {delegationInfo.task.slice(0, 80)}</span>}
|
|
341
|
+
</span>
|
|
342
|
+
) : (
|
|
343
|
+
<span className="text-[12px] text-text-2 font-mono truncate flex-1">
|
|
344
|
+
{inputPreview}
|
|
345
|
+
</span>
|
|
346
|
+
)}
|
|
309
347
|
{hasMedia && !expanded && (
|
|
310
348
|
<span className="text-[10px] text-text-3/50 font-500 shrink-0">
|
|
311
349
|
{media.images.length > 0 && `${media.images.length} image${media.images.length > 1 ? 's' : ''}`}
|
|
@@ -3,15 +3,7 @@
|
|
|
3
3
|
import { useState } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
5
|
import { api } from '@/lib/api-client'
|
|
6
|
-
|
|
7
|
-
const TOOL_LABELS: Record<string, string> = {
|
|
8
|
-
shell: 'Shell', files: 'Files', edit_file: 'Edit File', process: 'Process',
|
|
9
|
-
web_search: 'Web Search', web_fetch: 'Web Fetch', browser: 'Browser', memory: 'Memory',
|
|
10
|
-
claude_code: 'Claude Code', codex_cli: 'Codex CLI', opencode_cli: 'OpenCode CLI',
|
|
11
|
-
orchestrator: 'Orchestrator', manage_agents: 'Agents', manage_tasks: 'Tasks', manage_schedules: 'Schedules',
|
|
12
|
-
manage_skills: 'Skills', manage_documents: 'Documents', manage_webhooks: 'Webhooks',
|
|
13
|
-
manage_connectors: 'Connectors', manage_sessions: 'Sessions', manage_secrets: 'Secrets',
|
|
14
|
-
}
|
|
6
|
+
import { TOOL_LABELS } from '@/lib/tool-definitions'
|
|
15
7
|
|
|
16
8
|
interface Props {
|
|
17
9
|
text: string
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useCallback, useEffect, useState } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
import { useWs } from '@/hooks/use-ws'
|
|
5
6
|
import { api } from '@/lib/api-client'
|
|
6
7
|
import type { Connector } from '@/types'
|
|
7
8
|
import { ConnectorPlatformBadge, getConnectorPlatformLabel } from '@/components/shared/connector-platform-icon'
|
|
@@ -24,14 +25,8 @@ export function ConnectorList({ inSidebar: _inSidebar }: { inSidebar?: boolean }
|
|
|
24
25
|
setLoaded(true)
|
|
25
26
|
}, [loadConnectors, loadAgents])
|
|
26
27
|
|
|
27
|
-
useEffect(() => {
|
|
28
|
-
|
|
29
|
-
const poll = setInterval(() => { void loadConnectors() }, 15_000)
|
|
30
|
-
return () => {
|
|
31
|
-
clearTimeout(bootstrap)
|
|
32
|
-
clearInterval(poll)
|
|
33
|
-
}
|
|
34
|
-
}, [refresh, loadConnectors])
|
|
28
|
+
useEffect(() => { void refresh() }, [refresh])
|
|
29
|
+
useWs('connectors', loadConnectors, 15_000)
|
|
35
30
|
|
|
36
31
|
// Auto-clear error after 5s
|
|
37
32
|
useEffect(() => {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect } from 'react'
|
|
3
|
+
import { useState, useEffect, useCallback } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
5
|
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
6
6
|
import { api } from '@/lib/api-client'
|
|
7
|
+
import { useWs } from '@/hooks/use-ws'
|
|
7
8
|
import { toast } from 'sonner'
|
|
8
9
|
import type { Connector, ConnectorPlatform } from '@/types'
|
|
9
10
|
import { ConnectorPlatformBadge } from '@/components/shared/connector-platform-icon'
|
|
@@ -255,35 +256,29 @@ export function ConnectorSheet() {
|
|
|
255
256
|
|
|
256
257
|
// Poll for QR code when WhatsApp connector is running or connecting
|
|
257
258
|
const isWaRunning = editing?.platform === 'whatsapp' && (editing?.status === 'running' || waConnecting)
|
|
258
|
-
|
|
259
|
-
if (!editing
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if (data.status === 'running' && editing.status !== 'running') {
|
|
272
|
-
// Store is stale — update it directly
|
|
273
|
-
const store = useAppStore.getState()
|
|
274
|
-
const updated = { ...store.connectors }
|
|
275
|
-
if (updated[editing.id]) {
|
|
276
|
-
updated[editing.id] = { ...updated[editing.id], status: 'running' as const }
|
|
277
|
-
useAppStore.setState({ connectors: updated })
|
|
278
|
-
}
|
|
279
|
-
}
|
|
259
|
+
const pollWaStatus = useCallback(async () => {
|
|
260
|
+
if (!editing) return
|
|
261
|
+
try {
|
|
262
|
+
const data = await api<any>('GET', `/connectors/${editing.id}`)
|
|
263
|
+
setQrDataUrl(data.qrDataUrl || null)
|
|
264
|
+
setWaAuthenticated(data.authenticated ?? false)
|
|
265
|
+
setWaHasCreds(data.hasCredentials ?? false)
|
|
266
|
+
if (data.status === 'running' && editing.status !== 'running') {
|
|
267
|
+
const store = useAppStore.getState()
|
|
268
|
+
const updated = { ...store.connectors }
|
|
269
|
+
if (updated[editing.id]) {
|
|
270
|
+
updated[editing.id] = { ...updated[editing.id], status: 'running' as const }
|
|
271
|
+
useAppStore.setState({ connectors: updated })
|
|
280
272
|
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
273
|
+
}
|
|
274
|
+
} catch { /* ignore */ }
|
|
275
|
+
}, [editing?.id, editing?.status])
|
|
276
|
+
|
|
277
|
+
useEffect(() => {
|
|
278
|
+
if (editing && isWaRunning) pollWaStatus()
|
|
279
|
+
}, [editing?.id, isWaRunning, pollWaStatus])
|
|
280
|
+
|
|
281
|
+
useWs('connectors', pollWaStatus, isWaRunning ? 2000 : undefined)
|
|
287
282
|
|
|
288
283
|
const handleSave = async () => {
|
|
289
284
|
if (!agentId) return
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useCallback, useRef, useState } from 'react'
|
|
4
|
-
import { useChatStore } from '@/stores/use-chat-store'
|
|
4
|
+
import { useChatStore, type PendingFile } from '@/stores/use-chat-store'
|
|
5
5
|
import { useAppStore } from '@/stores/use-app-store'
|
|
6
6
|
import { uploadImage } from '@/lib/upload'
|
|
7
7
|
import { useAutoResize } from '@/hooks/use-auto-resize'
|
|
@@ -13,23 +13,56 @@ interface Props {
|
|
|
13
13
|
onStop: () => void
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
function FilePreview({ file, onRemove }: { file: PendingFile; onRemove: () => void }) {
|
|
17
|
+
const isImage = file.file.type.startsWith('image/')
|
|
18
|
+
return (
|
|
19
|
+
<div className="relative">
|
|
20
|
+
{isImage ? (
|
|
21
|
+
<img
|
|
22
|
+
src={URL.createObjectURL(file.file)}
|
|
23
|
+
alt="Preview"
|
|
24
|
+
className="h-16 rounded-[10px] object-cover border border-white/[0.06]"
|
|
25
|
+
/>
|
|
26
|
+
) : (
|
|
27
|
+
<div className="flex items-center gap-2.5 px-3 py-2.5 rounded-[10px] border border-white/[0.06] bg-white/[0.03]">
|
|
28
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-text-3 shrink-0">
|
|
29
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
30
|
+
<polyline points="14 2 14 8 20 8" />
|
|
31
|
+
</svg>
|
|
32
|
+
<span className="text-[13px] text-text-2 font-500 truncate max-w-[180px]">{file.file.name}</span>
|
|
33
|
+
</div>
|
|
34
|
+
)}
|
|
35
|
+
<button
|
|
36
|
+
onClick={onRemove}
|
|
37
|
+
className="absolute -top-1.5 -right-1.5 w-5 h-5 rounded-full border border-white/10 bg-raised
|
|
38
|
+
text-text-2 text-[10px] cursor-pointer flex items-center justify-center
|
|
39
|
+
hover:bg-danger-soft hover:text-danger hover:border-danger/20 transition-colors"
|
|
40
|
+
>
|
|
41
|
+
×
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
16
47
|
export function ChatInput({ streaming, onSend, onStop }: Props) {
|
|
17
48
|
const [value, setValue] = useState('')
|
|
18
49
|
const { ref: textareaRef, resize } = useAutoResize()
|
|
19
50
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
20
|
-
const
|
|
21
|
-
const
|
|
51
|
+
const imageInputRef = useRef<HTMLInputElement>(null)
|
|
52
|
+
const pendingFiles = useChatStore((s) => s.pendingFiles)
|
|
53
|
+
const addPendingFile = useChatStore((s) => s.addPendingFile)
|
|
54
|
+
const removePendingFile = useChatStore((s) => s.removePendingFile)
|
|
22
55
|
const speechRecognitionLang = useAppStore((s) => s.appSettings.speechRecognitionLang)
|
|
23
56
|
|
|
24
57
|
const handleSend = useCallback(() => {
|
|
25
58
|
const text = value.trim()
|
|
26
|
-
if (!text || streaming) return
|
|
27
|
-
onSend(text)
|
|
59
|
+
if ((!text && !pendingFiles.length) || streaming) return
|
|
60
|
+
onSend(text || 'See attached file(s).')
|
|
28
61
|
setValue('')
|
|
29
62
|
if (textareaRef.current) {
|
|
30
63
|
textareaRef.current.style.height = 'auto'
|
|
31
64
|
}
|
|
32
|
-
}, [value, streaming, onSend])
|
|
65
|
+
}, [value, streaming, onSend, pendingFiles.length])
|
|
33
66
|
|
|
34
67
|
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
|
35
68
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
@@ -47,6 +80,15 @@ export function ChatInput({ streaming, onSend, onStop }: Props) {
|
|
|
47
80
|
{ lang: speechRecognitionLang || undefined },
|
|
48
81
|
)
|
|
49
82
|
|
|
83
|
+
const uploadAndAdd = useCallback(async (file: File) => {
|
|
84
|
+
try {
|
|
85
|
+
const result = await uploadImage(file)
|
|
86
|
+
addPendingFile({ file, path: result.path, url: result.url })
|
|
87
|
+
} catch {
|
|
88
|
+
// ignore upload errors
|
|
89
|
+
}
|
|
90
|
+
}, [addPendingFile])
|
|
91
|
+
|
|
50
92
|
const handlePaste = useCallback(async (e: React.ClipboardEvent) => {
|
|
51
93
|
const items = e.clipboardData?.items
|
|
52
94
|
if (!items) return
|
|
@@ -54,35 +96,22 @@ export function ChatInput({ streaming, onSend, onStop }: Props) {
|
|
|
54
96
|
if (item.type.startsWith('image/')) {
|
|
55
97
|
e.preventDefault()
|
|
56
98
|
const file = item.getAsFile()
|
|
57
|
-
if (
|
|
58
|
-
try {
|
|
59
|
-
const result = await uploadImage(file)
|
|
60
|
-
setPendingImage({ file, path: result.path, url: result.url })
|
|
61
|
-
} catch {
|
|
62
|
-
// ignore
|
|
63
|
-
}
|
|
99
|
+
if (file) await uploadAndAdd(file)
|
|
64
100
|
return
|
|
65
101
|
}
|
|
66
102
|
}
|
|
67
|
-
}, [])
|
|
103
|
+
}, [uploadAndAdd])
|
|
68
104
|
|
|
69
105
|
const handleFileChange = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
70
|
-
const
|
|
71
|
-
if (!
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
setPendingImage({
|
|
75
|
-
file,
|
|
76
|
-
path: result.path,
|
|
77
|
-
url: result.url,
|
|
78
|
-
})
|
|
79
|
-
} catch {
|
|
80
|
-
// ignore
|
|
106
|
+
const files = e.target.files
|
|
107
|
+
if (!files?.length) return
|
|
108
|
+
for (const file of Array.from(files)) {
|
|
109
|
+
await uploadAndAdd(file)
|
|
81
110
|
}
|
|
82
111
|
e.target.value = ''
|
|
83
|
-
}, [])
|
|
112
|
+
}, [uploadAndAdd])
|
|
84
113
|
|
|
85
|
-
const hasContent = value.trim().length > 0 ||
|
|
114
|
+
const hasContent = value.trim().length > 0 || pendingFiles.length > 0
|
|
86
115
|
|
|
87
116
|
return (
|
|
88
117
|
<div className="shrink-0 px-6 md:px-12 lg:px-16 pb-4 pt-2"
|
|
@@ -105,33 +134,11 @@ export function ChatInput({ streaming, onSend, onStop }: Props) {
|
|
|
105
134
|
<div className="glass rounded-[20px] overflow-hidden
|
|
106
135
|
shadow-[0_4px_32px_rgba(0,0,0,0.3)] focus-within:border-border-focus focus-within:shadow-[0_4px_32px_rgba(99,102,241,0.08)] transition-all duration-300">
|
|
107
136
|
|
|
108
|
-
{
|
|
109
|
-
<div className="flex items-center gap-2 px-5 pt-4">
|
|
110
|
-
|
|
111
|
-
{
|
|
112
|
-
|
|
113
|
-
src={URL.createObjectURL(pendingImage.file)}
|
|
114
|
-
alt="Preview"
|
|
115
|
-
className="h-16 rounded-[10px] object-cover border border-white/[0.06]"
|
|
116
|
-
/>
|
|
117
|
-
) : (
|
|
118
|
-
<div className="flex items-center gap-2.5 px-3 py-2.5 rounded-[10px] border border-white/[0.06] bg-white/[0.03]">
|
|
119
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-text-3 shrink-0">
|
|
120
|
-
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
121
|
-
<polyline points="14 2 14 8 20 8" />
|
|
122
|
-
</svg>
|
|
123
|
-
<span className="text-[13px] text-text-2 font-500 truncate max-w-[180px]">{pendingImage.file.name}</span>
|
|
124
|
-
</div>
|
|
125
|
-
)}
|
|
126
|
-
<button
|
|
127
|
-
onClick={() => setPendingImage(null)}
|
|
128
|
-
className="absolute -top-1.5 -right-1.5 w-5 h-5 rounded-full border border-white/10 bg-raised
|
|
129
|
-
text-text-2 text-[10px] cursor-pointer flex items-center justify-center
|
|
130
|
-
hover:bg-danger-soft hover:text-danger hover:border-danger/20 transition-colors"
|
|
131
|
-
>
|
|
132
|
-
×
|
|
133
|
-
</button>
|
|
134
|
-
</div>
|
|
137
|
+
{pendingFiles.length > 0 && (
|
|
138
|
+
<div className="flex items-center gap-2 px-5 pt-4 flex-wrap">
|
|
139
|
+
{pendingFiles.map((f, i) => (
|
|
140
|
+
<FilePreview key={`${f.path}-${i}`} file={f} onRemove={() => removePendingFile(i)} />
|
|
141
|
+
))}
|
|
135
142
|
</div>
|
|
136
143
|
)}
|
|
137
144
|
|
|
@@ -162,7 +169,7 @@ export function ChatInput({ streaming, onSend, onStop }: Props) {
|
|
|
162
169
|
</button>
|
|
163
170
|
|
|
164
171
|
<button
|
|
165
|
-
onClick={() =>
|
|
172
|
+
onClick={() => imageInputRef.current?.click()}
|
|
166
173
|
className="flex items-center gap-1.5 px-3 py-2 rounded-[10px] border-none bg-transparent
|
|
167
174
|
text-text-3 text-[13px] cursor-pointer hover:text-text-2 hover:bg-white/[0.05] transition-all duration-200"
|
|
168
175
|
style={{ fontFamily: 'inherit' }}
|
|
@@ -217,7 +224,16 @@ export function ChatInput({ streaming, onSend, onStop }: Props) {
|
|
|
217
224
|
<input
|
|
218
225
|
ref={fileInputRef}
|
|
219
226
|
type="file"
|
|
220
|
-
|
|
227
|
+
multiple
|
|
228
|
+
accept="image/*,.pdf,.txt,.md,.csv,.json,.xml,.html,.js,.ts,.tsx,.jsx,.py,.go,.rs,.java,.c,.cpp,.h,.yml,.yaml,.toml,.env,.log,.sh,.sql,.css,.scss"
|
|
229
|
+
onChange={handleFileChange}
|
|
230
|
+
className="hidden"
|
|
231
|
+
/>
|
|
232
|
+
<input
|
|
233
|
+
ref={imageInputRef}
|
|
234
|
+
type="file"
|
|
235
|
+
multiple
|
|
236
|
+
accept="image/*"
|
|
221
237
|
onChange={handleFileChange}
|
|
222
238
|
className="hidden"
|
|
223
239
|
/>
|
|
@@ -12,7 +12,6 @@ export function KnowledgeList() {
|
|
|
12
12
|
const [loaded, setLoaded] = useState(false)
|
|
13
13
|
const [error, setError] = useState<string | null>(null)
|
|
14
14
|
const [activeTag, setActiveTag] = useState<string | null>(null)
|
|
15
|
-
const [selectedId, setSelectedId] = useState<string | null>(null)
|
|
16
15
|
const searchRef = useRef(search)
|
|
17
16
|
const setKnowledgeSheetOpen = useAppStore((s) => s.setKnowledgeSheetOpen)
|
|
18
17
|
const setEditingKnowledgeId = useAppStore((s) => s.setEditingKnowledgeId)
|
|
@@ -64,7 +63,6 @@ export function KnowledgeList() {
|
|
|
64
63
|
try {
|
|
65
64
|
await api('DELETE', `/knowledge/${id}`)
|
|
66
65
|
setEntries((prev) => prev.filter((e) => e.id !== id))
|
|
67
|
-
if (selectedId === id) setSelectedId(null)
|
|
68
66
|
} catch {
|
|
69
67
|
// silent
|
|
70
68
|
}
|
|
@@ -78,7 +76,7 @@ export function KnowledgeList() {
|
|
|
78
76
|
<div className="flex-1 flex flex-col overflow-y-auto">
|
|
79
77
|
{/* Search — only show when there are entries */}
|
|
80
78
|
{entries.length > 0 && (
|
|
81
|
-
<div className="px-
|
|
79
|
+
<div className="px-5 py-2 shrink-0">
|
|
82
80
|
<input
|
|
83
81
|
type="text"
|
|
84
82
|
value={search}
|
|
@@ -93,7 +91,7 @@ export function KnowledgeList() {
|
|
|
93
91
|
|
|
94
92
|
{/* Tag filters */}
|
|
95
93
|
{uniqueTags.length > 0 && (
|
|
96
|
-
<div className="px-
|
|
94
|
+
<div className="px-5 pb-1.5 shrink-0">
|
|
97
95
|
<div className="flex gap-1 flex-wrap">
|
|
98
96
|
<button
|
|
99
97
|
onClick={() => setActiveTag(null)}
|
|
@@ -120,52 +118,50 @@ export function KnowledgeList() {
|
|
|
120
118
|
|
|
121
119
|
{/* Entries */}
|
|
122
120
|
{entries.length > 0 ? (
|
|
123
|
-
<div className="
|
|
121
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 px-5 pb-6">
|
|
124
122
|
{entries.map((entry) => {
|
|
125
123
|
const meta = entry.metadata as { tags?: string[] } | undefined
|
|
126
124
|
const tags = meta?.tags || []
|
|
127
125
|
return (
|
|
128
126
|
<div
|
|
129
127
|
key={entry.id}
|
|
130
|
-
|
|
131
|
-
className={`p-3 rounded-[12px] border cursor-pointer transition-all
|
|
132
|
-
${selectedId === entry.id
|
|
133
|
-
? 'border-accent-bright/30 bg-accent-soft/30'
|
|
134
|
-
: 'border-white/[0.04] bg-transparent hover:bg-surface-2'
|
|
135
|
-
}`}
|
|
128
|
+
className="p-3 rounded-[12px] border border-white/[0.04] bg-transparent hover:bg-surface-2 transition-all relative group"
|
|
136
129
|
>
|
|
137
130
|
<div className="flex items-start justify-between gap-2 mb-1">
|
|
138
131
|
<span className="font-display text-[13px] font-600 text-text truncate">{entry.title}</span>
|
|
139
|
-
<
|
|
132
|
+
<div className="flex items-center gap-1.5 shrink-0">
|
|
133
|
+
<button
|
|
134
|
+
onClick={() => openSheet(entry.id)}
|
|
135
|
+
className="text-text-3/40 hover:text-accent-bright transition-colors p-0.5 cursor-pointer"
|
|
136
|
+
title="Edit"
|
|
137
|
+
>
|
|
138
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
139
|
+
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
|
140
|
+
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
|
141
|
+
</svg>
|
|
142
|
+
</button>
|
|
143
|
+
<button
|
|
144
|
+
onClick={() => void handleDelete(entry.id)}
|
|
145
|
+
className="text-text-3/40 hover:text-red-400 transition-colors p-0.5 cursor-pointer"
|
|
146
|
+
title="Delete"
|
|
147
|
+
>
|
|
148
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
149
|
+
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
|
150
|
+
</svg>
|
|
151
|
+
</button>
|
|
152
|
+
<span className="text-[10px] text-text-3/50">{formatDate(entry.createdAt)}</span>
|
|
153
|
+
</div>
|
|
140
154
|
</div>
|
|
141
155
|
<p className="text-[11px] text-text-3/60 line-clamp-2 mb-2">
|
|
142
156
|
{entry.content.slice(0, 200)}
|
|
143
157
|
</p>
|
|
144
158
|
{tags.length > 0 && (
|
|
145
|
-
<div className="flex gap-1 flex-wrap
|
|
159
|
+
<div className="flex gap-1 flex-wrap">
|
|
146
160
|
{tags.map((t) => (
|
|
147
161
|
<Badge key={t} variant="secondary" className="text-[9px] px-1.5 py-0">{t}</Badge>
|
|
148
162
|
))}
|
|
149
163
|
</div>
|
|
150
164
|
)}
|
|
151
|
-
{selectedId === entry.id && (
|
|
152
|
-
<div className="flex gap-2 pt-2 border-t border-white/[0.04]">
|
|
153
|
-
<button
|
|
154
|
-
onClick={(e) => { e.stopPropagation(); openSheet(entry.id) }}
|
|
155
|
-
className="px-2.5 py-1 rounded-[7px] text-[10px] font-600 text-accent-bright bg-accent-soft cursor-pointer border-none hover:brightness-110 transition-all"
|
|
156
|
-
style={{ fontFamily: 'inherit' }}
|
|
157
|
-
>
|
|
158
|
-
Edit
|
|
159
|
-
</button>
|
|
160
|
-
<button
|
|
161
|
-
onClick={(e) => { e.stopPropagation(); void handleDelete(entry.id) }}
|
|
162
|
-
className="px-2.5 py-1 rounded-[7px] text-[10px] font-600 text-red-400 bg-red-400/10 cursor-pointer border-none hover:bg-red-400/20 transition-all"
|
|
163
|
-
style={{ fontFamily: 'inherit' }}
|
|
164
|
-
>
|
|
165
|
-
Delete
|
|
166
|
-
</button>
|
|
167
|
-
</div>
|
|
168
|
-
)}
|
|
169
165
|
</div>
|
|
170
166
|
)
|
|
171
167
|
})}
|