@swarmclawai/swarmclaw 0.5.3 → 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 +53 -9
- package/bin/server-cmd.js +1 -0
- package/bin/swarmclaw.js +76 -16
- package/next.config.ts +11 -1
- package/package.json +5 -2
- package/scripts/postinstall.mjs +18 -0
- package/src/app/api/canvas/[sessionId]/route.ts +31 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +284 -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/connectors/[id]/route.ts +1 -0
- package/src/app/api/connectors/route.ts +2 -1
- package/src/app/api/credentials/route.ts +2 -3
- package/src/app/api/files/open/route.ts +43 -0
- 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/route.ts +4 -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 +53 -1
- 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/[id]/messages/route.ts +70 -2
- package/src/app/api/sessions/[id]/route.ts +4 -0
- 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 +12 -0
- package/src/app/api/tasks/bulk/route.ts +100 -0
- package/src/app/api/tasks/metrics/route.ts +101 -0
- package/src/app/api/tasks/route.ts +18 -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/api/webhooks/[id]/route.ts +15 -1
- package/src/app/globals.css +63 -15
- package/src/app/page.tsx +142 -13
- package/src/cli/index.js +40 -1
- package/src/cli/index.test.js +30 -0
- package/src/cli/spec.js +42 -0
- package/src/components/agents/agent-avatar.tsx +57 -10
- package/src/components/agents/agent-card.tsx +50 -17
- package/src/components/agents/agent-chat-list.tsx +148 -12
- package/src/components/agents/agent-list.tsx +50 -19
- package/src/components/agents/agent-sheet.tsx +120 -65
- 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/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/canvas/canvas-panel.tsx +96 -0
- package/src/components/chat/activity-moment.tsx +173 -0
- package/src/components/chat/chat-area.tsx +46 -22
- package/src/components/chat/chat-header.tsx +457 -286
- package/src/components/chat/chat-preview-panel.tsx +1 -2
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/delegation-banner.tsx +371 -0
- package/src/components/chat/file-path-chip.tsx +146 -0
- package/src/components/chat/heartbeat-history-panel.tsx +269 -0
- package/src/components/chat/markdown-utils.ts +9 -0
- package/src/components/chat/message-bubble.tsx +356 -315
- package/src/components/chat/message-list.tsx +230 -8
- package/src/components/chat/streaming-bubble.tsx +104 -47
- 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 +111 -73
- 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 +130 -0
- package/src/components/chatrooms/chatroom-message.tsx +432 -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-list.tsx +168 -90
- package/src/components/connectors/connector-sheet.tsx +95 -56
- package/src/components/home/home-view.tsx +501 -0
- package/src/components/input/chat-input.tsx +107 -43
- 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 +194 -97
- 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 +1 -1
- package/src/components/schedules/schedule-list.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +259 -126
- 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/settings/gateway-disconnect-overlay.tsx +80 -0
- 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/chatroom-picker-list.tsx +61 -0
- package/src/components/shared/check-icon.tsx +12 -0
- package/src/components/shared/confirm-dialog.tsx +1 -1
- package/src/components/shared/connector-platform-icon.tsx +51 -4
- 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/icon-button.tsx +16 -2
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
- package/src/components/shared/notification-center.tsx +44 -6
- 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 +31 -15
- 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-embedding.tsx +48 -13
- package/src/components/shared/settings/section-orchestrator.tsx +46 -15
- 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-storage.tsx +206 -0
- package/src/components/shared/settings/section-theme.tsx +95 -0
- package/src/components/shared/settings/section-user-preferences.tsx +57 -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 +182 -27
- package/src/components/shared/settings/settings-sheet.tsx +9 -73
- package/src/components/shared/settings/storage-browser.tsx +259 -0
- 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 +59 -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 +416 -74
- package/src/components/ui/hover-card.tsx +52 -0
- package/src/components/usage/metrics-dashboard.tsx +90 -6
- package/src/components/usage/usage-list.tsx +1 -1
- package/src/components/webhooks/webhook-sheet.tsx +1 -1
- package/src/hooks/use-continuous-speech.ts +10 -4
- package/src/hooks/use-view-router.ts +69 -19
- package/src/hooks/use-voice-conversation.ts +53 -10
- package/src/hooks/use-ws.ts +4 -2
- package/src/instrumentation.ts +15 -1
- package/src/lib/chat.ts +2 -0
- package/src/lib/memory.ts +3 -0
- 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 +75 -15
- package/src/lib/server/chatroom-helpers.ts +146 -0
- package/src/lib/server/connectors/manager.ts +229 -7
- package/src/lib/server/context-manager.ts +225 -13
- package/src/lib/server/create-notification.ts +14 -2
- package/src/lib/server/daemon-state.ts +157 -10
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service.ts +48 -6
- package/src/lib/server/heartbeat-wake.ts +110 -0
- package/src/lib/server/langgraph-checkpoint.ts +1 -0
- package/src/lib/server/main-agent-loop.ts +1 -1
- package/src/lib/server/memory-consolidation.ts +105 -0
- package/src/lib/server/memory-db.ts +183 -10
- package/src/lib/server/mime.ts +51 -0
- package/src/lib/server/openclaw-gateway.ts +9 -1
- 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/provider-health.ts +125 -0
- package/src/lib/server/queue.ts +56 -10
- package/src/lib/server/scheduler.ts +8 -0
- package/src/lib/server/session-run-manager.ts +4 -0
- package/src/lib/server/session-tools/canvas.ts +67 -0
- package/src/lib/server/session-tools/chatroom.ts +136 -0
- package/src/lib/server/session-tools/connector.ts +83 -9
- package/src/lib/server/session-tools/context-mgmt.ts +36 -18
- 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 +10 -0
- package/src/lib/server/session-tools/memory.ts +7 -1
- 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/storage.ts +53 -29
- package/src/lib/server/stream-agent-chat.ts +185 -57
- package/src/lib/server/system-events.ts +49 -0
- package/src/lib/server/task-mention.ts +41 -0
- package/src/lib/server/ws-hub.ts +11 -0
- package/src/lib/sessions.ts +10 -0
- package/src/lib/soul-library.ts +103 -0
- package/src/lib/soul-suggestions.ts +109 -0
- package/src/lib/task-dedupe.ts +26 -0
- package/src/lib/tasks.ts +4 -1
- package/src/lib/tool-definitions.ts +2 -0
- package/src/lib/tts.ts +2 -2
- package/src/lib/view-routes.ts +36 -1
- package/src/lib/ws-client.ts +14 -4
- package/src/stores/use-app-store.ts +41 -3
- package/src/stores/use-chat-store.ts +113 -5
- package/src/stores/use-chatroom-store.ts +276 -0
- package/src/types/index.ts +88 -4
|
@@ -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>
|
|
@@ -167,7 +167,7 @@ export function ProvidersSection({ inputClass }: SettingsSectionProps) {
|
|
|
167
167
|
<button
|
|
168
168
|
onClick={handleAdd}
|
|
169
169
|
disabled={!newKey.trim()}
|
|
170
|
-
className="flex-1 py-3 rounded-[14px] border-none bg-
|
|
170
|
+
className="flex-1 py-3 rounded-[14px] border-none bg-accent-bright text-white text-[14px] font-600 cursor-pointer disabled:opacity-30 transition-all hover:brightness-110"
|
|
171
171
|
style={{ fontFamily: 'inherit' }}
|
|
172
172
|
>
|
|
173
173
|
Save Key
|
|
@@ -121,7 +121,7 @@ export function SecretsSection({ appSettings, inputClass }: SettingsSectionProps
|
|
|
121
121
|
|
|
122
122
|
<div className="flex gap-3 pt-2">
|
|
123
123
|
<button onClick={() => setAddingSecret(false)} className="flex-1 py-3 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[14px] font-600 cursor-pointer hover:bg-surface-2 transition-colors" style={{ fontFamily: 'inherit' }}>Cancel</button>
|
|
124
|
-
<button onClick={handleAddSecret} disabled={!secretName.trim() || !secretValue.trim()} className="flex-1 py-3 rounded-[14px] border-none bg-
|
|
124
|
+
<button onClick={handleAddSecret} disabled={!secretName.trim() || !secretValue.trim()} className="flex-1 py-3 rounded-[14px] border-none bg-accent-bright text-white text-[14px] font-600 cursor-pointer disabled:opacity-30 transition-all hover:brightness-110" style={{ fontFamily: 'inherit' }}>Save Secret</button>
|
|
125
125
|
</div>
|
|
126
126
|
</div>
|
|
127
127
|
) : (
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { toast } from 'sonner'
|
|
5
|
+
import type { SettingsSectionProps } from './types'
|
|
6
|
+
|
|
7
|
+
const PRESETS = [
|
|
8
|
+
{ label: 'Default', color: '#1e1e30' },
|
|
9
|
+
{ label: 'Midnight', color: '#1a1a3a' },
|
|
10
|
+
{ label: 'Forest', color: '#1a2e1e' },
|
|
11
|
+
{ label: 'Warm', color: '#2e1e1a' },
|
|
12
|
+
{ label: 'Slate', color: '#1e2428' },
|
|
13
|
+
{ label: 'Rose', color: '#2e1a24' },
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
export function ThemeSection({ appSettings, patchSettings, inputClass }: SettingsSectionProps) {
|
|
17
|
+
const currentHue = appSettings.themeHue || PRESETS[0].color
|
|
18
|
+
const [customHex, setCustomHex] = useState(
|
|
19
|
+
PRESETS.some((p) => p.color === currentHue) ? '' : currentHue,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
const applyHue = (color: string) => {
|
|
23
|
+
patchSettings({ themeHue: color })
|
|
24
|
+
document.documentElement.style.setProperty('--neutral-tint', color)
|
|
25
|
+
toast.success('Theme updated')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const handleCustomChange = (value: string) => {
|
|
29
|
+
setCustomHex(value)
|
|
30
|
+
if (/^#[0-9a-fA-F]{6}$/.test(value)) {
|
|
31
|
+
applyHue(value)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="mb-10">
|
|
37
|
+
<h3 className="font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
|
|
38
|
+
Theme
|
|
39
|
+
</h3>
|
|
40
|
+
<p className="text-[12px] text-text-3 mb-5">
|
|
41
|
+
Shift the UI color palette. Pick a preset or enter a custom hex color.
|
|
42
|
+
</p>
|
|
43
|
+
|
|
44
|
+
{/* Preset swatches */}
|
|
45
|
+
<div className="flex flex-wrap gap-3 mb-4">
|
|
46
|
+
{PRESETS.map((preset) => {
|
|
47
|
+
const isActive = currentHue === preset.color && !customHex
|
|
48
|
+
return (
|
|
49
|
+
<button
|
|
50
|
+
key={preset.color}
|
|
51
|
+
onClick={() => { setCustomHex(''); applyHue(preset.color) }}
|
|
52
|
+
className={`group flex flex-col items-center gap-1.5 cursor-pointer bg-transparent border-none p-0`}
|
|
53
|
+
title={preset.label}
|
|
54
|
+
>
|
|
55
|
+
<div
|
|
56
|
+
className={`w-9 h-9 rounded-full transition-all duration-200 ${
|
|
57
|
+
isActive
|
|
58
|
+
? 'ring-2 ring-accent-bright ring-offset-2 ring-offset-bg scale-110'
|
|
59
|
+
: 'hover:scale-105'
|
|
60
|
+
}`}
|
|
61
|
+
style={{ backgroundColor: preset.color }}
|
|
62
|
+
/>
|
|
63
|
+
<span className={`text-[10px] font-500 ${isActive ? 'text-text' : 'text-text-3'}`}>
|
|
64
|
+
{preset.label}
|
|
65
|
+
</span>
|
|
66
|
+
</button>
|
|
67
|
+
)
|
|
68
|
+
})}
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
{/* Custom color picker + hex input */}
|
|
72
|
+
<div className="flex items-center gap-3">
|
|
73
|
+
<label className="text-[12px] text-text-3 shrink-0">Custom</label>
|
|
74
|
+
<div className="relative shrink-0">
|
|
75
|
+
<input
|
|
76
|
+
type="color"
|
|
77
|
+
value={customHex || currentHue}
|
|
78
|
+
onChange={(e) => handleCustomChange(e.target.value)}
|
|
79
|
+
className="w-9 h-9 rounded-full cursor-pointer border-2 border-white/[0.1] bg-transparent appearance-none [&::-webkit-color-swatch-wrapper]:p-0 [&::-webkit-color-swatch]:rounded-full [&::-webkit-color-swatch]:border-none [&::-moz-color-swatch]:rounded-full [&::-moz-color-swatch]:border-none"
|
|
80
|
+
title="Pick a custom color"
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
<input
|
|
84
|
+
type="text"
|
|
85
|
+
value={customHex}
|
|
86
|
+
onChange={(e) => handleCustomChange(e.target.value)}
|
|
87
|
+
placeholder="#2a1f3d"
|
|
88
|
+
maxLength={7}
|
|
89
|
+
className={`${inputClass} max-w-[120px] font-mono`}
|
|
90
|
+
style={{ fontFamily: 'inherit' }}
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
4
|
+
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
3
5
|
import type { SettingsSectionProps } from './types'
|
|
4
6
|
|
|
5
7
|
export function UserPreferencesSection({ appSettings, patchSettings, inputClass }: SettingsSectionProps) {
|
|
8
|
+
const agents = useAppStore((s) => s.agents)
|
|
9
|
+
const sortedAgents = Object.values(agents).sort((a, b) => a.name.localeCompare(b.name))
|
|
10
|
+
|
|
6
11
|
return (
|
|
7
12
|
<div className="mb-10">
|
|
8
13
|
<h3 className="font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
|
|
@@ -19,6 +24,58 @@ export function UserPreferencesSection({ appSettings, patchSettings, inputClass
|
|
|
19
24
|
className={`${inputClass} resize-y min-h-[100px]`}
|
|
20
25
|
style={{ fontFamily: 'inherit' }}
|
|
21
26
|
/>
|
|
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
|
+
|
|
46
|
+
{/* Default agent */}
|
|
47
|
+
<div className="mt-6">
|
|
48
|
+
<label className="text-[12px] font-600 text-text-2 block mb-1.5">Default Agent</label>
|
|
49
|
+
<p className="text-[11px] text-text-3/60 mb-2">
|
|
50
|
+
The agent that opens automatically when you start the app or click Main Chat.
|
|
51
|
+
</p>
|
|
52
|
+
<div className="flex flex-wrap gap-2">
|
|
53
|
+
<button
|
|
54
|
+
onClick={() => patchSettings({ defaultAgentId: null })}
|
|
55
|
+
className={`flex items-center gap-2 px-3 py-2 rounded-[10px] text-[12px] font-600 cursor-pointer transition-all border
|
|
56
|
+
${!appSettings.defaultAgentId
|
|
57
|
+
? 'bg-white/[0.06] border-accent-bright/30 text-text'
|
|
58
|
+
: 'bg-transparent border-white/[0.06] text-text-3 hover:bg-white/[0.03]'}`}
|
|
59
|
+
style={{ fontFamily: 'inherit' }}
|
|
60
|
+
>
|
|
61
|
+
Auto (first agent)
|
|
62
|
+
</button>
|
|
63
|
+
{sortedAgents.map((agent) => (
|
|
64
|
+
<button
|
|
65
|
+
key={agent.id}
|
|
66
|
+
onClick={() => patchSettings({ defaultAgentId: agent.id })}
|
|
67
|
+
className={`flex items-center gap-2 px-3 py-2 rounded-[10px] text-[12px] font-600 cursor-pointer transition-all border
|
|
68
|
+
${appSettings.defaultAgentId === agent.id
|
|
69
|
+
? 'bg-white/[0.06] border-accent-bright/30 text-text'
|
|
70
|
+
: 'bg-transparent border-white/[0.06] text-text-3 hover:bg-white/[0.03]'}`}
|
|
71
|
+
style={{ fontFamily: 'inherit' }}
|
|
72
|
+
>
|
|
73
|
+
<AgentAvatar seed={agent.avatarSeed || null} name={agent.name} size={18} />
|
|
74
|
+
{agent.name}
|
|
75
|
+
</button>
|
|
76
|
+
))}
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
22
79
|
</div>
|
|
23
80
|
)
|
|
24
81
|
}
|
|
@@ -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>
|