@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
|
@@ -76,10 +76,57 @@ export function ConnectorPlatformIcon({
|
|
|
76
76
|
return <BsMicrosoftTeams size={size} className={className} />
|
|
77
77
|
case 'openclaw':
|
|
78
78
|
return (
|
|
79
|
-
<svg width={size} height={size} viewBox="0 0
|
|
80
|
-
|
|
81
|
-
<
|
|
82
|
-
|
|
79
|
+
<svg width={size} height={size} viewBox="0 0 16 16" aria-hidden className={className}>
|
|
80
|
+
{/* OpenClaw pixel lobster mark */}
|
|
81
|
+
<g fill="#3a0a0d">
|
|
82
|
+
<rect x="1" y="5" width="1" height="3" />
|
|
83
|
+
<rect x="2" y="4" width="1" height="1" />
|
|
84
|
+
<rect x="2" y="8" width="1" height="1" />
|
|
85
|
+
<rect x="3" y="3" width="1" height="1" />
|
|
86
|
+
<rect x="3" y="9" width="1" height="1" />
|
|
87
|
+
<rect x="4" y="2" width="1" height="1" />
|
|
88
|
+
<rect x="4" y="10" width="1" height="1" />
|
|
89
|
+
<rect x="5" y="2" width="6" height="1" />
|
|
90
|
+
<rect x="11" y="2" width="1" height="1" />
|
|
91
|
+
<rect x="12" y="3" width="1" height="1" />
|
|
92
|
+
<rect x="12" y="9" width="1" height="1" />
|
|
93
|
+
<rect x="13" y="4" width="1" height="1" />
|
|
94
|
+
<rect x="13" y="8" width="1" height="1" />
|
|
95
|
+
<rect x="14" y="5" width="1" height="3" />
|
|
96
|
+
<rect x="5" y="11" width="6" height="1" />
|
|
97
|
+
<rect x="4" y="12" width="1" height="1" />
|
|
98
|
+
<rect x="11" y="12" width="1" height="1" />
|
|
99
|
+
<rect x="3" y="13" width="1" height="1" />
|
|
100
|
+
<rect x="12" y="13" width="1" height="1" />
|
|
101
|
+
<rect x="5" y="14" width="6" height="1" />
|
|
102
|
+
</g>
|
|
103
|
+
<g fill="#ff4f40">
|
|
104
|
+
<rect x="5" y="3" width="6" height="1" />
|
|
105
|
+
<rect x="4" y="4" width="8" height="1" />
|
|
106
|
+
<rect x="3" y="5" width="10" height="1" />
|
|
107
|
+
<rect x="3" y="6" width="10" height="1" />
|
|
108
|
+
<rect x="3" y="7" width="10" height="1" />
|
|
109
|
+
<rect x="4" y="8" width="8" height="1" />
|
|
110
|
+
<rect x="5" y="9" width="6" height="1" />
|
|
111
|
+
<rect x="5" y="12" width="6" height="1" />
|
|
112
|
+
<rect x="6" y="13" width="4" height="1" />
|
|
113
|
+
</g>
|
|
114
|
+
<g fill="#ff775f">
|
|
115
|
+
<rect x="1" y="6" width="2" height="1" />
|
|
116
|
+
<rect x="2" y="5" width="1" height="1" />
|
|
117
|
+
<rect x="2" y="7" width="1" height="1" />
|
|
118
|
+
<rect x="13" y="6" width="2" height="1" />
|
|
119
|
+
<rect x="13" y="5" width="1" height="1" />
|
|
120
|
+
<rect x="13" y="7" width="1" height="1" />
|
|
121
|
+
</g>
|
|
122
|
+
<g fill="#081016">
|
|
123
|
+
<rect x="6" y="5" width="1" height="1" />
|
|
124
|
+
<rect x="9" y="5" width="1" height="1" />
|
|
125
|
+
</g>
|
|
126
|
+
<g fill="#f5fbff">
|
|
127
|
+
<rect x="6" y="4" width="1" height="1" />
|
|
128
|
+
<rect x="9" y="4" width="1" height="1" />
|
|
129
|
+
</g>
|
|
83
130
|
</svg>
|
|
84
131
|
)
|
|
85
132
|
default:
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import type { ButtonHTMLAttributes, ReactNode } from 'react'
|
|
4
|
+
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
|
|
4
5
|
|
|
5
6
|
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
6
7
|
children: ReactNode
|
|
7
8
|
variant?: 'default' | 'accent' | 'danger'
|
|
8
9
|
active?: boolean
|
|
9
10
|
size?: 'sm' | 'md'
|
|
11
|
+
tooltip?: string
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
export function IconButton({ children, variant = 'default', active, size = 'md', className = '', ...props }: Props) {
|
|
14
|
+
export function IconButton({ children, variant = 'default', active, size = 'md', className = '', tooltip, ...props }: Props) {
|
|
13
15
|
const sizeClass = size === 'sm' ? 'w-8 h-8 rounded-[9px]' : 'w-9 h-9 rounded-[10px]'
|
|
14
16
|
const base = `${sizeClass} border-none bg-transparent flex items-center justify-center cursor-pointer shrink-0 transition-all duration-200 hover:bg-white/[0.06] active:scale-90`
|
|
15
17
|
const color =
|
|
@@ -17,9 +19,21 @@ export function IconButton({ children, variant = 'default', active, size = 'md',
|
|
|
17
19
|
variant === 'danger' ? 'text-danger' :
|
|
18
20
|
active ? 'text-accent-bright bg-accent-soft' : 'text-text-3 hover:text-text-2'
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
const btn = (
|
|
21
23
|
<button className={`${base} ${color} ${className}`} {...props}>
|
|
22
24
|
{children}
|
|
23
25
|
</button>
|
|
24
26
|
)
|
|
27
|
+
|
|
28
|
+
if (!tooltip) return btn
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Tooltip>
|
|
32
|
+
<TooltipTrigger asChild>{btn}</TooltipTrigger>
|
|
33
|
+
<TooltipContent side="bottom" sideOffset={6}
|
|
34
|
+
className="bg-raised border border-white/[0.08] text-text shadow-[0_8px_32px_rgba(0,0,0,0.5)] rounded-[8px] px-2.5 py-1.5 text-[11px]">
|
|
35
|
+
{tooltip}
|
|
36
|
+
</TooltipContent>
|
|
37
|
+
</Tooltip>
|
|
38
|
+
)
|
|
25
39
|
}
|
|
@@ -77,7 +77,7 @@ export function KeyboardShortcutsDialog() {
|
|
|
77
77
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
78
78
|
<DialogContent
|
|
79
79
|
showCloseButton={false}
|
|
80
|
-
className="sm:max-w-[420px] p-0 bg-
|
|
80
|
+
className="sm:max-w-[420px] p-0 bg-surface/95 backdrop-blur-xl border-white/[0.08] shadow-[0_24px_80px_rgba(0,0,0,0.6)] rounded-[16px] overflow-hidden gap-0"
|
|
81
81
|
>
|
|
82
82
|
<DialogTitle className="sr-only">Keyboard shortcuts</DialogTitle>
|
|
83
83
|
<div className="flex items-center justify-between px-5 py-3.5 border-b border-white/[0.06]">
|
|
@@ -96,12 +96,12 @@ export function SearchDialog() {
|
|
|
96
96
|
|
|
97
97
|
// Reset on open
|
|
98
98
|
useEffect(() => {
|
|
99
|
-
if (open)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
99
|
+
if (!open) return
|
|
100
|
+
setQuery('')
|
|
101
|
+
setResults([])
|
|
102
|
+
setSelectedIdx(0)
|
|
103
|
+
const timer = setTimeout(() => inputRef.current?.focus(), 50)
|
|
104
|
+
return () => clearTimeout(timer)
|
|
105
105
|
}, [open])
|
|
106
106
|
|
|
107
107
|
// Debounced search
|
|
@@ -152,6 +152,12 @@ export function SearchDialog() {
|
|
|
152
152
|
case 'message':
|
|
153
153
|
setCurrentSession(result.id)
|
|
154
154
|
setActiveView('agents')
|
|
155
|
+
// Scroll to the matched message after the chat renders
|
|
156
|
+
if (result.messageIndex != null) {
|
|
157
|
+
setTimeout(() => {
|
|
158
|
+
window.dispatchEvent(new CustomEvent('swarmclaw:scroll-to-message', { detail: { index: result.messageIndex } }))
|
|
159
|
+
}, 300)
|
|
160
|
+
}
|
|
155
161
|
break
|
|
156
162
|
case 'schedule':
|
|
157
163
|
setEditingScheduleId(result.id)
|
|
@@ -194,7 +200,7 @@ export function SearchDialog() {
|
|
|
194
200
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
195
201
|
<DialogContent
|
|
196
202
|
showCloseButton={false}
|
|
197
|
-
className="sm:max-w-[520px] p-0 bg-
|
|
203
|
+
className="sm:max-w-[520px] p-0 bg-surface/95 backdrop-blur-xl border-white/[0.08] shadow-[0_24px_80px_rgba(0,0,0,0.6)] rounded-[16px] overflow-hidden gap-0"
|
|
198
204
|
onKeyDown={handleKeyDown}
|
|
199
205
|
>
|
|
200
206
|
<DialogTitle className="sr-only">Search</DialogTitle>
|
|
@@ -208,6 +214,7 @@ export function SearchDialog() {
|
|
|
208
214
|
value={query}
|
|
209
215
|
onChange={(e) => handleQueryChange(e.target.value)}
|
|
210
216
|
placeholder="Search agents, tasks, schedules..."
|
|
217
|
+
aria-label="Search"
|
|
211
218
|
className="flex-1 bg-transparent border-none outline-none text-[14px] text-text placeholder:text-text-3/60 font-[inherit]"
|
|
212
219
|
autoFocus
|
|
213
220
|
/>
|
|
@@ -235,10 +242,10 @@ export function SearchDialog() {
|
|
|
235
242
|
)}
|
|
236
243
|
{results.map((result, idx) => (
|
|
237
244
|
<button
|
|
238
|
-
key={`${result.type}-${result.id}`}
|
|
245
|
+
key={result.type === 'message' ? `${result.type}-${result.id}-${result.messageIndex}` : `${result.type}-${result.id}`}
|
|
239
246
|
onClick={() => navigateTo(result)}
|
|
240
247
|
onMouseEnter={() => setSelectedIdx(idx)}
|
|
241
|
-
className={`w-full flex items-center gap-3 px-4 py-2.5 text-left cursor-pointer transition-colors border-none bg-transparent
|
|
248
|
+
className={`w-full flex items-center gap-3 px-4 py-2.5 text-left cursor-pointer transition-colors border-none bg-transparent focus-visible:ring-1 focus-visible:ring-accent-bright/50 focus-visible:ring-inset
|
|
242
249
|
${idx === selectedIdx ? 'bg-white/[0.06]' : 'hover:bg-white/[0.04]'}`}
|
|
243
250
|
style={{ fontFamily: 'inherit' }}
|
|
244
251
|
>
|
|
@@ -246,7 +253,7 @@ export function SearchDialog() {
|
|
|
246
253
|
<div className={`w-8 h-8 rounded-[8px] flex items-center justify-center shrink-0
|
|
247
254
|
${idx === selectedIdx ? 'bg-accent-bright/20' : 'bg-white/[0.04]'}`}>
|
|
248
255
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"
|
|
249
|
-
className={idx === selectedIdx ? 'text-
|
|
256
|
+
className={idx === selectedIdx ? 'text-accent-bright' : 'text-text-3'}>
|
|
250
257
|
<path d={TYPE_ICONS[result.type]} />
|
|
251
258
|
{TYPE_EXTRA_PATHS[result.type] && <path d={TYPE_EXTRA_PATHS[result.type]} />}
|
|
252
259
|
</svg>
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { api } from '@/lib/api-client'
|
|
5
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
6
|
+
import toast from 'react-hot-toast'
|
|
3
7
|
import type { SettingsSectionProps } from './types'
|
|
4
8
|
|
|
5
9
|
interface EmbeddingSectionProps extends SettingsSectionProps {
|
|
@@ -7,6 +11,12 @@ interface EmbeddingSectionProps extends SettingsSectionProps {
|
|
|
7
11
|
}
|
|
8
12
|
|
|
9
13
|
export function EmbeddingSection({ appSettings, patchSettings, inputClass, credList }: EmbeddingSectionProps) {
|
|
14
|
+
const loadCredentials = useAppStore((s) => s.loadCredentials)
|
|
15
|
+
const [addingKey, setAddingKey] = useState(false)
|
|
16
|
+
const [newKeyName, setNewKeyName] = useState('')
|
|
17
|
+
const [newKeyValue, setNewKeyValue] = useState('')
|
|
18
|
+
const [savingKey, setSavingKey] = useState(false)
|
|
19
|
+
|
|
10
20
|
return (
|
|
11
21
|
<div className="mb-10">
|
|
12
22
|
<h3 className="font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
|
|
@@ -60,20 +70,45 @@ export function EmbeddingSection({ appSettings, patchSettings, inputClass, credL
|
|
|
60
70
|
</div>
|
|
61
71
|
<div>
|
|
62
72
|
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-3">API Key</label>
|
|
63
|
-
{credList.filter((c) => c.provider === 'openai').length > 0 ? (
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
{credList.filter((c) => c.provider === 'openai').length > 0 && !addingKey ? (
|
|
74
|
+
<div className="flex gap-2 items-center">
|
|
75
|
+
<select
|
|
76
|
+
value={appSettings.embeddingCredentialId || ''}
|
|
77
|
+
onChange={(e) => patchSettings({ embeddingCredentialId: e.target.value || null })}
|
|
78
|
+
className={`${inputClass} appearance-none cursor-pointer flex-1`}
|
|
79
|
+
style={{ fontFamily: 'inherit' }}
|
|
80
|
+
>
|
|
81
|
+
<option value="">Select a key...</option>
|
|
82
|
+
{credList.filter((c) => c.provider === 'openai').map((c) => (
|
|
83
|
+
<option key={c.id} value={c.id}>{c.name}</option>
|
|
84
|
+
))}
|
|
85
|
+
</select>
|
|
86
|
+
<button type="button" onClick={() => setAddingKey(true)} className="text-accent-bright text-[11px] font-600 cursor-pointer bg-transparent border-none hover:brightness-110 transition-all" style={{ fontFamily: 'inherit' }}>+ New</button>
|
|
87
|
+
</div>
|
|
75
88
|
) : (
|
|
76
|
-
<
|
|
89
|
+
<div className="space-y-2">
|
|
90
|
+
<input type="text" value={newKeyName} onChange={e => setNewKeyName(e.target.value)} placeholder="Key name (optional)" className={inputClass} style={{ fontFamily: 'inherit' }} />
|
|
91
|
+
<input type="password" value={newKeyValue} onChange={e => setNewKeyValue(e.target.value)} placeholder="sk-..." className={inputClass} style={{ fontFamily: 'inherit' }} />
|
|
92
|
+
<div className="flex gap-2">
|
|
93
|
+
<button type="button" disabled={savingKey || !newKeyValue.trim()} onClick={async () => {
|
|
94
|
+
setSavingKey(true)
|
|
95
|
+
try {
|
|
96
|
+
const cred = await api<{ id: string }>('POST', '/credentials', { provider: 'openai', name: newKeyName.trim() || 'OpenAI key', apiKey: newKeyValue.trim() })
|
|
97
|
+
await loadCredentials()
|
|
98
|
+
patchSettings({ embeddingCredentialId: cred.id })
|
|
99
|
+
setAddingKey(false)
|
|
100
|
+
setNewKeyName('')
|
|
101
|
+
setNewKeyValue('')
|
|
102
|
+
} catch (err: unknown) { toast.error(`Failed to save: ${err instanceof Error ? err.message : String(err)}`) }
|
|
103
|
+
finally { setSavingKey(false) }
|
|
104
|
+
}} className="px-4 py-1.5 rounded-[8px] bg-accent-bright text-white text-[12px] font-600 cursor-pointer border-none hover:brightness-110 transition-all disabled:opacity-40" style={{ fontFamily: 'inherit' }}>
|
|
105
|
+
{savingKey ? 'Saving...' : 'Save Key'}
|
|
106
|
+
</button>
|
|
107
|
+
{credList.filter(c => c.provider === 'openai').length > 0 && (
|
|
108
|
+
<button type="button" onClick={() => { setAddingKey(false); setNewKeyName(''); setNewKeyValue('') }} className="px-4 py-1.5 rounded-[8px] bg-surface-2 text-text-2 text-[12px] font-600 cursor-pointer border-none hover:bg-surface-3 transition-all" style={{ fontFamily: 'inherit' }}>Cancel</button>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
77
112
|
)}
|
|
78
113
|
</div>
|
|
79
114
|
</>
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import { useState } from 'react'
|
|
3
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
import { api } from '@/lib/api-client'
|
|
6
|
+
import toast from 'react-hot-toast'
|
|
4
7
|
import { ModelCombobox } from '@/components/shared/model-combobox'
|
|
5
8
|
import { NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
|
|
6
9
|
import type { SettingsSectionProps } from './types'
|
|
@@ -8,7 +11,12 @@ import type { SettingsSectionProps } from './types'
|
|
|
8
11
|
export function OrchestratorSection({ appSettings, patchSettings, inputClass }: SettingsSectionProps) {
|
|
9
12
|
const providers = useAppStore((s) => s.providers)
|
|
10
13
|
const credentials = useAppStore((s) => s.credentials)
|
|
14
|
+
const loadCredentials = useAppStore((s) => s.loadCredentials)
|
|
11
15
|
const credList = Object.values(credentials)
|
|
16
|
+
const [addingKey, setAddingKey] = useState(false)
|
|
17
|
+
const [newKeyName, setNewKeyName] = useState('')
|
|
18
|
+
const [newKeyValue, setNewKeyValue] = useState('')
|
|
19
|
+
const [savingKey, setSavingKey] = useState(false)
|
|
12
20
|
|
|
13
21
|
const lgProviders = providers.filter((p) => !NON_LANGGRAPH_PROVIDER_IDS.has(String(p.id)))
|
|
14
22
|
const hasConfiguredLgProvider = !!appSettings.langGraphProvider && lgProviders.some((p) => p.id === appSettings.langGraphProvider)
|
|
@@ -82,22 +90,45 @@ export function OrchestratorSection({ appSettings, patchSettings, inputClass }:
|
|
|
82
90
|
{/* API Key picker */}
|
|
83
91
|
<div>
|
|
84
92
|
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-3">API Key</label>
|
|
85
|
-
{lgCredentials.length > 0 ? (
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
{lgCredentials.length > 0 && !addingKey ? (
|
|
94
|
+
<div className="flex gap-2 items-center">
|
|
95
|
+
<select
|
|
96
|
+
value={appSettings.langGraphCredentialId || ''}
|
|
97
|
+
onChange={(e) => patchSettings({ langGraphCredentialId: e.target.value || null })}
|
|
98
|
+
className={`${inputClass} appearance-none cursor-pointer flex-1`}
|
|
99
|
+
style={{ fontFamily: 'inherit' }}
|
|
100
|
+
>
|
|
101
|
+
<option value="">Select a key...</option>
|
|
102
|
+
{lgCredentials.map((c) => (
|
|
103
|
+
<option key={c.id} value={c.id}>{c.name}</option>
|
|
104
|
+
))}
|
|
105
|
+
</select>
|
|
106
|
+
<button type="button" onClick={() => setAddingKey(true)} className="text-accent-bright text-[11px] font-600 cursor-pointer bg-transparent border-none hover:brightness-110 transition-all" style={{ fontFamily: 'inherit' }}>+ New</button>
|
|
107
|
+
</div>
|
|
97
108
|
) : (
|
|
98
|
-
<
|
|
99
|
-
|
|
100
|
-
|
|
109
|
+
<div className="space-y-2">
|
|
110
|
+
<input type="text" value={newKeyName} onChange={e => setNewKeyName(e.target.value)} placeholder="Key name (optional)" className={inputClass} style={{ fontFamily: 'inherit' }} />
|
|
111
|
+
<input type="password" value={newKeyValue} onChange={e => setNewKeyValue(e.target.value)} placeholder="sk-..." className={inputClass} style={{ fontFamily: 'inherit' }} />
|
|
112
|
+
<div className="flex gap-2">
|
|
113
|
+
<button type="button" disabled={savingKey || !newKeyValue.trim()} onClick={async () => {
|
|
114
|
+
setSavingKey(true)
|
|
115
|
+
try {
|
|
116
|
+
const cred = await api<{ id: string }>('POST', '/credentials', { provider: lgProvider, name: newKeyName.trim() || `${lgProvider} key`, apiKey: newKeyValue.trim() })
|
|
117
|
+
await loadCredentials()
|
|
118
|
+
patchSettings({ langGraphCredentialId: cred.id })
|
|
119
|
+
setAddingKey(false)
|
|
120
|
+
setNewKeyName('')
|
|
121
|
+
setNewKeyValue('')
|
|
122
|
+
} catch (err: unknown) { toast.error(`Failed to save: ${err instanceof Error ? err.message : String(err)}`) }
|
|
123
|
+
finally { setSavingKey(false) }
|
|
124
|
+
}} className="px-4 py-1.5 rounded-[8px] bg-accent-bright text-white text-[12px] font-600 cursor-pointer border-none hover:brightness-110 transition-all disabled:opacity-40" style={{ fontFamily: 'inherit' }}>
|
|
125
|
+
{savingKey ? 'Saving...' : 'Save Key'}
|
|
126
|
+
</button>
|
|
127
|
+
{lgCredentials.length > 0 && (
|
|
128
|
+
<button type="button" onClick={() => { setAddingKey(false); setNewKeyName(''); setNewKeyValue('') }} className="px-4 py-1.5 rounded-[8px] bg-surface-2 text-text-2 text-[12px] font-600 cursor-pointer border-none hover:bg-surface-3 transition-all" style={{ fontFamily: 'inherit' }}>Cancel</button>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
101
132
|
)}
|
|
102
133
|
</div>
|
|
103
134
|
</div>
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from 'react'
|
|
4
|
+
import { api } from '@/lib/api-client'
|
|
5
|
+
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
6
|
+
import { ConfirmDialog } from '@/components/shared/confirm-dialog'
|
|
7
|
+
import { StorageBrowser } from './storage-browser'
|
|
8
|
+
import type { SettingsSectionProps } from './types'
|
|
9
|
+
|
|
10
|
+
interface UploadFile {
|
|
11
|
+
name: string
|
|
12
|
+
size: number
|
|
13
|
+
modified: number
|
|
14
|
+
category: string
|
|
15
|
+
url: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface UploadsResponse {
|
|
19
|
+
files: UploadFile[]
|
|
20
|
+
totalSize: number
|
|
21
|
+
count: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function formatBytes(bytes: number): string {
|
|
25
|
+
if (bytes === 0) return '0 B'
|
|
26
|
+
const units = ['B', 'KB', 'MB', 'GB']
|
|
27
|
+
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1)
|
|
28
|
+
const value = bytes / Math.pow(1024, i)
|
|
29
|
+
return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[i]}`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function StorageSection(
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
34
|
+
_props: SettingsSectionProps,
|
|
35
|
+
) {
|
|
36
|
+
const [data, setData] = useState<UploadsResponse | null>(null)
|
|
37
|
+
const [loading, setLoading] = useState(true)
|
|
38
|
+
const [browserOpen, setBrowserOpen] = useState(false)
|
|
39
|
+
const [confirmAction, setConfirmAction] = useState<'clearOld' | 'clearAll' | null>(null)
|
|
40
|
+
const [deleting, setDeleting] = useState(false)
|
|
41
|
+
|
|
42
|
+
const fetchFiles = useCallback(async () => {
|
|
43
|
+
try {
|
|
44
|
+
setLoading(true)
|
|
45
|
+
const res = await api<UploadsResponse>('GET', '/uploads')
|
|
46
|
+
setData(res)
|
|
47
|
+
} catch {
|
|
48
|
+
// silent — section just shows empty
|
|
49
|
+
} finally {
|
|
50
|
+
setLoading(false)
|
|
51
|
+
}
|
|
52
|
+
}, [])
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
fetchFiles()
|
|
56
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
57
|
+
}, [])
|
|
58
|
+
|
|
59
|
+
const handleBulkDelete = useCallback(async (filenames: string[]) => {
|
|
60
|
+
try {
|
|
61
|
+
await api('DELETE', '/uploads', { filenames })
|
|
62
|
+
await fetchFiles()
|
|
63
|
+
} catch {
|
|
64
|
+
// silent
|
|
65
|
+
}
|
|
66
|
+
}, [fetchFiles])
|
|
67
|
+
|
|
68
|
+
const handleConfirmAction = useCallback(async () => {
|
|
69
|
+
if (!confirmAction) return
|
|
70
|
+
setDeleting(true)
|
|
71
|
+
try {
|
|
72
|
+
if (confirmAction === 'clearOld') {
|
|
73
|
+
await api('DELETE', '/uploads', { olderThanDays: 30 })
|
|
74
|
+
} else {
|
|
75
|
+
await api('DELETE', '/uploads', { all: true })
|
|
76
|
+
}
|
|
77
|
+
await fetchFiles()
|
|
78
|
+
} catch {
|
|
79
|
+
// silent
|
|
80
|
+
} finally {
|
|
81
|
+
setDeleting(false)
|
|
82
|
+
setConfirmAction(null)
|
|
83
|
+
}
|
|
84
|
+
}, [confirmAction, fetchFiles])
|
|
85
|
+
|
|
86
|
+
// Breakdown by category
|
|
87
|
+
const breakdown = data?.files.reduce<Record<string, { count: number; size: number }>>((acc, f) => {
|
|
88
|
+
if (!acc[f.category]) acc[f.category] = { count: 0, size: 0 }
|
|
89
|
+
acc[f.category].count += 1
|
|
90
|
+
acc[f.category].size += f.size
|
|
91
|
+
return acc
|
|
92
|
+
}, {}) ?? {}
|
|
93
|
+
|
|
94
|
+
const CATEGORY_LABELS: Record<string, string> = {
|
|
95
|
+
image: 'Images',
|
|
96
|
+
video: 'Videos',
|
|
97
|
+
audio: 'Audio',
|
|
98
|
+
document: 'Documents',
|
|
99
|
+
archive: 'Archives',
|
|
100
|
+
other: 'Other',
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div className="mb-10">
|
|
105
|
+
<h3 className="font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
|
|
106
|
+
Storage
|
|
107
|
+
</h3>
|
|
108
|
+
<p className="text-[12px] text-text-3 mb-5">
|
|
109
|
+
Uploaded files from agent tools (screenshots, images, documents). Manage disk usage.
|
|
110
|
+
</p>
|
|
111
|
+
|
|
112
|
+
<div className="p-6 rounded-[18px] bg-surface border border-white/[0.06]">
|
|
113
|
+
{/* Summary */}
|
|
114
|
+
{loading ? (
|
|
115
|
+
<div className="text-[13px] text-text-3/60 animate-pulse">Loading storage info...</div>
|
|
116
|
+
) : (
|
|
117
|
+
<>
|
|
118
|
+
<div className="flex items-baseline gap-3 mb-4">
|
|
119
|
+
<span className="font-display text-[28px] font-700 tracking-[-0.02em] text-text">
|
|
120
|
+
{formatBytes(data?.totalSize ?? 0)}
|
|
121
|
+
</span>
|
|
122
|
+
<span className="text-[13px] text-text-3">
|
|
123
|
+
{data?.count ?? 0} file{data?.count !== 1 ? 's' : ''}
|
|
124
|
+
</span>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
{/* Category breakdown */}
|
|
128
|
+
{Object.keys(breakdown).length > 0 && (
|
|
129
|
+
<div className="flex flex-wrap gap-x-4 gap-y-1 mb-5">
|
|
130
|
+
{Object.entries(breakdown).map(([cat, info]) => (
|
|
131
|
+
<span key={cat} className="text-[11px] text-text-3/70">
|
|
132
|
+
{CATEGORY_LABELS[cat] || cat}: {info.count} ({formatBytes(info.size)})
|
|
133
|
+
</span>
|
|
134
|
+
))}
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
|
|
138
|
+
{/* Actions */}
|
|
139
|
+
<div className="flex flex-wrap gap-2">
|
|
140
|
+
<button
|
|
141
|
+
onClick={() => setBrowserOpen(true)}
|
|
142
|
+
disabled={!data?.count}
|
|
143
|
+
className="px-4 py-2.5 rounded-[12px] bg-accent-soft text-accent-bright text-[12px] font-600 cursor-pointer
|
|
144
|
+
hover:brightness-110 active:scale-[0.97] transition-all border border-accent-bright/20
|
|
145
|
+
disabled:opacity-40 disabled:cursor-not-allowed"
|
|
146
|
+
style={{ fontFamily: 'inherit' }}
|
|
147
|
+
>
|
|
148
|
+
Manage Files
|
|
149
|
+
</button>
|
|
150
|
+
<button
|
|
151
|
+
onClick={() => setConfirmAction('clearOld')}
|
|
152
|
+
disabled={!data?.count}
|
|
153
|
+
className="px-4 py-2.5 rounded-[12px] bg-white/[0.04] text-text-2 text-[12px] font-600 cursor-pointer
|
|
154
|
+
hover:bg-white/[0.06] active:scale-[0.97] transition-all border border-white/[0.06]
|
|
155
|
+
disabled:opacity-40 disabled:cursor-not-allowed"
|
|
156
|
+
style={{ fontFamily: 'inherit' }}
|
|
157
|
+
>
|
|
158
|
+
Clear Old Files
|
|
159
|
+
</button>
|
|
160
|
+
<button
|
|
161
|
+
onClick={() => setConfirmAction('clearAll')}
|
|
162
|
+
disabled={!data?.count}
|
|
163
|
+
className="px-4 py-2.5 rounded-[12px] bg-danger/10 text-danger text-[12px] font-600 cursor-pointer
|
|
164
|
+
hover:bg-danger/20 active:scale-[0.97] transition-all border border-danger/20
|
|
165
|
+
disabled:opacity-40 disabled:cursor-not-allowed"
|
|
166
|
+
style={{ fontFamily: 'inherit' }}
|
|
167
|
+
>
|
|
168
|
+
Clear All
|
|
169
|
+
</button>
|
|
170
|
+
</div>
|
|
171
|
+
</>
|
|
172
|
+
)}
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
{/* File browser sheet */}
|
|
176
|
+
<BottomSheet open={browserOpen} onClose={() => setBrowserOpen(false)} wide>
|
|
177
|
+
{data && (
|
|
178
|
+
<StorageBrowser
|
|
179
|
+
files={data.files}
|
|
180
|
+
onDelete={handleBulkDelete}
|
|
181
|
+
/>
|
|
182
|
+
)}
|
|
183
|
+
</BottomSheet>
|
|
184
|
+
|
|
185
|
+
{/* Confirm dialogs */}
|
|
186
|
+
<ConfirmDialog
|
|
187
|
+
open={confirmAction === 'clearOld'}
|
|
188
|
+
title="Clear Old Files"
|
|
189
|
+
message="Delete all uploaded files older than 30 days? This cannot be undone."
|
|
190
|
+
confirmLabel={deleting ? 'Deleting...' : 'Delete Old Files'}
|
|
191
|
+
danger
|
|
192
|
+
onConfirm={handleConfirmAction}
|
|
193
|
+
onCancel={() => setConfirmAction(null)}
|
|
194
|
+
/>
|
|
195
|
+
<ConfirmDialog
|
|
196
|
+
open={confirmAction === 'clearAll'}
|
|
197
|
+
title="Clear All Files"
|
|
198
|
+
message="Delete ALL uploaded files? This will free up all storage but cannot be undone."
|
|
199
|
+
confirmLabel={deleting ? 'Deleting...' : 'Delete All'}
|
|
200
|
+
danger
|
|
201
|
+
onConfirm={handleConfirmAction}
|
|
202
|
+
onCancel={() => setConfirmAction(null)}
|
|
203
|
+
/>
|
|
204
|
+
</div>
|
|
205
|
+
)
|
|
206
|
+
}
|
|
@@ -25,6 +25,24 @@ export function UserPreferencesSection({ appSettings, patchSettings, inputClass
|
|
|
25
25
|
style={{ fontFamily: 'inherit' }}
|
|
26
26
|
/>
|
|
27
27
|
|
|
28
|
+
{/* Suggested replies toggle */}
|
|
29
|
+
<div className="mt-6 flex items-center justify-between">
|
|
30
|
+
<div>
|
|
31
|
+
<label className="text-[12px] font-600 text-text-2 block">Suggested Replies</label>
|
|
32
|
+
<p className="text-[11px] text-text-3/60 mt-0.5">
|
|
33
|
+
Show follow-up suggestions after each agent response.
|
|
34
|
+
</p>
|
|
35
|
+
</div>
|
|
36
|
+
<button
|
|
37
|
+
type="button"
|
|
38
|
+
onClick={() => patchSettings({ suggestionsEnabled: appSettings.suggestionsEnabled === false })}
|
|
39
|
+
className={`relative w-9 h-5 rounded-full transition-colors ${appSettings.suggestionsEnabled !== false ? 'bg-accent-bright' : 'bg-white/[0.10]'}`}
|
|
40
|
+
style={{ fontFamily: 'inherit' }}
|
|
41
|
+
>
|
|
42
|
+
<span className={`absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-white transition-transform ${appSettings.suggestionsEnabled !== false ? 'translate-x-4' : ''}`} />
|
|
43
|
+
</button>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
28
46
|
{/* Default agent */}
|
|
29
47
|
<div className="mt-6">
|
|
30
48
|
<label className="text-[12px] font-600 text-text-2 block mb-1.5">Default Agent</label>
|