@swarmclawai/swarmclaw 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -2
- package/bin/server-cmd.js +1 -0
- package/package.json +2 -1
- package/src/app/api/canvas/[sessionId]/route.ts +31 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +10 -136
- package/src/app/api/connectors/[id]/route.ts +1 -0
- package/src/app/api/connectors/route.ts +2 -1
- package/src/app/api/files/open/route.ts +43 -0
- package/src/app/api/search/route.ts +9 -7
- package/src/app/api/sessions/[id]/messages/route.ts +70 -2
- package/src/app/api/sessions/[id]/route.ts +4 -0
- package/src/app/api/tasks/metrics/route.ts +101 -0
- package/src/app/api/tasks/route.ts +17 -2
- package/src/app/api/tts/route.ts +3 -2
- package/src/app/api/tts/stream/route.ts +3 -2
- package/src/app/api/uploads/[filename]/route.ts +19 -34
- package/src/app/api/uploads/route.ts +94 -0
- package/src/app/globals.css +5 -0
- package/src/cli/index.js +16 -1
- package/src/cli/spec.js +26 -0
- package/src/components/agents/agent-card.tsx +3 -3
- package/src/components/agents/agent-chat-list.tsx +29 -6
- package/src/components/agents/agent-sheet.tsx +66 -4
- package/src/components/agents/inspector-panel.tsx +81 -6
- package/src/components/agents/openclaw-skills-panel.tsx +32 -3
- package/src/components/agents/personality-builder.tsx +42 -14
- package/src/components/agents/soul-library-picker.tsx +89 -0
- package/src/components/canvas/canvas-panel.tsx +96 -0
- package/src/components/chat/activity-moment.tsx +8 -4
- package/src/components/chat/chat-area.tsx +46 -22
- package/src/components/chat/chat-header.tsx +455 -286
- package/src/components/chat/chat-preview-panel.tsx +1 -2
- package/src/components/chat/delegation-banner.tsx +371 -0
- package/src/components/chat/file-path-chip.tsx +23 -2
- package/src/components/chat/heartbeat-history-panel.tsx +269 -0
- package/src/components/chat/message-bubble.tsx +315 -25
- package/src/components/chat/message-list.tsx +180 -7
- package/src/components/chat/streaming-bubble.tsx +68 -1
- package/src/components/chat/tool-call-bubble.tsx +45 -3
- package/src/components/chat/transfer-agent-picker.tsx +1 -1
- package/src/components/chatrooms/chatroom-list.tsx +8 -1
- package/src/components/chatrooms/chatroom-message.tsx +8 -3
- package/src/components/chatrooms/chatroom-view.tsx +3 -3
- package/src/components/connectors/connector-list.tsx +168 -90
- package/src/components/connectors/connector-sheet.tsx +68 -16
- package/src/components/home/home-view.tsx +1 -1
- package/src/components/input/chat-input.tsx +28 -2
- package/src/components/layout/app-layout.tsx +19 -2
- package/src/components/projects/project-detail.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +260 -127
- package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
- package/src/components/shared/agent-switch-dialog.tsx +1 -1
- package/src/components/shared/chatroom-picker-list.tsx +61 -0
- package/src/components/shared/connector-platform-icon.tsx +51 -4
- package/src/components/shared/icon-button.tsx +16 -2
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +1 -1
- package/src/components/shared/search-dialog.tsx +17 -10
- package/src/components/shared/settings/section-embedding.tsx +48 -13
- package/src/components/shared/settings/section-orchestrator.tsx +46 -15
- package/src/components/shared/settings/section-storage.tsx +206 -0
- package/src/components/shared/settings/section-user-preferences.tsx +18 -0
- package/src/components/shared/settings/section-voice.tsx +42 -21
- package/src/components/shared/settings/section-web-search.tsx +30 -6
- package/src/components/shared/settings/settings-page.tsx +3 -1
- package/src/components/shared/settings/storage-browser.tsx +259 -0
- package/src/components/tasks/task-card.tsx +14 -1
- package/src/components/tasks/task-sheet.tsx +328 -3
- package/src/components/usage/metrics-dashboard.tsx +90 -6
- package/src/hooks/use-continuous-speech.ts +10 -4
- package/src/hooks/use-voice-conversation.ts +53 -10
- package/src/hooks/use-ws.ts +4 -2
- package/src/lib/providers/anthropic.ts +13 -7
- package/src/lib/providers/index.ts +1 -0
- package/src/lib/providers/openai.ts +13 -7
- package/src/lib/server/chat-execution.ts +51 -11
- package/src/lib/server/chatroom-helpers.ts +146 -0
- package/src/lib/server/connectors/manager.ts +218 -7
- package/src/lib/server/heartbeat-service.ts +8 -1
- package/src/lib/server/main-agent-loop.ts +1 -1
- package/src/lib/server/memory-consolidation.ts +15 -2
- package/src/lib/server/memory-db.ts +134 -6
- package/src/lib/server/mime.ts +51 -0
- package/src/lib/server/openclaw-gateway.ts +2 -2
- package/src/lib/server/orchestrator-lg.ts +2 -0
- package/src/lib/server/orchestrator.ts +5 -2
- package/src/lib/server/playwright-proxy.mjs +2 -3
- package/src/lib/server/prompt-runtime-context.ts +53 -0
- package/src/lib/server/queue.ts +52 -7
- package/src/lib/server/session-tools/canvas.ts +67 -0
- package/src/lib/server/session-tools/connector.ts +83 -9
- package/src/lib/server/session-tools/crud.ts +21 -0
- package/src/lib/server/session-tools/delegate.ts +68 -4
- package/src/lib/server/session-tools/git.ts +71 -0
- package/src/lib/server/session-tools/http.ts +57 -0
- package/src/lib/server/session-tools/index.ts +8 -0
- package/src/lib/server/session-tools/memory.ts +1 -0
- package/src/lib/server/session-tools/search-providers.ts +16 -8
- package/src/lib/server/session-tools/subagent.ts +106 -0
- package/src/lib/server/session-tools/web.ts +115 -4
- package/src/lib/server/stream-agent-chat.ts +32 -10
- package/src/lib/server/task-mention.ts +41 -0
- package/src/lib/sessions.ts +10 -0
- package/src/lib/soul-library.ts +103 -0
- package/src/lib/task-dedupe.ts +26 -0
- package/src/lib/tool-definitions.ts +2 -0
- package/src/lib/tts.ts +2 -2
- package/src/stores/use-app-store.ts +5 -1
- package/src/stores/use-chat-store.ts +65 -2
- package/src/types/index.ts +32 -2
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import type { SettingsSectionProps } from './types'
|
|
4
4
|
|
|
5
5
|
export function VoiceSection({ appSettings, patchSettings, inputClass }: SettingsSectionProps) {
|
|
6
|
+
const enabled = appSettings.elevenLabsEnabled ?? false
|
|
7
|
+
|
|
6
8
|
return (
|
|
7
9
|
<div className="mb-10">
|
|
8
10
|
<h3 className="font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
|
|
@@ -12,30 +14,49 @@ export function VoiceSection({ appSettings, patchSettings, inputClass }: Setting
|
|
|
12
14
|
Configure voice playback (TTS) and speech-to-text input.
|
|
13
15
|
</p>
|
|
14
16
|
<div className="p-6 rounded-[18px] bg-surface border border-white/[0.06]">
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">ElevenLabs API Key</label>
|
|
18
|
-
<input
|
|
19
|
-
type="password"
|
|
20
|
-
value={appSettings.elevenLabsApiKey || ''}
|
|
21
|
-
onChange={(e) => patchSettings({ elevenLabsApiKey: e.target.value || null })}
|
|
22
|
-
placeholder="sk_..."
|
|
23
|
-
className={inputClass}
|
|
24
|
-
style={{ fontFamily: 'inherit' }}
|
|
25
|
-
/>
|
|
26
|
-
</div>
|
|
17
|
+
{/* ElevenLabs toggle */}
|
|
18
|
+
<div className="flex items-center justify-between mb-5">
|
|
27
19
|
<div>
|
|
28
|
-
<label className="
|
|
29
|
-
<
|
|
30
|
-
type="text"
|
|
31
|
-
value={appSettings.elevenLabsVoiceId || ''}
|
|
32
|
-
onChange={(e) => patchSettings({ elevenLabsVoiceId: e.target.value || null })}
|
|
33
|
-
placeholder="JBFqnCBsd6RMkjVDRZzb"
|
|
34
|
-
className={inputClass}
|
|
35
|
-
style={{ fontFamily: 'inherit' }}
|
|
36
|
-
/>
|
|
20
|
+
<label className="font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em]">ElevenLabs TTS</label>
|
|
21
|
+
<p className="text-[11px] text-text-3/60 mt-0.5">Enable text-to-speech for agent responses</p>
|
|
37
22
|
</div>
|
|
23
|
+
<button
|
|
24
|
+
type="button"
|
|
25
|
+
onClick={() => patchSettings({ elevenLabsEnabled: !enabled })}
|
|
26
|
+
className={`relative w-10 h-[22px] rounded-full transition-colors cursor-pointer border-none ${enabled ? 'bg-accent-bright' : 'bg-surface-3'}`}
|
|
27
|
+
>
|
|
28
|
+
<span className={`absolute top-[3px] left-[3px] w-4 h-4 rounded-full bg-white transition-transform ${enabled ? 'translate-x-[18px]' : ''}`} />
|
|
29
|
+
</button>
|
|
38
30
|
</div>
|
|
31
|
+
|
|
32
|
+
{enabled && (
|
|
33
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mb-5">
|
|
34
|
+
<div>
|
|
35
|
+
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">API Key</label>
|
|
36
|
+
<input
|
|
37
|
+
type="password"
|
|
38
|
+
value={appSettings.elevenLabsApiKey || ''}
|
|
39
|
+
onChange={(e) => patchSettings({ elevenLabsApiKey: e.target.value || null })}
|
|
40
|
+
placeholder="sk_..."
|
|
41
|
+
className={inputClass}
|
|
42
|
+
style={{ fontFamily: 'inherit' }}
|
|
43
|
+
/>
|
|
44
|
+
</div>
|
|
45
|
+
<div>
|
|
46
|
+
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Default Voice ID</label>
|
|
47
|
+
<input
|
|
48
|
+
type="text"
|
|
49
|
+
value={appSettings.elevenLabsVoiceId || ''}
|
|
50
|
+
onChange={(e) => patchSettings({ elevenLabsVoiceId: e.target.value || null })}
|
|
51
|
+
placeholder="JBFqnCBsd6RMkjVDRZzb"
|
|
52
|
+
className={inputClass}
|
|
53
|
+
style={{ fontFamily: 'inherit' }}
|
|
54
|
+
/>
|
|
55
|
+
<p className="text-[11px] text-text-3/60 mt-1.5">Fallback voice when an agent has no override set.</p>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
)}
|
|
59
|
+
|
|
39
60
|
<div>
|
|
40
61
|
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Speech Recognition Language</label>
|
|
41
62
|
<input
|
|
@@ -26,8 +26,8 @@ export function WebSearchSection({ appSettings, patchSettings, inputClass }: Set
|
|
|
26
26
|
<option value="google">Google (scraping, no key required)</option>
|
|
27
27
|
<option value="bing">Bing (scraping, no key required)</option>
|
|
28
28
|
<option value="searxng">SearXNG (self-hosted, no key required)</option>
|
|
29
|
-
<option value="tavily">Tavily (
|
|
30
|
-
<option value="brave">Brave Search (
|
|
29
|
+
<option value="tavily">Tavily (API key required)</option>
|
|
30
|
+
<option value="brave">Brave Search (API key required)</option>
|
|
31
31
|
</select>
|
|
32
32
|
</div>
|
|
33
33
|
|
|
@@ -45,10 +45,34 @@ export function WebSearchSection({ appSettings, patchSettings, inputClass }: Set
|
|
|
45
45
|
</div>
|
|
46
46
|
)}
|
|
47
47
|
|
|
48
|
-
{
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
{provider === 'tavily' && (
|
|
49
|
+
<div>
|
|
50
|
+
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Tavily API Key</label>
|
|
51
|
+
<input
|
|
52
|
+
type="password"
|
|
53
|
+
value={appSettings.tavilyApiKey || ''}
|
|
54
|
+
onChange={(e) => patchSettings({ tavilyApiKey: e.target.value || null })}
|
|
55
|
+
placeholder="tvly-..."
|
|
56
|
+
className={inputClass}
|
|
57
|
+
style={{ fontFamily: 'inherit' }}
|
|
58
|
+
/>
|
|
59
|
+
<p className="text-[11px] text-text-3/60 mt-2">Get your API key from <a href="https://tavily.com" target="_blank" rel="noopener noreferrer" className="text-accent-bright hover:underline">tavily.com</a></p>
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
|
|
63
|
+
{provider === 'brave' && (
|
|
64
|
+
<div>
|
|
65
|
+
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Brave Search API Key</label>
|
|
66
|
+
<input
|
|
67
|
+
type="password"
|
|
68
|
+
value={appSettings.braveApiKey || ''}
|
|
69
|
+
onChange={(e) => patchSettings({ braveApiKey: e.target.value || null })}
|
|
70
|
+
placeholder="BSA..."
|
|
71
|
+
className={inputClass}
|
|
72
|
+
style={{ fontFamily: 'inherit' }}
|
|
73
|
+
/>
|
|
74
|
+
<p className="text-[11px] text-text-3/60 mt-2">Get your API key from <a href="https://brave.com/search/api/" target="_blank" rel="noopener noreferrer" className="text-accent-bright hover:underline">brave.com/search/api</a></p>
|
|
75
|
+
</div>
|
|
52
76
|
)}
|
|
53
77
|
</div>
|
|
54
78
|
</div>
|
|
@@ -8,6 +8,7 @@ import { ThemeSection } from './section-theme'
|
|
|
8
8
|
import { OrchestratorSection } from './section-orchestrator'
|
|
9
9
|
import { RuntimeLoopSection } from './section-runtime-loop'
|
|
10
10
|
import { CapabilityPolicySection } from './section-capability-policy'
|
|
11
|
+
import { StorageSection } from './section-storage'
|
|
11
12
|
import { VoiceSection } from './section-voice'
|
|
12
13
|
import { WebSearchSection } from './section-web-search'
|
|
13
14
|
import { HeartbeatSection } from './section-heartbeat'
|
|
@@ -28,7 +29,7 @@ const TABS: Tab[] = [
|
|
|
28
29
|
{
|
|
29
30
|
id: 'general',
|
|
30
31
|
label: 'General',
|
|
31
|
-
keywords: ['preferences', 'user', 'language', 'default', 'capability', 'policy', 'permissions', 'tools'],
|
|
32
|
+
keywords: ['preferences', 'user', 'language', 'default', 'capability', 'policy', 'permissions', 'tools', 'storage', 'uploads', 'disk', 'files', 'cleanup'],
|
|
32
33
|
icon: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><circle cx="12" cy="12" r="3" /><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" /></svg>,
|
|
33
34
|
},
|
|
34
35
|
{
|
|
@@ -180,6 +181,7 @@ export function SettingsPage() {
|
|
|
180
181
|
<>
|
|
181
182
|
<UserPreferencesSection {...sectionProps} />
|
|
182
183
|
<CapabilityPolicySection {...sectionProps} />
|
|
184
|
+
<StorageSection {...sectionProps} />
|
|
183
185
|
</>
|
|
184
186
|
)}
|
|
185
187
|
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo } from 'react'
|
|
4
|
+
import { ConfirmDialog } from '@/components/shared/confirm-dialog'
|
|
5
|
+
|
|
6
|
+
interface UploadFile {
|
|
7
|
+
name: string
|
|
8
|
+
size: number
|
|
9
|
+
modified: number
|
|
10
|
+
category: string
|
|
11
|
+
url: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type SortField = 'modified' | 'size' | 'name'
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
files: UploadFile[]
|
|
18
|
+
onDelete: (filenames: string[]) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function formatBytes(bytes: number): string {
|
|
22
|
+
if (bytes === 0) return '0 B'
|
|
23
|
+
const units = ['B', 'KB', 'MB', 'GB']
|
|
24
|
+
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1)
|
|
25
|
+
const value = bytes / Math.pow(1024, i)
|
|
26
|
+
return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[i]}`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function formatDate(ms: number): string {
|
|
30
|
+
const d = new Date(ms)
|
|
31
|
+
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const CATEGORY_ICONS: Record<string, string> = {
|
|
35
|
+
image: '\u{1F5BC}',
|
|
36
|
+
video: '\u{1F3AC}',
|
|
37
|
+
audio: '\u{1F3B5}',
|
|
38
|
+
document: '\u{1F4C4}',
|
|
39
|
+
archive: '\u{1F4E6}',
|
|
40
|
+
other: '\u{1F4CE}',
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const CATEGORY_LABELS: Record<string, string> = {
|
|
44
|
+
image: 'Images',
|
|
45
|
+
video: 'Videos',
|
|
46
|
+
audio: 'Audio',
|
|
47
|
+
document: 'Docs',
|
|
48
|
+
archive: 'Archives',
|
|
49
|
+
other: 'Other',
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function StorageBrowser({ files, onDelete }: Props) {
|
|
53
|
+
const [selected, setSelected] = useState<Set<string>>(new Set())
|
|
54
|
+
const [sortBy, setSortBy] = useState<SortField>('modified')
|
|
55
|
+
const [filterCategory, setFilterCategory] = useState<string | null>(null)
|
|
56
|
+
const [confirmDelete, setConfirmDelete] = useState<string[] | null>(null)
|
|
57
|
+
|
|
58
|
+
const categories = useMemo(() => {
|
|
59
|
+
const cats = new Set<string>()
|
|
60
|
+
for (const f of files) cats.add(f.category)
|
|
61
|
+
return Array.from(cats).sort()
|
|
62
|
+
}, [files])
|
|
63
|
+
|
|
64
|
+
const filtered = useMemo(() => {
|
|
65
|
+
let list = filterCategory ? files.filter((f) => f.category === filterCategory) : files
|
|
66
|
+
list = [...list].sort((a, b) => {
|
|
67
|
+
if (sortBy === 'modified') return b.modified - a.modified
|
|
68
|
+
if (sortBy === 'size') return b.size - a.size
|
|
69
|
+
return a.name.localeCompare(b.name)
|
|
70
|
+
})
|
|
71
|
+
return list
|
|
72
|
+
}, [files, filterCategory, sortBy])
|
|
73
|
+
|
|
74
|
+
const totalSize = useMemo(() => files.reduce((s, f) => s + f.size, 0), [files])
|
|
75
|
+
|
|
76
|
+
const toggleSelect = (name: string) => {
|
|
77
|
+
setSelected((prev) => {
|
|
78
|
+
const next = new Set(prev)
|
|
79
|
+
if (next.has(name)) next.delete(name)
|
|
80
|
+
else next.add(name)
|
|
81
|
+
return next
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const toggleSelectAll = () => {
|
|
86
|
+
if (selected.size === filtered.length) {
|
|
87
|
+
setSelected(new Set())
|
|
88
|
+
} else {
|
|
89
|
+
setSelected(new Set(filtered.map((f) => f.name)))
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const handleDeleteSelected = () => {
|
|
94
|
+
const names = Array.from(selected)
|
|
95
|
+
if (names.length > 0) setConfirmDelete(names)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const executeDelete = () => {
|
|
99
|
+
if (confirmDelete) {
|
|
100
|
+
onDelete(confirmDelete)
|
|
101
|
+
setSelected((prev) => {
|
|
102
|
+
const next = new Set(prev)
|
|
103
|
+
for (const name of confirmDelete) next.delete(name)
|
|
104
|
+
return next
|
|
105
|
+
})
|
|
106
|
+
setConfirmDelete(null)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div>
|
|
112
|
+
{/* Header */}
|
|
113
|
+
<div className="flex items-center justify-between mb-5">
|
|
114
|
+
<div>
|
|
115
|
+
<h3 className="font-display text-[18px] font-700 tracking-[-0.02em] text-text">File Browser</h3>
|
|
116
|
+
<p className="text-[12px] text-text-3 mt-0.5">
|
|
117
|
+
{files.length} file{files.length !== 1 ? 's' : ''} · {formatBytes(totalSize)}
|
|
118
|
+
</p>
|
|
119
|
+
</div>
|
|
120
|
+
<select
|
|
121
|
+
value={sortBy}
|
|
122
|
+
onChange={(e) => setSortBy(e.target.value as SortField)}
|
|
123
|
+
className="px-3 py-1.5 rounded-[10px] border border-white/[0.08] bg-bg text-text text-[12px] outline-none cursor-pointer"
|
|
124
|
+
style={{ fontFamily: 'inherit' }}
|
|
125
|
+
>
|
|
126
|
+
<option value="modified">Newest first</option>
|
|
127
|
+
<option value="size">Largest first</option>
|
|
128
|
+
<option value="name">Name A-Z</option>
|
|
129
|
+
</select>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
{/* Category filters */}
|
|
133
|
+
<div className="flex gap-1.5 mb-4 flex-wrap">
|
|
134
|
+
<button
|
|
135
|
+
onClick={() => setFilterCategory(null)}
|
|
136
|
+
className={`px-3 py-1 rounded-full text-[11px] font-600 cursor-pointer transition-all border
|
|
137
|
+
${!filterCategory
|
|
138
|
+
? 'bg-accent-soft border-accent-bright/30 text-accent-bright'
|
|
139
|
+
: 'bg-transparent border-white/[0.06] text-text-3 hover:bg-white/[0.04]'}`}
|
|
140
|
+
style={{ fontFamily: 'inherit' }}
|
|
141
|
+
>
|
|
142
|
+
All
|
|
143
|
+
</button>
|
|
144
|
+
{categories.map((cat) => (
|
|
145
|
+
<button
|
|
146
|
+
key={cat}
|
|
147
|
+
onClick={() => setFilterCategory(filterCategory === cat ? null : cat)}
|
|
148
|
+
className={`px-3 py-1 rounded-full text-[11px] font-600 cursor-pointer transition-all border
|
|
149
|
+
${filterCategory === cat
|
|
150
|
+
? 'bg-accent-soft border-accent-bright/30 text-accent-bright'
|
|
151
|
+
: 'bg-transparent border-white/[0.06] text-text-3 hover:bg-white/[0.04]'}`}
|
|
152
|
+
style={{ fontFamily: 'inherit' }}
|
|
153
|
+
>
|
|
154
|
+
{CATEGORY_ICONS[cat] || ''} {CATEGORY_LABELS[cat] || cat}
|
|
155
|
+
</button>
|
|
156
|
+
))}
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
{/* Select all */}
|
|
160
|
+
{filtered.length > 0 && (
|
|
161
|
+
<div className="flex items-center gap-2 mb-3">
|
|
162
|
+
<button
|
|
163
|
+
onClick={toggleSelectAll}
|
|
164
|
+
className="text-[11px] text-accent-bright hover:underline cursor-pointer bg-transparent border-none"
|
|
165
|
+
style={{ fontFamily: 'inherit' }}
|
|
166
|
+
>
|
|
167
|
+
{selected.size === filtered.length ? 'Deselect all' : 'Select all'}
|
|
168
|
+
</button>
|
|
169
|
+
{selected.size > 0 && (
|
|
170
|
+
<span className="text-[11px] text-text-3">
|
|
171
|
+
{selected.size} selected
|
|
172
|
+
</span>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
176
|
+
|
|
177
|
+
{/* File grid */}
|
|
178
|
+
{filtered.length === 0 ? (
|
|
179
|
+
<div className="py-12 text-center text-[13px] text-text-3/60">
|
|
180
|
+
{files.length === 0 ? 'No uploaded files.' : 'No files match this filter.'}
|
|
181
|
+
</div>
|
|
182
|
+
) : (
|
|
183
|
+
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2 max-h-[400px] overflow-y-auto pr-1">
|
|
184
|
+
{filtered.map((file) => (
|
|
185
|
+
<div
|
|
186
|
+
key={file.name}
|
|
187
|
+
onClick={() => toggleSelect(file.name)}
|
|
188
|
+
className={`relative p-3 rounded-[14px] border cursor-pointer transition-all
|
|
189
|
+
${selected.has(file.name)
|
|
190
|
+
? 'border-accent-bright/40 bg-accent-soft/30'
|
|
191
|
+
: 'border-white/[0.06] bg-surface hover:border-white/[0.12]'}`}
|
|
192
|
+
>
|
|
193
|
+
{/* Checkbox */}
|
|
194
|
+
<div className={`absolute top-2 right-2 w-4 h-4 rounded-[5px] border transition-all flex items-center justify-center
|
|
195
|
+
${selected.has(file.name)
|
|
196
|
+
? 'border-accent-bright bg-accent-bright'
|
|
197
|
+
: 'border-white/[0.15] bg-transparent'}`}
|
|
198
|
+
>
|
|
199
|
+
{selected.has(file.name) && (
|
|
200
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
|
|
201
|
+
<polyline points="20 6 9 17 4 12" />
|
|
202
|
+
</svg>
|
|
203
|
+
)}
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
{/* Thumbnail / icon */}
|
|
207
|
+
<div className="w-full aspect-square rounded-[10px] bg-white/[0.03] mb-2 flex items-center justify-center overflow-hidden">
|
|
208
|
+
{file.category === 'image' ? (
|
|
209
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
210
|
+
<img
|
|
211
|
+
src={file.url}
|
|
212
|
+
alt={file.name}
|
|
213
|
+
className="w-full h-full object-cover rounded-[10px]"
|
|
214
|
+
loading="lazy"
|
|
215
|
+
/>
|
|
216
|
+
) : (
|
|
217
|
+
<span className="text-[28px]">{CATEGORY_ICONS[file.category] || CATEGORY_ICONS.other}</span>
|
|
218
|
+
)}
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
{/* Meta */}
|
|
222
|
+
<p className="text-[11px] font-600 text-text truncate" title={file.name}>{file.name}</p>
|
|
223
|
+
<p className="text-[10px] text-text-3/60 mt-0.5">
|
|
224
|
+
{formatBytes(file.size)} · {formatDate(file.modified)}
|
|
225
|
+
</p>
|
|
226
|
+
</div>
|
|
227
|
+
))}
|
|
228
|
+
</div>
|
|
229
|
+
)}
|
|
230
|
+
|
|
231
|
+
{/* Bulk delete footer */}
|
|
232
|
+
{selected.size > 0 && (
|
|
233
|
+
<div className="mt-4 pt-4 border-t border-white/[0.06] flex items-center justify-between">
|
|
234
|
+
<span className="text-[12px] text-text-3">
|
|
235
|
+
{selected.size} file{selected.size !== 1 ? 's' : ''} selected
|
|
236
|
+
</span>
|
|
237
|
+
<button
|
|
238
|
+
onClick={handleDeleteSelected}
|
|
239
|
+
className="px-4 py-2 rounded-[10px] bg-danger text-white text-[12px] font-600 cursor-pointer
|
|
240
|
+
hover:brightness-110 active:scale-[0.97] transition-all border-none"
|
|
241
|
+
style={{ fontFamily: 'inherit' }}
|
|
242
|
+
>
|
|
243
|
+
Delete Selected
|
|
244
|
+
</button>
|
|
245
|
+
</div>
|
|
246
|
+
)}
|
|
247
|
+
|
|
248
|
+
<ConfirmDialog
|
|
249
|
+
open={!!confirmDelete}
|
|
250
|
+
title="Delete Files"
|
|
251
|
+
message={`Permanently delete ${confirmDelete?.length ?? 0} file${(confirmDelete?.length ?? 0) !== 1 ? 's' : ''}? This cannot be undone.`}
|
|
252
|
+
confirmLabel="Delete"
|
|
253
|
+
danger
|
|
254
|
+
onConfirm={executeDelete}
|
|
255
|
+
onCancel={() => setConfirmDelete(null)}
|
|
256
|
+
/>
|
|
257
|
+
</div>
|
|
258
|
+
)
|
|
259
|
+
}
|
|
@@ -34,6 +34,14 @@ export function TaskCard({ task, selectionMode, selected, onToggleSelect }: Task
|
|
|
34
34
|
const agent = agents[task.agentId]
|
|
35
35
|
const project = task.projectId ? projects[task.projectId] : null
|
|
36
36
|
|
|
37
|
+
const priorityConfig = {
|
|
38
|
+
critical: { label: 'Critical', cls: 'bg-red-500/10 text-red-400' },
|
|
39
|
+
high: { label: 'High', cls: 'bg-orange-500/10 text-orange-400' },
|
|
40
|
+
medium: { label: 'Med', cls: 'bg-amber-500/10 text-amber-400' },
|
|
41
|
+
low: { label: 'Low', cls: 'bg-sky-500/10 text-sky-400' },
|
|
42
|
+
} as const
|
|
43
|
+
const prio = task.priority && priorityConfig[task.priority]
|
|
44
|
+
|
|
37
45
|
const isBlocked = Array.isArray(task.blockedBy) && task.blockedBy.length > 0
|
|
38
46
|
const isOverdue = task.dueAt && task.dueAt < Date.now() && task.status !== 'completed' && task.status !== 'archived'
|
|
39
47
|
const borderColor = isBlocked ? 'border-l-rose-500'
|
|
@@ -86,7 +94,7 @@ export function TaskCard({ task, selectionMode, selected, onToggleSelect }: Task
|
|
|
86
94
|
setTaskSheetOpen(true)
|
|
87
95
|
}
|
|
88
96
|
}}
|
|
89
|
-
className={`
|
|
97
|
+
className={`py-3 px-4 rounded-[14px] border border-l-[3px] ${borderColor} bg-surface hover:bg-surface-2 transition-all group
|
|
90
98
|
${selectionMode ? 'cursor-pointer' : 'cursor-grab active:cursor-grabbing'}
|
|
91
99
|
${dragging ? 'opacity-40 scale-[0.97]' : ''}
|
|
92
100
|
${selected ? 'border-accent-bright/40 bg-accent-bright/[0.04] ring-1 ring-accent-bright/20' : 'border-white/[0.06]'}`}
|
|
@@ -114,6 +122,11 @@ export function TaskCard({ task, selectionMode, selected, onToggleSelect }: Task
|
|
|
114
122
|
</svg>
|
|
115
123
|
)}
|
|
116
124
|
<h4 className="flex-1 text-[14px] font-600 text-text leading-[1.4] line-clamp-2">{task.title}</h4>
|
|
125
|
+
{prio && (
|
|
126
|
+
<span className={`px-1.5 py-0.5 rounded-[5px] text-[10px] font-600 shrink-0 ${prio.cls}`}>
|
|
127
|
+
{prio.label}
|
|
128
|
+
</span>
|
|
129
|
+
)}
|
|
117
130
|
{isBlocked && (
|
|
118
131
|
<span className="px-1.5 py-0.5 rounded-[5px] bg-rose-500/10 text-rose-400 text-[10px] font-600 shrink-0">
|
|
119
132
|
{task.blockedBy?.length}
|