@swarmclawai/swarmclaw 0.5.2 → 0.6.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 +42 -7
- package/bin/swarmclaw.js +76 -16
- package/next.config.ts +11 -1
- package/package.json +4 -2
- package/public/screenshots/agents.png +0 -0
- package/public/screenshots/dashboard.png +0 -0
- package/public/screenshots/providers.png +0 -0
- package/public/screenshots/tasks.png +0 -0
- package/scripts/postinstall.mjs +18 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +410 -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/credentials/route.ts +2 -3
- 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/[id]/route.ts +27 -0
- package/src/app/api/notifications/route.ts +68 -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 +155 -0
- 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/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 +20 -0
- package/src/app/api/tasks/bulk/route.ts +100 -0
- package/src/app/api/tasks/route.ts +1 -0
- package/src/app/api/usage/route.ts +45 -0
- package/src/app/api/webhooks/[id]/route.ts +15 -1
- package/src/app/globals.css +58 -15
- package/src/app/page.tsx +142 -13
- package/src/cli/index.js +42 -0
- package/src/cli/index.test.js +30 -0
- package/src/cli/spec.js +32 -0
- package/src/components/agents/agent-avatar.tsx +57 -10
- package/src/components/agents/agent-card.tsx +48 -15
- package/src/components/agents/agent-chat-list.tsx +123 -10
- package/src/components/agents/agent-list.tsx +50 -19
- package/src/components/agents/agent-sheet.tsx +56 -63
- 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/chat/activity-moment.tsx +169 -0
- package/src/components/chat/chat-header.tsx +2 -0
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/file-path-chip.tsx +125 -0
- package/src/components/chat/markdown-utils.ts +9 -0
- package/src/components/chat/message-bubble.tsx +46 -295
- package/src/components/chat/message-list.tsx +50 -1
- package/src/components/chat/streaming-bubble.tsx +36 -46
- 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 +66 -70
- 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 +123 -0
- package/src/components/chatrooms/chatroom-message.tsx +427 -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-sheet.tsx +34 -47
- package/src/components/home/home-view.tsx +501 -0
- package/src/components/input/chat-input.tsx +79 -41
- 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 +209 -83
- package/src/components/layout/mobile-header.tsx +2 -0
- 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 +3 -2
- package/src/components/schedules/schedule-list.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +25 -25
- 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/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/check-icon.tsx +12 -0
- package/src/components/shared/confirm-dialog.tsx +1 -1
- 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/keyboard-shortcuts-dialog.tsx +116 -0
- package/src/components/shared/notification-center.tsx +223 -0
- 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 +296 -0
- 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-providers.tsx +1 -1
- package/src/components/shared/settings/section-secrets.tsx +1 -1
- package/src/components/shared/settings/section-theme.tsx +95 -0
- package/src/components/shared/settings/section-user-preferences.tsx +39 -0
- package/src/components/shared/settings/settings-page.tsx +180 -27
- package/src/components/shared/settings/settings-sheet.tsx +9 -73
- 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 +46 -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 +89 -72
- package/src/components/ui/hover-card.tsx +52 -0
- package/src/components/usage/metrics-dashboard.tsx +78 -0
- package/src/components/usage/usage-list.tsx +1 -1
- package/src/components/webhooks/webhook-sheet.tsx +1 -1
- package/src/hooks/use-view-router.ts +69 -19
- package/src/instrumentation.ts +15 -1
- package/src/lib/chat.ts +2 -0
- package/src/lib/cron-human.ts +114 -0
- package/src/lib/memory.ts +3 -0
- package/src/lib/server/chat-execution.ts +24 -4
- package/src/lib/server/connectors/manager.ts +11 -0
- package/src/lib/server/context-manager.ts +225 -13
- package/src/lib/server/create-notification.ts +42 -0
- package/src/lib/server/daemon-state.ts +165 -10
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service.ts +40 -5
- package/src/lib/server/heartbeat-wake.ts +110 -0
- package/src/lib/server/langgraph-checkpoint.ts +1 -0
- package/src/lib/server/memory-consolidation.ts +92 -0
- package/src/lib/server/memory-db.ts +51 -6
- package/src/lib/server/openclaw-gateway.ts +9 -1
- package/src/lib/server/provider-health.ts +125 -0
- package/src/lib/server/queue.ts +5 -4
- package/src/lib/server/scheduler.ts +8 -0
- package/src/lib/server/session-run-manager.ts +4 -0
- package/src/lib/server/session-tools/chatroom.ts +136 -0
- package/src/lib/server/session-tools/context-mgmt.ts +36 -18
- package/src/lib/server/session-tools/index.ts +2 -0
- package/src/lib/server/session-tools/memory.ts +6 -1
- package/src/lib/server/storage.ts +80 -29
- package/src/lib/server/stream-agent-chat.ts +153 -47
- package/src/lib/server/system-events.ts +49 -0
- package/src/lib/server/ws-hub.ts +11 -0
- package/src/lib/soul-suggestions.ts +109 -0
- package/src/lib/tasks.ts +4 -1
- package/src/lib/view-routes.ts +36 -1
- package/src/lib/ws-client.ts +14 -4
- package/src/proxy.ts +79 -2
- package/src/stores/use-app-store.ts +94 -3
- package/src/stores/use-chat-store.ts +48 -3
- package/src/stores/use-chatroom-store.ts +276 -0
- package/src/types/index.ts +69 -2
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useCallback, useEffect, useMemo, type KeyboardEvent } from 'react'
|
|
4
|
+
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
5
|
+
import { FilePreview } from '@/components/shared/file-preview'
|
|
6
|
+
import { useChatroomStore } from '@/stores/use-chatroom-store'
|
|
7
|
+
import { uploadImage } from '@/lib/upload'
|
|
8
|
+
import type { Agent } from '@/types'
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
agents: Agent[]
|
|
12
|
+
onSend: (text: string) => void
|
|
13
|
+
disabled?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function ChatroomInput({ agents, onSend, disabled }: Props) {
|
|
17
|
+
const [text, setText] = useState('')
|
|
18
|
+
const [showMentions, setShowMentions] = useState(false)
|
|
19
|
+
const [mentionFilter, setMentionFilter] = useState('')
|
|
20
|
+
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
21
|
+
const chatroomId = useChatroomStore((s) => s.currentChatroomId)
|
|
22
|
+
const inputRef = useRef<HTMLTextAreaElement>(null)
|
|
23
|
+
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
24
|
+
const imageInputRef = useRef<HTMLInputElement>(null)
|
|
25
|
+
|
|
26
|
+
const pendingFiles = useChatroomStore((s) => s.pendingFiles)
|
|
27
|
+
const addPendingFile = useChatroomStore((s) => s.addPendingFile)
|
|
28
|
+
const removePendingFile = useChatroomStore((s) => s.removePendingFile)
|
|
29
|
+
const replyingTo = useChatroomStore((s) => s.replyingTo)
|
|
30
|
+
const setReplyingTo = useChatroomStore((s) => s.setReplyingTo)
|
|
31
|
+
|
|
32
|
+
// Draft persistence: restore on chatroom change
|
|
33
|
+
const draftTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!chatroomId) return
|
|
36
|
+
const draft = localStorage.getItem(`sc_draft_cr_${chatroomId}`)
|
|
37
|
+
setText(draft || '')
|
|
38
|
+
}, [chatroomId])
|
|
39
|
+
|
|
40
|
+
// Debounced save to localStorage
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!chatroomId) return
|
|
43
|
+
if (draftTimerRef.current) clearTimeout(draftTimerRef.current)
|
|
44
|
+
draftTimerRef.current = setTimeout(() => {
|
|
45
|
+
if (text) localStorage.setItem(`sc_draft_cr_${chatroomId}`, text)
|
|
46
|
+
else localStorage.removeItem(`sc_draft_cr_${chatroomId}`)
|
|
47
|
+
}, 300)
|
|
48
|
+
return () => { if (draftTimerRef.current) clearTimeout(draftTimerRef.current) }
|
|
49
|
+
}, [text, chatroomId])
|
|
50
|
+
|
|
51
|
+
const uploadAndAdd = useCallback(async (file: File) => {
|
|
52
|
+
try {
|
|
53
|
+
const result = await uploadImage(file)
|
|
54
|
+
addPendingFile({ file, path: result.path, url: result.url })
|
|
55
|
+
} catch {
|
|
56
|
+
// ignore upload errors
|
|
57
|
+
}
|
|
58
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
59
|
+
}, [])
|
|
60
|
+
|
|
61
|
+
const handlePaste = useCallback(async (e: React.ClipboardEvent) => {
|
|
62
|
+
const items = e.clipboardData?.items
|
|
63
|
+
if (!items) return
|
|
64
|
+
for (const item of items) {
|
|
65
|
+
if (item.type.startsWith('image/')) {
|
|
66
|
+
e.preventDefault()
|
|
67
|
+
const file = item.getAsFile()
|
|
68
|
+
if (file) await uploadAndAdd(file)
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}, [uploadAndAdd])
|
|
73
|
+
|
|
74
|
+
const handleFileChange = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
75
|
+
const files = e.target.files
|
|
76
|
+
if (!files?.length) return
|
|
77
|
+
for (const file of Array.from(files)) {
|
|
78
|
+
await uploadAndAdd(file)
|
|
79
|
+
}
|
|
80
|
+
e.target.value = ''
|
|
81
|
+
}, [uploadAndAdd])
|
|
82
|
+
|
|
83
|
+
const handleChange = useCallback((value: string) => {
|
|
84
|
+
setText(value)
|
|
85
|
+
const cursorPos = inputRef.current?.selectionStart || value.length
|
|
86
|
+
const beforeCursor = value.slice(0, cursorPos)
|
|
87
|
+
const mentionMatch = beforeCursor.match(/@(\S*)$/)
|
|
88
|
+
if (mentionMatch) {
|
|
89
|
+
setShowMentions(true)
|
|
90
|
+
setMentionFilter(mentionMatch[1].toLowerCase())
|
|
91
|
+
setSelectedIndex(0)
|
|
92
|
+
} else {
|
|
93
|
+
setShowMentions(false)
|
|
94
|
+
setMentionFilter('')
|
|
95
|
+
setSelectedIndex(0)
|
|
96
|
+
}
|
|
97
|
+
}, [])
|
|
98
|
+
|
|
99
|
+
const insertMention = useCallback((name: string) => {
|
|
100
|
+
const cursorPos = inputRef.current?.selectionStart || text.length
|
|
101
|
+
const beforeCursor = text.slice(0, cursorPos)
|
|
102
|
+
const afterCursor = text.slice(cursorPos)
|
|
103
|
+
const mentionMatch = beforeCursor.match(/@(\S*)$/)
|
|
104
|
+
if (mentionMatch) {
|
|
105
|
+
const newBefore = beforeCursor.slice(0, mentionMatch.index) + `@${name.replace(/\s+/g, '')} `
|
|
106
|
+
setText(newBefore + afterCursor)
|
|
107
|
+
}
|
|
108
|
+
setShowMentions(false)
|
|
109
|
+
inputRef.current?.focus()
|
|
110
|
+
}, [text])
|
|
111
|
+
|
|
112
|
+
const filteredAgents = agents.filter((a) =>
|
|
113
|
+
a.name.toLowerCase().replace(/\s+/g, '').includes(mentionFilter)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
// Build highlighted segments for the mirror overlay
|
|
117
|
+
const highlightedSegments = useMemo(() => {
|
|
118
|
+
if (!text) return null
|
|
119
|
+
const parts: React.ReactNode[] = []
|
|
120
|
+
let lastIndex = 0
|
|
121
|
+
const regex = /@\S+/g
|
|
122
|
+
let match: RegExpExecArray | null
|
|
123
|
+
while ((match = regex.exec(text)) !== null) {
|
|
124
|
+
if (match.index > lastIndex) {
|
|
125
|
+
parts.push(text.slice(lastIndex, match.index))
|
|
126
|
+
}
|
|
127
|
+
parts.push(
|
|
128
|
+
<span key={match.index} className="bg-accent-soft/50 text-accent-bright rounded px-0.5">
|
|
129
|
+
{match[0]}
|
|
130
|
+
</span>
|
|
131
|
+
)
|
|
132
|
+
lastIndex = regex.lastIndex
|
|
133
|
+
}
|
|
134
|
+
if (lastIndex < text.length) {
|
|
135
|
+
parts.push(text.slice(lastIndex))
|
|
136
|
+
}
|
|
137
|
+
return parts.length > 0 ? parts : null
|
|
138
|
+
}, [text])
|
|
139
|
+
|
|
140
|
+
const mentionDropdownVisible = showMentions && (filteredAgents.length > 0 || mentionFilter === '')
|
|
141
|
+
const mentionItems = mentionDropdownVisible
|
|
142
|
+
? ['all', ...filteredAgents.map((a) => a.name)]
|
|
143
|
+
: []
|
|
144
|
+
|
|
145
|
+
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
146
|
+
if (mentionDropdownVisible) {
|
|
147
|
+
if (e.key === 'ArrowDown') {
|
|
148
|
+
e.preventDefault()
|
|
149
|
+
setSelectedIndex((i) => (i + 1) % mentionItems.length)
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
if (e.key === 'ArrowUp') {
|
|
153
|
+
e.preventDefault()
|
|
154
|
+
setSelectedIndex((i) => (i - 1 + mentionItems.length) % mentionItems.length)
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
if (e.key === 'Enter' || e.key === 'Tab') {
|
|
158
|
+
e.preventDefault()
|
|
159
|
+
const selected = mentionItems[selectedIndex]
|
|
160
|
+
if (selected) insertMention(selected)
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
166
|
+
e.preventDefault()
|
|
167
|
+
if ((text.trim() || pendingFiles.length) && !disabled) {
|
|
168
|
+
onSend(text)
|
|
169
|
+
setText('')
|
|
170
|
+
if (chatroomId) localStorage.removeItem(`sc_draft_cr_${chatroomId}`)
|
|
171
|
+
setShowMentions(false)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (e.key === 'Escape') {
|
|
175
|
+
if (replyingTo) {
|
|
176
|
+
setReplyingTo(null)
|
|
177
|
+
}
|
|
178
|
+
setShowMentions(false)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<div className="relative px-4 py-3 border-t border-white/[0.06]">
|
|
184
|
+
{/* Mention dropdown */}
|
|
185
|
+
{mentionDropdownVisible && (
|
|
186
|
+
<div className="absolute bottom-full left-4 right-4 mb-1 bg-raised border border-white/[0.1] rounded-[8px] shadow-xl max-h-[200px] overflow-y-auto z-50">
|
|
187
|
+
<button
|
|
188
|
+
onClick={() => insertMention('all')}
|
|
189
|
+
className={`w-full flex items-center gap-2 px-3 py-2 text-left transition-all cursor-pointer ${
|
|
190
|
+
selectedIndex === 0 ? 'bg-white/[0.08]' : 'hover:bg-white/[0.06]'
|
|
191
|
+
}`}
|
|
192
|
+
>
|
|
193
|
+
<div className="w-5 h-5 rounded-full bg-accent-soft flex items-center justify-center text-[9px] font-700 text-accent-bright">@</div>
|
|
194
|
+
<span className="text-[13px] text-text">all</span>
|
|
195
|
+
<span className="text-[11px] text-text-3 ml-auto">Mention all agents</span>
|
|
196
|
+
</button>
|
|
197
|
+
{filteredAgents.map((agent, i) => (
|
|
198
|
+
<button
|
|
199
|
+
key={agent.id}
|
|
200
|
+
onClick={() => insertMention(agent.name)}
|
|
201
|
+
className={`w-full flex items-center gap-2 px-3 py-2 text-left transition-all cursor-pointer ${
|
|
202
|
+
selectedIndex === i + 1 ? 'bg-white/[0.08]' : 'hover:bg-white/[0.06]'
|
|
203
|
+
}`}
|
|
204
|
+
>
|
|
205
|
+
<AgentAvatar seed={agent.avatarSeed} name={agent.name} size={20} />
|
|
206
|
+
<span className="text-[13px] text-text">{agent.name}</span>
|
|
207
|
+
</button>
|
|
208
|
+
))}
|
|
209
|
+
</div>
|
|
210
|
+
)}
|
|
211
|
+
|
|
212
|
+
{/* Reply preview banner */}
|
|
213
|
+
{replyingTo && (
|
|
214
|
+
<div className="flex items-center gap-2 mb-2 px-2 py-1.5 rounded-[8px] bg-white/[0.04] border border-white/[0.06]">
|
|
215
|
+
<div className="w-0.5 self-stretch rounded-full bg-accent-bright/50 shrink-0" />
|
|
216
|
+
<div className="flex-1 min-w-0">
|
|
217
|
+
<span className="text-[11px] font-600 text-accent-bright">{replyingTo.senderName}</span>
|
|
218
|
+
<p className="text-[12px] text-text-3 truncate m-0">
|
|
219
|
+
{replyingTo.text.length > 100 ? replyingTo.text.slice(0, 100) + '...' : replyingTo.text}
|
|
220
|
+
</p>
|
|
221
|
+
</div>
|
|
222
|
+
<button
|
|
223
|
+
onClick={() => setReplyingTo(null)}
|
|
224
|
+
className="shrink-0 w-5 h-5 rounded-full flex items-center justify-center hover:bg-white/[0.08] cursor-pointer text-text-3 hover:text-text transition-colors"
|
|
225
|
+
>
|
|
226
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
|
|
227
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
228
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
229
|
+
</svg>
|
|
230
|
+
</button>
|
|
231
|
+
</div>
|
|
232
|
+
)}
|
|
233
|
+
|
|
234
|
+
{/* File previews */}
|
|
235
|
+
{pendingFiles.length > 0 && (
|
|
236
|
+
<div className="flex gap-2 mb-2 flex-wrap">
|
|
237
|
+
{pendingFiles.map((f, i) => (
|
|
238
|
+
<FilePreview key={i} file={f} onRemove={() => removePendingFile(i)} />
|
|
239
|
+
))}
|
|
240
|
+
</div>
|
|
241
|
+
)}
|
|
242
|
+
|
|
243
|
+
<div className="flex items-end gap-2">
|
|
244
|
+
{/* Attach file button */}
|
|
245
|
+
<button
|
|
246
|
+
onClick={() => fileInputRef.current?.click()}
|
|
247
|
+
disabled={disabled}
|
|
248
|
+
className="shrink-0 w-9 h-9 rounded-[8px] flex items-center justify-center hover:bg-white/[0.08] transition-all cursor-pointer disabled:opacity-30"
|
|
249
|
+
title="Attach file"
|
|
250
|
+
>
|
|
251
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-text-3">
|
|
252
|
+
<path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" />
|
|
253
|
+
</svg>
|
|
254
|
+
</button>
|
|
255
|
+
|
|
256
|
+
{/* Image button */}
|
|
257
|
+
<button
|
|
258
|
+
onClick={() => imageInputRef.current?.click()}
|
|
259
|
+
disabled={disabled}
|
|
260
|
+
className="shrink-0 w-9 h-9 rounded-[8px] flex items-center justify-center hover:bg-white/[0.08] transition-all cursor-pointer disabled:opacity-30"
|
|
261
|
+
title="Attach image"
|
|
262
|
+
>
|
|
263
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-text-3">
|
|
264
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
|
265
|
+
<circle cx="8.5" cy="8.5" r="1.5" />
|
|
266
|
+
<polyline points="21 15 16 10 5 21" />
|
|
267
|
+
</svg>
|
|
268
|
+
</button>
|
|
269
|
+
|
|
270
|
+
<div className="flex-1 relative rounded-[8px] bg-white/[0.06] border border-white/[0.08] focus-within:border-accent-bright/40">
|
|
271
|
+
{/* Highlight mirror — renders @mentions with accent background behind the transparent textarea */}
|
|
272
|
+
<div
|
|
273
|
+
aria-hidden
|
|
274
|
+
className="absolute inset-0 px-3 py-2 text-[13px] leading-[1.5] break-words whitespace-pre-wrap pointer-events-none overflow-hidden"
|
|
275
|
+
style={{ minHeight: '38px', color: 'transparent' }}
|
|
276
|
+
>
|
|
277
|
+
{highlightedSegments}
|
|
278
|
+
</div>
|
|
279
|
+
<textarea
|
|
280
|
+
ref={inputRef}
|
|
281
|
+
value={text}
|
|
282
|
+
onChange={(e) => handleChange(e.target.value)}
|
|
283
|
+
onKeyDown={handleKeyDown}
|
|
284
|
+
onPaste={handlePaste}
|
|
285
|
+
placeholder="Type a message... Use @ to mention agents"
|
|
286
|
+
disabled={disabled}
|
|
287
|
+
rows={1}
|
|
288
|
+
className="w-full resize-none px-3 py-2 rounded-[8px] bg-transparent text-[13px] text-text placeholder:text-text-3 focus:outline-none max-h-[120px] disabled:opacity-50 relative border-none"
|
|
289
|
+
style={{ minHeight: '38px' }}
|
|
290
|
+
/>
|
|
291
|
+
</div>
|
|
292
|
+
<button
|
|
293
|
+
onClick={() => {
|
|
294
|
+
if ((text.trim() || pendingFiles.length) && !disabled) {
|
|
295
|
+
onSend(text)
|
|
296
|
+
setText('')
|
|
297
|
+
if (chatroomId) localStorage.removeItem(`sc_draft_cr_${chatroomId}`)
|
|
298
|
+
setShowMentions(false)
|
|
299
|
+
}
|
|
300
|
+
}}
|
|
301
|
+
disabled={(!text.trim() && !pendingFiles.length) || disabled}
|
|
302
|
+
className="shrink-0 w-9 h-9 rounded-[8px] bg-accent-bright flex items-center justify-center hover:bg-accent-bright/90 transition-all disabled:opacity-30 cursor-pointer"
|
|
303
|
+
>
|
|
304
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
305
|
+
<line x1="22" y1="2" x2="11" y2="13" />
|
|
306
|
+
<polygon points="22 2 15 22 11 13 2 9 22 2" />
|
|
307
|
+
</svg>
|
|
308
|
+
</button>
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
{/* Hidden file inputs */}
|
|
312
|
+
<input ref={fileInputRef} type="file" multiple
|
|
313
|
+
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"
|
|
314
|
+
onChange={handleFileChange} className="hidden" />
|
|
315
|
+
<input ref={imageInputRef} type="file" multiple
|
|
316
|
+
accept="image/*"
|
|
317
|
+
onChange={handleFileChange} className="hidden" />
|
|
318
|
+
</div>
|
|
319
|
+
)
|
|
320
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useCallback, useMemo, useState } from 'react'
|
|
4
|
+
import { useChatroomStore } from '@/stores/use-chatroom-store'
|
|
5
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
6
|
+
import { useWs } from '@/hooks/use-ws'
|
|
7
|
+
import type { Chatroom } from '@/types'
|
|
8
|
+
import { EmptyState } from '@/components/shared/empty-state'
|
|
9
|
+
|
|
10
|
+
export function ChatroomList() {
|
|
11
|
+
const chatrooms = useChatroomStore((s) => s.chatrooms)
|
|
12
|
+
const currentChatroomId = useChatroomStore((s) => s.currentChatroomId)
|
|
13
|
+
const loadChatrooms = useChatroomStore((s) => s.loadChatrooms)
|
|
14
|
+
const setCurrentChatroom = useChatroomStore((s) => s.setCurrentChatroom)
|
|
15
|
+
const setChatroomSheetOpen = useChatroomStore((s) => s.setChatroomSheetOpen)
|
|
16
|
+
const setEditingChatroomId = useChatroomStore((s) => s.setEditingChatroomId)
|
|
17
|
+
const agents = useAppStore((s) => s.agents)
|
|
18
|
+
|
|
19
|
+
const refresh = useCallback(() => {
|
|
20
|
+
loadChatrooms()
|
|
21
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
22
|
+
}, [])
|
|
23
|
+
|
|
24
|
+
useEffect(() => { refresh() }, [refresh])
|
|
25
|
+
useWs('chatrooms', refresh, 15_000)
|
|
26
|
+
|
|
27
|
+
const [filter, setFilter] = useState<'all' | 'active' | 'recent'>('all')
|
|
28
|
+
|
|
29
|
+
const sorted = useMemo(() =>
|
|
30
|
+
Object.values(chatrooms).sort(
|
|
31
|
+
(a: Chatroom, b: Chatroom) => b.updatedAt - a.updatedAt
|
|
32
|
+
), [chatrooms])
|
|
33
|
+
|
|
34
|
+
const filtered = useMemo(() => {
|
|
35
|
+
if (filter === 'all') return sorted
|
|
36
|
+
const now = Date.now()
|
|
37
|
+
return sorted.filter((c) => {
|
|
38
|
+
if (filter === 'active') return now - c.updatedAt < 3_600_000 // 1h
|
|
39
|
+
return now - c.updatedAt < 86_400_000 // 24h
|
|
40
|
+
})
|
|
41
|
+
}, [sorted, filter])
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className="flex-1 overflow-y-auto">
|
|
45
|
+
{sorted.length === 0 ? (
|
|
46
|
+
<EmptyState
|
|
47
|
+
icon={
|
|
48
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" className="text-accent-bright">
|
|
49
|
+
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" fill="currentColor" />
|
|
50
|
+
</svg>
|
|
51
|
+
}
|
|
52
|
+
title="No chatrooms yet"
|
|
53
|
+
subtitle="Create one to start a group chat"
|
|
54
|
+
action={{ label: '+ New Chatroom', onClick: () => { setEditingChatroomId(null); setChatroomSheetOpen(true) } }}
|
|
55
|
+
/>
|
|
56
|
+
) : (
|
|
57
|
+
<div className="p-3 space-y-1">
|
|
58
|
+
{sorted.length > 2 && (
|
|
59
|
+
<div className="flex items-center gap-1 px-1 pb-2">
|
|
60
|
+
{(['all', 'active', 'recent'] as const).map((f) => (
|
|
61
|
+
<button
|
|
62
|
+
key={f}
|
|
63
|
+
type="button"
|
|
64
|
+
onClick={() => setFilter(f)}
|
|
65
|
+
data-active={filter === f || undefined}
|
|
66
|
+
className="px-3 py-1.5 rounded-[8px] text-[11px] font-600 border-none cursor-pointer transition-all
|
|
67
|
+
data-[active]:bg-accent-soft data-[active]:text-accent-bright
|
|
68
|
+
bg-transparent text-text-3 hover:text-text-2 hover:bg-white/[0.04]"
|
|
69
|
+
>
|
|
70
|
+
{f}
|
|
71
|
+
</button>
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
{filtered.map((chatroom) => {
|
|
76
|
+
const isActive = chatroom.id === currentChatroomId
|
|
77
|
+
const memberNames = chatroom.agentIds
|
|
78
|
+
.map((id) => agents[id]?.name)
|
|
79
|
+
.filter(Boolean)
|
|
80
|
+
.slice(0, 3)
|
|
81
|
+
const lastMsg = chatroom.messages[chatroom.messages.length - 1]
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<button
|
|
85
|
+
key={chatroom.id}
|
|
86
|
+
onClick={() => setCurrentChatroom(chatroom.id)}
|
|
87
|
+
className={`w-full text-left py-3.5 px-4 rounded-[14px] transition-all cursor-pointer group border border-transparent ${
|
|
88
|
+
isActive
|
|
89
|
+
? 'bg-accent-soft/60'
|
|
90
|
+
: 'hover:bg-white/[0.04]'
|
|
91
|
+
}`}
|
|
92
|
+
>
|
|
93
|
+
<div className="flex items-center gap-2 mb-0.5">
|
|
94
|
+
<div className="w-7 h-7 rounded-full bg-accent-soft flex items-center justify-center shrink-0">
|
|
95
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-accent-bright">
|
|
96
|
+
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
|
97
|
+
</svg>
|
|
98
|
+
</div>
|
|
99
|
+
<span className={`text-[13px] font-600 truncate ${isActive ? 'text-accent-bright' : 'text-text'}`}>
|
|
100
|
+
{chatroom.name}
|
|
101
|
+
</span>
|
|
102
|
+
<span className="label-mono ml-auto shrink-0">
|
|
103
|
+
{chatroom.agentIds.length} agents
|
|
104
|
+
</span>
|
|
105
|
+
</div>
|
|
106
|
+
{memberNames.length > 0 && (
|
|
107
|
+
<p className="text-[11px] text-text-3 truncate pl-9">
|
|
108
|
+
{memberNames.join(', ')}{chatroom.agentIds.length > 3 ? ` +${chatroom.agentIds.length - 3}` : ''}
|
|
109
|
+
</p>
|
|
110
|
+
)}
|
|
111
|
+
{lastMsg && (
|
|
112
|
+
<p className="text-[11px] text-text-3/70 truncate pl-9 mt-0.5">
|
|
113
|
+
{lastMsg.senderName}: {lastMsg.text.slice(0, 60)}
|
|
114
|
+
</p>
|
|
115
|
+
)}
|
|
116
|
+
</button>
|
|
117
|
+
)
|
|
118
|
+
})}
|
|
119
|
+
</div>
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
)
|
|
123
|
+
}
|