@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,501 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
4
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
import { useChatStore } from '@/stores/use-chat-store'
|
|
6
|
+
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
7
|
+
import { api } from '@/lib/api-client'
|
|
8
|
+
import type { Agent, Session, ActivityEntry, BoardTask, AppNotification } from '@/types'
|
|
9
|
+
|
|
10
|
+
function timeAgo(ts: number): string {
|
|
11
|
+
const diff = Date.now() - ts
|
|
12
|
+
const mins = Math.floor(diff / 60000)
|
|
13
|
+
if (mins < 1) return 'just now'
|
|
14
|
+
if (mins < 60) return `${mins}m ago`
|
|
15
|
+
const hours = Math.floor(mins / 60)
|
|
16
|
+
if (hours < 24) return `${hours}h ago`
|
|
17
|
+
const days = Math.floor(hours / 24)
|
|
18
|
+
return `${days}d ago`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function timeUntil(ts: number): string {
|
|
22
|
+
const diff = ts - Date.now()
|
|
23
|
+
if (diff <= 0) return 'now'
|
|
24
|
+
const mins = Math.floor(diff / 60000)
|
|
25
|
+
if (mins < 60) return `in ${mins}m`
|
|
26
|
+
const hours = Math.floor(mins / 60)
|
|
27
|
+
if (hours < 24) return `in ${hours}h`
|
|
28
|
+
const days = Math.floor(hours / 24)
|
|
29
|
+
return `in ${days}d`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const ACTIVITY_ICONS: Record<ActivityEntry['action'], string> = {
|
|
33
|
+
created: 'M12 5v14m-7-7h14',
|
|
34
|
+
updated: 'M17 3a2.85 2.85 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z',
|
|
35
|
+
deleted: 'M3 6h18m-2 0v14a2 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',
|
|
36
|
+
started: 'M5 3l14 9-14 9V3z',
|
|
37
|
+
stopped: 'M6 4h4v16H6zm8 0h4v16h-4z',
|
|
38
|
+
queued: 'M12 6v6l4 2',
|
|
39
|
+
completed: 'M20 6L9 17l-5-5',
|
|
40
|
+
failed: 'M18 6L6 18M6 6l12 12',
|
|
41
|
+
approved: 'M22 11.08V12a10 10 0 1 1-5.93-9.14',
|
|
42
|
+
rejected: 'M10 15l5-5m0 5l-5-5',
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const ACTIVITY_COLORS: Record<ActivityEntry['action'], string> = {
|
|
46
|
+
created: 'text-emerald-400',
|
|
47
|
+
updated: 'text-sky-400',
|
|
48
|
+
deleted: 'text-red-400',
|
|
49
|
+
started: 'text-emerald-400',
|
|
50
|
+
stopped: 'text-text-3',
|
|
51
|
+
queued: 'text-amber-400',
|
|
52
|
+
completed: 'text-emerald-400',
|
|
53
|
+
failed: 'text-red-400',
|
|
54
|
+
approved: 'text-emerald-400',
|
|
55
|
+
rejected: 'text-red-400',
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const PLATFORM_LABELS: Record<string, string> = {
|
|
59
|
+
discord: 'Discord',
|
|
60
|
+
telegram: 'Telegram',
|
|
61
|
+
slack: 'Slack',
|
|
62
|
+
whatsapp: 'WhatsApp',
|
|
63
|
+
openclaw: 'OpenClaw',
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function HomeView() {
|
|
67
|
+
const agents = useAppStore((s) => s.agents)
|
|
68
|
+
const sessions = useAppStore((s) => s.sessions)
|
|
69
|
+
const tasks = useAppStore((s) => s.tasks)
|
|
70
|
+
const connectors = useAppStore((s) => s.connectors)
|
|
71
|
+
const schedules = useAppStore((s) => s.schedules)
|
|
72
|
+
const activityEntries = useAppStore((s) => s.activityEntries)
|
|
73
|
+
const notifications = useAppStore((s) => s.notifications)
|
|
74
|
+
const unreadNotificationCount = useAppStore((s) => s.unreadNotificationCount)
|
|
75
|
+
const streamingSessionId = useChatStore((s) => s.streamingSessionId)
|
|
76
|
+
const loadActivity = useAppStore((s) => s.loadActivity)
|
|
77
|
+
const loadSchedules = useAppStore((s) => s.loadSchedules)
|
|
78
|
+
const loadNotifications = useAppStore((s) => s.loadNotifications)
|
|
79
|
+
const loadConnectors = useAppStore((s) => s.loadConnectors)
|
|
80
|
+
const markNotificationRead = useAppStore((s) => s.markNotificationRead)
|
|
81
|
+
const setActiveView = useAppStore((s) => s.setActiveView)
|
|
82
|
+
const setCurrentAgent = useAppStore((s) => s.setCurrentAgent)
|
|
83
|
+
const setCurrentSession = useAppStore((s) => s.setCurrentSession)
|
|
84
|
+
const setEditingTaskId = useAppStore((s) => s.setEditingTaskId)
|
|
85
|
+
const setTaskSheetOpen = useAppStore((s) => s.setTaskSheetOpen)
|
|
86
|
+
const setMessages = useChatStore((s) => s.setMessages)
|
|
87
|
+
const [todayCost, setTodayCost] = useState(0)
|
|
88
|
+
|
|
89
|
+
const allAgents = Object.values(agents).filter((a) => !a.trashedAt)
|
|
90
|
+
const pinnedAgents = allAgents.filter((a) => a.pinned)
|
|
91
|
+
|
|
92
|
+
const recentChats = useMemo(
|
|
93
|
+
() =>
|
|
94
|
+
Object.values(sessions)
|
|
95
|
+
.sort((a, b) => (b.lastActiveAt || 0) - (a.lastActiveAt || 0))
|
|
96
|
+
.slice(0, 5),
|
|
97
|
+
[sessions],
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
// Quick stats
|
|
101
|
+
const agentCount = allAgents.length
|
|
102
|
+
const allTasks = Object.values(tasks)
|
|
103
|
+
const activeTaskCount = allTasks.filter((t) => t.status === 'running' || t.status === 'queued').length
|
|
104
|
+
const allConnectors = Object.values(connectors)
|
|
105
|
+
const activeConnectorCount = allConnectors.filter((c) => c.status === 'running').length
|
|
106
|
+
|
|
107
|
+
// Agents with running tasks
|
|
108
|
+
const runningAgentIds = useMemo(() => {
|
|
109
|
+
const set = new Set<string>()
|
|
110
|
+
for (const task of allTasks) {
|
|
111
|
+
if (task.status === 'running' && task.agentId) set.add(task.agentId)
|
|
112
|
+
}
|
|
113
|
+
return set
|
|
114
|
+
}, [allTasks])
|
|
115
|
+
|
|
116
|
+
// Running tasks for the running tasks section
|
|
117
|
+
const runningTasks = useMemo(
|
|
118
|
+
() => allTasks.filter((t) => t.status === 'running' || t.status === 'queued').slice(0, 5),
|
|
119
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
120
|
+
[tasks],
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
// Upcoming schedules
|
|
124
|
+
const upcomingSchedules = useMemo(() => {
|
|
125
|
+
const now = Date.now()
|
|
126
|
+
return Object.values(schedules)
|
|
127
|
+
.filter((s) => s.status === 'active' && s.nextRunAt && s.nextRunAt > now)
|
|
128
|
+
.sort((a, b) => (a.nextRunAt || 0) - (b.nextRunAt || 0))
|
|
129
|
+
.slice(0, 5)
|
|
130
|
+
}, [schedules])
|
|
131
|
+
|
|
132
|
+
// Unread notifications
|
|
133
|
+
const unreadNotifications = useMemo(
|
|
134
|
+
() => notifications.filter((n) => !n.read).slice(0, 5),
|
|
135
|
+
[notifications],
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
// Recent activity (last 8)
|
|
139
|
+
const recentActivity = useMemo(() => activityEntries.slice(0, 8), [activityEntries])
|
|
140
|
+
|
|
141
|
+
// Load data on mount
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
void loadActivity({ limit: 8 })
|
|
144
|
+
void loadSchedules()
|
|
145
|
+
void loadNotifications()
|
|
146
|
+
void loadConnectors()
|
|
147
|
+
api<{ records: Array<{ estimatedCost: number }> }>('GET', '/usage?range=24h')
|
|
148
|
+
.then((data) => {
|
|
149
|
+
const total = (data.records || []).reduce((s, r) => s + (r.estimatedCost || 0), 0)
|
|
150
|
+
setTodayCost(total)
|
|
151
|
+
})
|
|
152
|
+
.catch(() => {})
|
|
153
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
154
|
+
}, [])
|
|
155
|
+
|
|
156
|
+
const handleAgentClick = (agent: Agent) => {
|
|
157
|
+
setMessages([])
|
|
158
|
+
void setCurrentAgent(agent.id)
|
|
159
|
+
setActiveView('agents')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const handleChatClick = (session: Session) => {
|
|
163
|
+
setCurrentSession(session.id)
|
|
164
|
+
setActiveView('agents')
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const handleTaskClick = (task: BoardTask) => {
|
|
168
|
+
setEditingTaskId(task.id)
|
|
169
|
+
setTaskSheetOpen(true)
|
|
170
|
+
setActiveView('tasks')
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const handleNotificationClick = (n: AppNotification) => {
|
|
174
|
+
if (!n.read) void markNotificationRead(n.id)
|
|
175
|
+
if (n.entityType === 'agent' && n.entityId) {
|
|
176
|
+
void setCurrentAgent(n.entityId)
|
|
177
|
+
setActiveView('agents')
|
|
178
|
+
} else if (n.entityType === 'task' && n.entityId) {
|
|
179
|
+
setEditingTaskId(n.entityId)
|
|
180
|
+
setTaskSheetOpen(true)
|
|
181
|
+
setActiveView('tasks')
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<div className="flex-1 overflow-y-auto">
|
|
187
|
+
<div className="max-w-[800px] mx-auto px-6 py-10">
|
|
188
|
+
{/* Header */}
|
|
189
|
+
<div className="mb-10">
|
|
190
|
+
<h1 className="font-display text-[28px] font-700 text-text tracking-[-0.03em]">
|
|
191
|
+
SwarmClaw
|
|
192
|
+
</h1>
|
|
193
|
+
<p className="text-[14px] text-text-3 mt-1">
|
|
194
|
+
Your AI agent orchestration dashboard
|
|
195
|
+
</p>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
{/* Quick Stats */}
|
|
199
|
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-10">
|
|
200
|
+
<StatCard label="Agents" value={String(agentCount)} />
|
|
201
|
+
<StatCard label="Active Tasks" value={String(activeTaskCount)} accent={activeTaskCount > 0} />
|
|
202
|
+
<StatCard label="Today's Cost" value={`$${todayCost.toFixed(2)}`} />
|
|
203
|
+
<StatCard label="Connectors" value={`${activeConnectorCount}/${allConnectors.length}`} accent={activeConnectorCount > 0} />
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
{/* Notifications banner */}
|
|
207
|
+
{unreadNotifications.length > 0 && (
|
|
208
|
+
<section className="mb-8">
|
|
209
|
+
<div className="rounded-[14px] border border-amber-400/20 bg-amber-400/[0.04] overflow-hidden">
|
|
210
|
+
<div className="flex items-center gap-2 px-4 py-2.5 border-b border-amber-400/10">
|
|
211
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-amber-400">
|
|
212
|
+
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
|
|
213
|
+
<path d="M13.73 21a2 2 0 0 1-3.46 0" />
|
|
214
|
+
</svg>
|
|
215
|
+
<span className="text-[12px] font-600 text-amber-400">
|
|
216
|
+
{unreadNotificationCount} unread notification{unreadNotificationCount !== 1 ? 's' : ''}
|
|
217
|
+
</span>
|
|
218
|
+
</div>
|
|
219
|
+
<div className="flex flex-col">
|
|
220
|
+
{unreadNotifications.map((n) => (
|
|
221
|
+
<button
|
|
222
|
+
key={n.id}
|
|
223
|
+
onClick={() => handleNotificationClick(n)}
|
|
224
|
+
className="flex items-start gap-3 px-4 py-2.5 text-left bg-transparent border-none cursor-pointer
|
|
225
|
+
hover:bg-white/[0.03] transition-colors w-full"
|
|
226
|
+
style={{ fontFamily: 'inherit' }}
|
|
227
|
+
>
|
|
228
|
+
<div className={`w-1.5 h-1.5 rounded-full mt-1.5 shrink-0 ${
|
|
229
|
+
n.type === 'error' ? 'bg-red-400' : n.type === 'warning' ? 'bg-amber-400' : n.type === 'success' ? 'bg-emerald-400' : 'bg-sky-400'
|
|
230
|
+
}`} />
|
|
231
|
+
<div className="flex-1 min-w-0">
|
|
232
|
+
<span className="text-[13px] font-500 text-text">{n.title}</span>
|
|
233
|
+
{n.message && <p className="text-[11px] text-text-3/60 truncate mt-0.5 m-0">{n.message}</p>}
|
|
234
|
+
</div>
|
|
235
|
+
<span className="text-[10px] text-text-3/40 shrink-0 mt-0.5">{timeAgo(n.createdAt)}</span>
|
|
236
|
+
</button>
|
|
237
|
+
))}
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
</section>
|
|
241
|
+
)}
|
|
242
|
+
|
|
243
|
+
{/* Connector Status */}
|
|
244
|
+
<section className="mb-8">
|
|
245
|
+
<SectionHeader label="Connectors" onViewAll={allConnectors.length > 0 ? () => setActiveView('connectors') : undefined} />
|
|
246
|
+
{allConnectors.length > 0 ? (
|
|
247
|
+
<div className="flex gap-2 flex-wrap">
|
|
248
|
+
{allConnectors.map((c) => (
|
|
249
|
+
<div
|
|
250
|
+
key={c.id}
|
|
251
|
+
className="flex items-center gap-2 px-3 py-2 rounded-[10px] bg-white/[0.03] border border-white/[0.06]"
|
|
252
|
+
>
|
|
253
|
+
<div className={`w-2 h-2 rounded-full ${
|
|
254
|
+
c.status === 'running' ? 'bg-emerald-400 shadow-[0_0_6px_rgba(52,211,153,0.4)]'
|
|
255
|
+
: c.status === 'error' ? 'bg-red-400' : 'bg-text-3/30'
|
|
256
|
+
}`} />
|
|
257
|
+
<span className="text-[12px] font-500 text-text">{c.name}</span>
|
|
258
|
+
<span className="text-[10px] text-text-3/50">{PLATFORM_LABELS[c.platform] || c.platform}</span>
|
|
259
|
+
</div>
|
|
260
|
+
))}
|
|
261
|
+
</div>
|
|
262
|
+
) : (
|
|
263
|
+
<EmptySection text="No connectors configured — bridge agents to Discord, Slack, Telegram, or WhatsApp" />
|
|
264
|
+
)}
|
|
265
|
+
</section>
|
|
266
|
+
|
|
267
|
+
{/* Two-column layout: Running Tasks + Upcoming Schedules */}
|
|
268
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
|
269
|
+
{/* Running Tasks */}
|
|
270
|
+
<section>
|
|
271
|
+
<SectionHeader label="Running Tasks" onViewAll={runningTasks.length > 0 ? () => setActiveView('tasks') : undefined} />
|
|
272
|
+
{runningTasks.length > 0 ? (
|
|
273
|
+
<div className="flex flex-col gap-1">
|
|
274
|
+
{runningTasks.map((task) => {
|
|
275
|
+
const agent = task.agentId ? agents[task.agentId] : null
|
|
276
|
+
return (
|
|
277
|
+
<button
|
|
278
|
+
key={task.id}
|
|
279
|
+
onClick={() => handleTaskClick(task)}
|
|
280
|
+
className="flex items-center gap-2.5 px-3 py-2.5 rounded-[10px] bg-transparent border-none
|
|
281
|
+
hover:bg-white/[0.04] transition-colors cursor-pointer w-full text-left"
|
|
282
|
+
style={{ fontFamily: 'inherit' }}
|
|
283
|
+
>
|
|
284
|
+
<div className={`w-2 h-2 rounded-full shrink-0 ${
|
|
285
|
+
task.status === 'running' ? 'bg-emerald-400 animate-pulse' : 'bg-amber-400'
|
|
286
|
+
}`} />
|
|
287
|
+
<div className="flex-1 min-w-0">
|
|
288
|
+
<span className="text-[13px] font-500 text-text truncate block">{task.title}</span>
|
|
289
|
+
<span className="text-[11px] text-text-3/50">
|
|
290
|
+
{agent?.name || 'Unassigned'} · {task.status === 'running' ? 'running' : 'queued'}{task.startedAt ? ` · ${timeAgo(task.startedAt)}` : ''}
|
|
291
|
+
</span>
|
|
292
|
+
</div>
|
|
293
|
+
</button>
|
|
294
|
+
)
|
|
295
|
+
})}
|
|
296
|
+
</div>
|
|
297
|
+
) : (
|
|
298
|
+
<div className="py-4 px-3 text-[12px] text-text-3/40">No tasks running</div>
|
|
299
|
+
)}
|
|
300
|
+
</section>
|
|
301
|
+
|
|
302
|
+
{/* Upcoming Schedules */}
|
|
303
|
+
<section>
|
|
304
|
+
<SectionHeader label="Upcoming Schedules" onViewAll={upcomingSchedules.length > 0 ? () => setActiveView('schedules') : undefined} />
|
|
305
|
+
{upcomingSchedules.length > 0 ? (
|
|
306
|
+
<div className="flex flex-col gap-1">
|
|
307
|
+
{upcomingSchedules.map((sched) => {
|
|
308
|
+
const agent = sched.agentId ? agents[sched.agentId] : null
|
|
309
|
+
return (
|
|
310
|
+
<div
|
|
311
|
+
key={sched.id}
|
|
312
|
+
className="flex items-center gap-2.5 px-3 py-2.5 rounded-[10px]"
|
|
313
|
+
>
|
|
314
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3/50 shrink-0">
|
|
315
|
+
<circle cx="12" cy="12" r="10" /><path d="M12 6v6l4 2" />
|
|
316
|
+
</svg>
|
|
317
|
+
<div className="flex-1 min-w-0">
|
|
318
|
+
<span className="text-[13px] font-500 text-text truncate block">{sched.name}</span>
|
|
319
|
+
<span className="text-[11px] text-text-3/50">
|
|
320
|
+
{agent?.name || 'No agent'} · {sched.nextRunAt ? timeUntil(sched.nextRunAt) : '—'}
|
|
321
|
+
</span>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
)
|
|
325
|
+
})}
|
|
326
|
+
</div>
|
|
327
|
+
) : (
|
|
328
|
+
<div className="py-4 px-3 text-[12px] text-text-3/40">No upcoming schedules</div>
|
|
329
|
+
)}
|
|
330
|
+
</section>
|
|
331
|
+
</div>
|
|
332
|
+
|
|
333
|
+
{/* Pinned Agents */}
|
|
334
|
+
<section className="mb-8">
|
|
335
|
+
<SectionHeader label="Pinned Agents" />
|
|
336
|
+
{pinnedAgents.length > 0 ? (
|
|
337
|
+
<div className="flex gap-3 overflow-x-auto pb-2">
|
|
338
|
+
{pinnedAgents.map((agent) => {
|
|
339
|
+
const threadSession = agent.threadSessionId ? sessions[agent.threadSessionId] as Session | undefined : undefined
|
|
340
|
+
const heartbeatOn = agent.heartbeatEnabled === true && (agent.tools?.length ?? 0) > 0
|
|
341
|
+
const recentlyActive = (threadSession?.lastActiveAt ?? 0) > Date.now() - 30 * 60 * 1000
|
|
342
|
+
const isOnline = runningAgentIds.has(agent.id) || (threadSession?.active ?? false) || heartbeatOn || recentlyActive
|
|
343
|
+
const isTyping = streamingSessionId === agent.threadSessionId
|
|
344
|
+
const lastActive = threadSession?.lastActiveAt || agent.lastUsedAt || agent.updatedAt
|
|
345
|
+
const modelLabel = agent.model ? agent.model.split('/').pop()?.split(':')[0] : agent.provider
|
|
346
|
+
|
|
347
|
+
return (
|
|
348
|
+
<button
|
|
349
|
+
key={agent.id}
|
|
350
|
+
onClick={() => handleAgentClick(agent)}
|
|
351
|
+
className="flex flex-col items-center gap-1.5 px-4 py-3.5 rounded-[14px] bg-white/[0.03] border border-white/[0.06]
|
|
352
|
+
hover:bg-white/[0.06] hover:border-white/[0.1] transition-all cursor-pointer min-w-[130px] shrink-0"
|
|
353
|
+
style={{ fontFamily: 'inherit' }}
|
|
354
|
+
>
|
|
355
|
+
<div className="relative">
|
|
356
|
+
<AgentAvatar seed={agent.avatarSeed} name={agent.name} size={36} />
|
|
357
|
+
<div className={`absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-[#1a1a2e] ${
|
|
358
|
+
isTyping ? 'bg-accent-bright animate-pulse'
|
|
359
|
+
: isOnline ? 'bg-emerald-400 shadow-[0_0_6px_rgba(52,211,153,0.4)]'
|
|
360
|
+
: 'bg-text-3/30'
|
|
361
|
+
}`} />
|
|
362
|
+
</div>
|
|
363
|
+
<span className="font-display text-[13px] font-600 text-text truncate max-w-[110px]">
|
|
364
|
+
{agent.name}
|
|
365
|
+
</span>
|
|
366
|
+
{isTyping ? (
|
|
367
|
+
<span className="text-[10px] text-accent-bright/70 flex items-center gap-1">
|
|
368
|
+
<span className="flex gap-0.5">
|
|
369
|
+
<span className="w-1 h-1 rounded-full bg-accent-bright/70 animate-bounce [animation-delay:0ms]" />
|
|
370
|
+
<span className="w-1 h-1 rounded-full bg-accent-bright/70 animate-bounce [animation-delay:150ms]" />
|
|
371
|
+
<span className="w-1 h-1 rounded-full bg-accent-bright/70 animate-bounce [animation-delay:300ms]" />
|
|
372
|
+
</span>
|
|
373
|
+
typing
|
|
374
|
+
</span>
|
|
375
|
+
) : (
|
|
376
|
+
<span className={`text-[10px] ${isOnline ? 'text-emerald-400/80' : 'text-text-3/50'}`}>
|
|
377
|
+
{isOnline ? 'Online' : lastActive ? timeAgo(lastActive) : 'Idle'}
|
|
378
|
+
</span>
|
|
379
|
+
)}
|
|
380
|
+
{modelLabel && (
|
|
381
|
+
<span className="text-[9px] text-text-3/40 font-mono truncate max-w-[110px]">
|
|
382
|
+
{modelLabel}
|
|
383
|
+
</span>
|
|
384
|
+
)}
|
|
385
|
+
</button>
|
|
386
|
+
)
|
|
387
|
+
})}
|
|
388
|
+
</div>
|
|
389
|
+
) : (
|
|
390
|
+
<div className="py-6 px-4 rounded-[14px] bg-white/[0.02] border border-dashed border-white/[0.06] text-center">
|
|
391
|
+
<p className="text-[13px] text-text-3/60">
|
|
392
|
+
Star agents from the chat list for quick access
|
|
393
|
+
</p>
|
|
394
|
+
</div>
|
|
395
|
+
)}
|
|
396
|
+
</section>
|
|
397
|
+
|
|
398
|
+
{/* Recent Chats */}
|
|
399
|
+
<section className="mb-8">
|
|
400
|
+
<SectionHeader label="Recent Chats" />
|
|
401
|
+
{recentChats.length > 0 ? (
|
|
402
|
+
<div className="flex flex-col gap-1">
|
|
403
|
+
{recentChats.map((session) => {
|
|
404
|
+
const agent = session.agentId ? agents[session.agentId] : null
|
|
405
|
+
const lastMsg = session.messages?.[session.messages.length - 1]
|
|
406
|
+
const displayName = agent?.name || 'Chat'
|
|
407
|
+
return (
|
|
408
|
+
<button
|
|
409
|
+
key={session.id}
|
|
410
|
+
onClick={() => handleChatClick(session)}
|
|
411
|
+
className="flex items-center gap-3 px-4 py-3 rounded-[12px] bg-transparent border-none
|
|
412
|
+
hover:bg-white/[0.04] transition-all cursor-pointer w-full text-left"
|
|
413
|
+
style={{ fontFamily: 'inherit' }}
|
|
414
|
+
>
|
|
415
|
+
<AgentAvatar
|
|
416
|
+
seed={agent?.avatarSeed}
|
|
417
|
+
name={displayName}
|
|
418
|
+
size={28}
|
|
419
|
+
/>
|
|
420
|
+
<div className="flex-1 min-w-0">
|
|
421
|
+
<div className="flex items-center gap-2">
|
|
422
|
+
<span className="text-[13px] font-600 text-text truncate">
|
|
423
|
+
{displayName}
|
|
424
|
+
</span>
|
|
425
|
+
<span className="text-[11px] text-text-3/50 shrink-0">
|
|
426
|
+
{timeAgo(session.lastActiveAt || session.createdAt)}
|
|
427
|
+
</span>
|
|
428
|
+
</div>
|
|
429
|
+
{lastMsg && (
|
|
430
|
+
<p className="text-[12px] text-text-3/60 truncate mt-0.5 m-0">
|
|
431
|
+
{lastMsg.text.slice(0, 80)}
|
|
432
|
+
</p>
|
|
433
|
+
)}
|
|
434
|
+
</div>
|
|
435
|
+
</button>
|
|
436
|
+
)
|
|
437
|
+
})}
|
|
438
|
+
</div>
|
|
439
|
+
) : (
|
|
440
|
+
<EmptySection text="No chats yet — start by clicking an agent" />
|
|
441
|
+
)}
|
|
442
|
+
</section>
|
|
443
|
+
|
|
444
|
+
{/* Activity Feed */}
|
|
445
|
+
{recentActivity.length > 0 && (
|
|
446
|
+
<section className="mb-10">
|
|
447
|
+
<SectionHeader label="Recent Activity" />
|
|
448
|
+
<div className="flex flex-col gap-0.5">
|
|
449
|
+
{recentActivity.map((entry) => (
|
|
450
|
+
<div key={entry.id} className="flex items-center gap-2.5 px-3 py-2 rounded-[10px]">
|
|
451
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"
|
|
452
|
+
className={`shrink-0 ${ACTIVITY_COLORS[entry.action] || 'text-text-3'}`}>
|
|
453
|
+
<path d={ACTIVITY_ICONS[entry.action] || ACTIVITY_ICONS.updated} />
|
|
454
|
+
</svg>
|
|
455
|
+
<span className="text-[12px] text-text-3/80 flex-1 truncate">{entry.summary}</span>
|
|
456
|
+
<span className="text-[10px] text-text-3/40 shrink-0">{timeAgo(entry.timestamp)}</span>
|
|
457
|
+
</div>
|
|
458
|
+
))}
|
|
459
|
+
</div>
|
|
460
|
+
</section>
|
|
461
|
+
)}
|
|
462
|
+
</div>
|
|
463
|
+
</div>
|
|
464
|
+
)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function SectionHeader({ label, onViewAll }: { label: string; onViewAll?: () => void }) {
|
|
468
|
+
return (
|
|
469
|
+
<div className="flex items-center justify-between mb-3">
|
|
470
|
+
<h2 className="font-display text-[13px] font-600 text-text-2 uppercase tracking-[0.08em]">
|
|
471
|
+
{label}
|
|
472
|
+
</h2>
|
|
473
|
+
{onViewAll && (
|
|
474
|
+
<button
|
|
475
|
+
onClick={onViewAll}
|
|
476
|
+
className="text-[11px] text-text-3/50 hover:text-text-3 transition-colors bg-transparent border-none cursor-pointer"
|
|
477
|
+
style={{ fontFamily: 'inherit' }}
|
|
478
|
+
>
|
|
479
|
+
View all →
|
|
480
|
+
</button>
|
|
481
|
+
)}
|
|
482
|
+
</div>
|
|
483
|
+
)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function StatCard({ label, value, accent }: { label: string; value: string; accent?: boolean }) {
|
|
487
|
+
return (
|
|
488
|
+
<div className="px-4 py-3 rounded-[12px] bg-white/[0.03] border border-white/[0.06]">
|
|
489
|
+
<p className="text-[11px] font-600 text-text-3/60 uppercase tracking-wider mb-1">{label}</p>
|
|
490
|
+
<p className={`font-display text-[20px] font-700 tracking-[-0.02em] ${accent ? 'text-accent-bright' : 'text-text'}`}>{value}</p>
|
|
491
|
+
</div>
|
|
492
|
+
)
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function EmptySection({ text }: { text: string }) {
|
|
496
|
+
return (
|
|
497
|
+
<div className="py-6 px-4 rounded-[14px] bg-white/[0.02] border border-dashed border-white/[0.06] text-center">
|
|
498
|
+
<p className="text-[13px] text-text-3/60">{text}</p>
|
|
499
|
+
</div>
|
|
500
|
+
)
|
|
501
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useCallback, useRef, useState } from 'react'
|
|
4
|
-
import { useChatStore
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
4
|
+
import { useChatStore } 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'
|
|
8
8
|
import { useSpeechRecognition } from '@/hooks/use-speech-recognition'
|
|
9
|
+
import { FilePreview } from '@/components/shared/file-preview'
|
|
9
10
|
|
|
10
11
|
interface Props {
|
|
11
12
|
streaming: boolean
|
|
@@ -13,36 +14,7 @@ interface Props {
|
|
|
13
14
|
onStop: () => void
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
|
|
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
|
-
}
|
|
17
|
+
// FilePreview is now imported from @/components/shared/file-preview
|
|
46
18
|
|
|
47
19
|
export function ChatInput({ streaming, onSend, onStop }: Props) {
|
|
48
20
|
const [value, setValue] = useState('')
|
|
@@ -53,16 +25,51 @@ export function ChatInput({ streaming, onSend, onStop }: Props) {
|
|
|
53
25
|
const addPendingFile = useChatStore((s) => s.addPendingFile)
|
|
54
26
|
const removePendingFile = useChatStore((s) => s.removePendingFile)
|
|
55
27
|
const speechRecognitionLang = useAppStore((s) => s.appSettings.speechRecognitionLang)
|
|
28
|
+
const sessionId = useAppStore((s) => s.currentSessionId)
|
|
29
|
+
|
|
30
|
+
const queuedMessages = useChatStore((s) => s.queuedMessages)
|
|
31
|
+
const addQueuedMessage = useChatStore((s) => s.addQueuedMessage)
|
|
32
|
+
const removeQueuedMessage = useChatStore((s) => s.removeQueuedMessage)
|
|
33
|
+
|
|
34
|
+
// Draft persistence: restore on session change
|
|
35
|
+
const draftTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!sessionId) return
|
|
38
|
+
const draft = localStorage.getItem(`sc_draft_${sessionId}`)
|
|
39
|
+
setValue(draft || '')
|
|
40
|
+
}, [sessionId])
|
|
41
|
+
|
|
42
|
+
// Debounced save to localStorage
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!sessionId) return
|
|
45
|
+
if (draftTimerRef.current) clearTimeout(draftTimerRef.current)
|
|
46
|
+
draftTimerRef.current = setTimeout(() => {
|
|
47
|
+
if (value) localStorage.setItem(`sc_draft_${sessionId}`, value)
|
|
48
|
+
else localStorage.removeItem(`sc_draft_${sessionId}`)
|
|
49
|
+
}, 300)
|
|
50
|
+
return () => { if (draftTimerRef.current) clearTimeout(draftTimerRef.current) }
|
|
51
|
+
}, [value, sessionId])
|
|
56
52
|
|
|
57
53
|
const handleSend = useCallback(() => {
|
|
58
54
|
const text = value.trim()
|
|
59
|
-
if (
|
|
55
|
+
if (!text && !pendingFiles.length) return
|
|
56
|
+
// If streaming, queue the message instead of blocking
|
|
57
|
+
if (streaming) {
|
|
58
|
+
if (text) {
|
|
59
|
+
addQueuedMessage(text)
|
|
60
|
+
setValue('')
|
|
61
|
+
if (textareaRef.current) textareaRef.current.style.height = 'auto'
|
|
62
|
+
}
|
|
63
|
+
return
|
|
64
|
+
}
|
|
60
65
|
onSend(text || 'See attached file(s).')
|
|
61
66
|
setValue('')
|
|
67
|
+
if (sessionId) localStorage.removeItem(`sc_draft_${sessionId}`)
|
|
62
68
|
if (textareaRef.current) {
|
|
63
69
|
textareaRef.current.style.height = 'auto'
|
|
64
70
|
}
|
|
65
|
-
|
|
71
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
72
|
+
}, [value, streaming, onSend, pendingFiles.length, sessionId])
|
|
66
73
|
|
|
67
74
|
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
|
68
75
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
@@ -131,6 +138,27 @@ export function ChatInput({ streaming, onSend, onStop }: Props) {
|
|
|
131
138
|
</div>
|
|
132
139
|
)}
|
|
133
140
|
|
|
141
|
+
{queuedMessages.length > 0 && (
|
|
142
|
+
<div className="flex flex-wrap items-center gap-1.5 mb-2">
|
|
143
|
+
<span className="label-mono text-amber-400/70">Queued</span>
|
|
144
|
+
{queuedMessages.map((msg, i) => (
|
|
145
|
+
<span key={i} className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] bg-amber-500/10 border border-amber-500/15 text-[12px] text-amber-300 font-mono max-w-[200px]">
|
|
146
|
+
<span className="truncate">{msg}</span>
|
|
147
|
+
<button
|
|
148
|
+
type="button"
|
|
149
|
+
onClick={() => removeQueuedMessage(i)}
|
|
150
|
+
className="shrink-0 text-amber-400/60 hover:text-amber-300 border-none bg-transparent cursor-pointer p-0"
|
|
151
|
+
>
|
|
152
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
|
|
153
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
154
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
155
|
+
</svg>
|
|
156
|
+
</button>
|
|
157
|
+
</span>
|
|
158
|
+
))}
|
|
159
|
+
</div>
|
|
160
|
+
)}
|
|
161
|
+
|
|
134
162
|
<div className="glass rounded-[20px] overflow-hidden
|
|
135
163
|
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">
|
|
136
164
|
|
|
@@ -206,17 +234,27 @@ export function ChatInput({ streaming, onSend, onStop }: Props) {
|
|
|
206
234
|
|
|
207
235
|
<button
|
|
208
236
|
onClick={handleSend}
|
|
209
|
-
disabled={!hasContent
|
|
237
|
+
disabled={!hasContent}
|
|
210
238
|
className={`w-9 h-9 rounded-[11px] border-none flex items-center justify-center
|
|
211
239
|
shrink-0 cursor-pointer transition-all duration-250
|
|
212
|
-
${hasContent
|
|
213
|
-
?
|
|
240
|
+
${hasContent
|
|
241
|
+
? streaming
|
|
242
|
+
? 'bg-amber-500/20 text-amber-400 active:scale-90 border border-amber-500/30'
|
|
243
|
+
: 'bg-accent-bright text-white active:scale-90 shadow-[0_4px_16px_rgba(99,102,241,0.3)]'
|
|
214
244
|
: 'bg-white/[0.04] text-text-3 pointer-events-none'}`}
|
|
245
|
+
title={streaming ? 'Queue message' : 'Send message'}
|
|
215
246
|
>
|
|
216
|
-
|
|
217
|
-
<
|
|
218
|
-
|
|
219
|
-
|
|
247
|
+
{streaming && hasContent ? (
|
|
248
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
|
|
249
|
+
<line x1="12" y1="5" x2="12" y2="19" />
|
|
250
|
+
<line x1="5" y1="12" x2="19" y2="12" />
|
|
251
|
+
</svg>
|
|
252
|
+
) : (
|
|
253
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
|
|
254
|
+
<line x1="12" y1="19" x2="12" y2="5" />
|
|
255
|
+
<polyline points="5 12 12 5 19 12" />
|
|
256
|
+
</svg>
|
|
257
|
+
)}
|
|
220
258
|
</button>
|
|
221
259
|
</div>
|
|
222
260
|
</div>
|