@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
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react'
|
|
4
|
+
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'
|
|
5
|
+
|
|
6
|
+
interface Shortcut {
|
|
7
|
+
keys: string[]
|
|
8
|
+
description: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ShortcutGroup {
|
|
12
|
+
title: string
|
|
13
|
+
shortcuts: Shortcut[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const isMac = typeof navigator !== 'undefined' && /Mac|iPod|iPhone|iPad/.test(navigator.userAgent)
|
|
17
|
+
const MOD = isMac ? '\u2318' : 'Ctrl'
|
|
18
|
+
|
|
19
|
+
const GROUPS: ShortcutGroup[] = [
|
|
20
|
+
{
|
|
21
|
+
title: 'Navigation',
|
|
22
|
+
shortcuts: [
|
|
23
|
+
{ keys: [MOD, 'K'], description: 'Open search' },
|
|
24
|
+
{ keys: [MOD, 'Shift', 'A'], description: 'Switch agent' },
|
|
25
|
+
{ keys: [MOD, 'N'], description: 'New chat' },
|
|
26
|
+
{ keys: [MOD, 'Shift', 'T'], description: 'Jump to tasks' },
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
title: 'Chat',
|
|
31
|
+
shortcuts: [
|
|
32
|
+
{ keys: ['Enter'], description: 'Send message' },
|
|
33
|
+
{ keys: ['Shift', 'Enter'], description: 'New line' },
|
|
34
|
+
{ keys: ['Esc'], description: 'Cancel reply / close' },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
title: 'General',
|
|
39
|
+
shortcuts: [
|
|
40
|
+
{ keys: ['?'], description: 'Show keyboard shortcuts' },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
function Kbd({ children }: { children: string }) {
|
|
46
|
+
return (
|
|
47
|
+
<kbd className="inline-flex items-center justify-center min-w-[22px] h-[22px] px-1.5 rounded-[5px] bg-white/[0.08] border border-white/[0.1] text-[11px] font-mono text-text-2 leading-none">
|
|
48
|
+
{children}
|
|
49
|
+
</kbd>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function KeyboardShortcutsDialog() {
|
|
54
|
+
const [open, setOpen] = useState(false)
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const handler = (e: KeyboardEvent) => {
|
|
58
|
+
// Ctrl+/ or Cmd+/
|
|
59
|
+
if ((e.metaKey || e.ctrlKey) && e.key === '/') {
|
|
60
|
+
e.preventDefault()
|
|
61
|
+
setOpen((v) => !v)
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
const tag = (e.target as HTMLElement)?.tagName
|
|
65
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return
|
|
66
|
+
if ((e.target as HTMLElement)?.isContentEditable) return
|
|
67
|
+
if (e.key === '?' && !e.metaKey && !e.ctrlKey && !e.altKey) {
|
|
68
|
+
e.preventDefault()
|
|
69
|
+
setOpen((v) => !v)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
window.addEventListener('keydown', handler)
|
|
73
|
+
return () => window.removeEventListener('keydown', handler)
|
|
74
|
+
}, [])
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
78
|
+
<DialogContent
|
|
79
|
+
showCloseButton={false}
|
|
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
|
+
>
|
|
82
|
+
<DialogTitle className="sr-only">Keyboard shortcuts</DialogTitle>
|
|
83
|
+
<div className="flex items-center justify-between px-5 py-3.5 border-b border-white/[0.06]">
|
|
84
|
+
<span className="text-[14px] font-600 text-text">Keyboard Shortcuts</span>
|
|
85
|
+
<kbd className="px-1.5 py-0.5 rounded-[5px] bg-white/[0.06] border border-white/[0.08] text-[10px] font-mono text-text-3">
|
|
86
|
+
ESC
|
|
87
|
+
</kbd>
|
|
88
|
+
</div>
|
|
89
|
+
<div className="py-2 max-h-[400px] overflow-y-auto">
|
|
90
|
+
{GROUPS.map((group) => (
|
|
91
|
+
<div key={group.title} className="px-5 py-2">
|
|
92
|
+
<h3 className="text-[11px] font-700 uppercase tracking-wider text-text-3/60 mb-2">
|
|
93
|
+
{group.title}
|
|
94
|
+
</h3>
|
|
95
|
+
<div className="flex flex-col gap-1.5">
|
|
96
|
+
{group.shortcuts.map((shortcut) => (
|
|
97
|
+
<div
|
|
98
|
+
key={shortcut.description}
|
|
99
|
+
className="flex items-center justify-between py-1"
|
|
100
|
+
>
|
|
101
|
+
<span className="text-[13px] text-text-2">{shortcut.description}</span>
|
|
102
|
+
<div className="flex items-center gap-1">
|
|
103
|
+
{shortcut.keys.map((key, i) => (
|
|
104
|
+
<Kbd key={i}>{key}</Kbd>
|
|
105
|
+
))}
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
))}
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
))}
|
|
112
|
+
</div>
|
|
113
|
+
</DialogContent>
|
|
114
|
+
</Dialog>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
@@ -34,7 +34,25 @@ const TYPE_ICON_COLORS: Record<AppNotification['type'], string> = {
|
|
|
34
34
|
error: 'text-red-400',
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
function resolveHttpUrl(raw: string | undefined): string | null {
|
|
38
|
+
if (!raw) return null
|
|
39
|
+
try {
|
|
40
|
+
const parsed = new URL(raw)
|
|
41
|
+
return parsed.protocol === 'http:' || parsed.protocol === 'https:' ? parsed.toString() : null
|
|
42
|
+
} catch {
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function NotificationCenter({
|
|
48
|
+
variant = 'icon',
|
|
49
|
+
align = 'right',
|
|
50
|
+
direction = 'down',
|
|
51
|
+
}: {
|
|
52
|
+
variant?: 'icon' | 'row'
|
|
53
|
+
align?: 'left' | 'right'
|
|
54
|
+
direction?: 'up' | 'down'
|
|
55
|
+
}) {
|
|
38
56
|
const [open, setOpen] = useState(false)
|
|
39
57
|
const panelRef = useRef<HTMLDivElement>(null)
|
|
40
58
|
const buttonRef = useRef<HTMLButtonElement>(null)
|
|
@@ -76,27 +94,42 @@ export function NotificationCenter() {
|
|
|
76
94
|
if (!n.read) {
|
|
77
95
|
markRead(n.id)
|
|
78
96
|
}
|
|
79
|
-
|
|
97
|
+
const actionUrl = resolveHttpUrl(n.actionUrl)
|
|
98
|
+
if (actionUrl) {
|
|
99
|
+
window.open(actionUrl, '_blank', 'noopener,noreferrer')
|
|
100
|
+
}
|
|
80
101
|
setOpen(false)
|
|
81
102
|
}
|
|
82
103
|
|
|
104
|
+
const isRow = variant === 'row'
|
|
105
|
+
const panelAlignClass = align === 'left' ? 'left-0' : 'right-0'
|
|
106
|
+
const panelDirectionClass = direction === 'up' ? 'bottom-full mb-2' : 'top-full mt-2'
|
|
107
|
+
|
|
83
108
|
return (
|
|
84
109
|
<div className="relative">
|
|
85
110
|
<button
|
|
86
111
|
ref={buttonRef}
|
|
87
112
|
onClick={() => setOpen((v) => !v)}
|
|
88
|
-
className=
|
|
113
|
+
className={
|
|
114
|
+
isRow
|
|
115
|
+
? 'relative w-full flex items-center gap-2.5 px-3 py-2 rounded-[10px] text-[13px] font-500 cursor-pointer transition-all bg-transparent text-text-3 hover:text-text hover:bg-white/[0.04] border-none'
|
|
116
|
+
: 'relative flex items-center justify-center w-8 h-8 rounded-[8px] bg-transparent hover:bg-white/[0.05] transition-colors cursor-pointer border-none'
|
|
117
|
+
}
|
|
89
118
|
aria-label="Notifications"
|
|
90
119
|
title={unreadCount > 0 ? `${unreadCount} unread notifications` : 'Notifications'}
|
|
91
120
|
>
|
|
92
121
|
{/* Bell icon */}
|
|
93
|
-
<svg width=
|
|
122
|
+
<svg width={isRow ? '16' : '16'} height={isRow ? '16' : '16'} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={isRow ? 'text-text-3 shrink-0' : 'text-text-2'}>
|
|
94
123
|
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
|
|
95
124
|
<path d="M13.73 21a2 2 0 0 1-3.46 0" />
|
|
96
125
|
</svg>
|
|
126
|
+
{isRow && <span className="text-[13px] font-500">Notifications</span>}
|
|
97
127
|
{/* Badge */}
|
|
98
128
|
{unreadCount > 0 && (
|
|
99
|
-
<span className=
|
|
129
|
+
<span className={isRow
|
|
130
|
+
? 'ml-auto min-w-[18px] h-[18px] flex items-center justify-center rounded-full bg-red-500 text-[10px] font-700 text-white px-1 leading-none'
|
|
131
|
+
: 'absolute -top-0.5 -right-0.5 min-w-[16px] h-4 flex items-center justify-center rounded-full bg-red-500 text-[10px] font-700 text-white px-1 leading-none'}
|
|
132
|
+
>
|
|
100
133
|
{unreadCount > 99 ? '99+' : unreadCount}
|
|
101
134
|
</span>
|
|
102
135
|
)}
|
|
@@ -105,7 +138,7 @@ export function NotificationCenter() {
|
|
|
105
138
|
{open && (
|
|
106
139
|
<div
|
|
107
140
|
ref={panelRef}
|
|
108
|
-
className=
|
|
141
|
+
className={`absolute ${panelAlignClass} ${panelDirectionClass} w-[340px] max-h-[460px] bg-raised border border-white/[0.06] rounded-[14px] shadow-[0_16px_64px_rgba(0,0,0,0.6)] backdrop-blur-xl z-90 flex flex-col overflow-hidden`}
|
|
109
142
|
style={{ animation: 'fade-in 0.15s cubic-bezier(0.16, 1, 0.3, 1)' }}
|
|
110
143
|
>
|
|
111
144
|
{/* Header */}
|
|
@@ -164,6 +197,11 @@ export function NotificationCenter() {
|
|
|
164
197
|
{n.message}
|
|
165
198
|
</p>
|
|
166
199
|
)}
|
|
200
|
+
{resolveHttpUrl(n.actionUrl) && (
|
|
201
|
+
<span className="inline-block mt-1 text-[11px] text-accent-bright/90">
|
|
202
|
+
{n.actionLabel || 'Open link'}
|
|
203
|
+
</span>
|
|
204
|
+
)}
|
|
167
205
|
{n.entityType && (
|
|
168
206
|
<span className="inline-block mt-1 text-[10px] text-text-3/40 font-mono">
|
|
169
207
|
{n.entityType}{n.entityId ? `:${n.entityId.slice(0, 8)}` : ''}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react'
|
|
4
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
6
|
+
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
7
|
+
import { api } from '@/lib/api-client'
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
open: boolean
|
|
11
|
+
onClose: () => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function ProfileSheet({ open, onClose }: Props) {
|
|
15
|
+
const appSettings = useAppStore((s) => s.appSettings)
|
|
16
|
+
const loadSettings = useAppStore((s) => s.loadSettings)
|
|
17
|
+
const setUser = useAppStore((s) => s.setUser)
|
|
18
|
+
const currentUser = useAppStore((s) => s.currentUser)
|
|
19
|
+
|
|
20
|
+
const [name, setName] = useState('')
|
|
21
|
+
const [avatarSeed, setAvatarSeed] = useState('')
|
|
22
|
+
const [saving, setSaving] = useState(false)
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (open) {
|
|
26
|
+
setName(appSettings.userName || currentUser || '')
|
|
27
|
+
setAvatarSeed(appSettings.userAvatarSeed || '')
|
|
28
|
+
}
|
|
29
|
+
}, [open, appSettings.userName, appSettings.userAvatarSeed, currentUser])
|
|
30
|
+
|
|
31
|
+
const handleSave = async () => {
|
|
32
|
+
const trimmed = name.trim()
|
|
33
|
+
if (!trimmed || saving) return
|
|
34
|
+
setSaving(true)
|
|
35
|
+
try {
|
|
36
|
+
await api('PUT', '/settings', {
|
|
37
|
+
userName: trimmed.toLowerCase(),
|
|
38
|
+
userAvatarSeed: avatarSeed.trim() || undefined,
|
|
39
|
+
})
|
|
40
|
+
setUser(trimmed.toLowerCase())
|
|
41
|
+
await loadSettings()
|
|
42
|
+
onClose()
|
|
43
|
+
} finally {
|
|
44
|
+
setSaving(false)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const handleSignOut = () => {
|
|
49
|
+
setUser(null)
|
|
50
|
+
onClose()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<BottomSheet open={open} onClose={onClose}>
|
|
55
|
+
<div className="p-6 max-w-[400px] mx-auto">
|
|
56
|
+
<h2 className="font-display text-[18px] font-700 text-text mb-6 text-center">Profile</h2>
|
|
57
|
+
|
|
58
|
+
{/* Avatar preview */}
|
|
59
|
+
<div className="flex justify-center mb-6">
|
|
60
|
+
<AgentAvatar seed={avatarSeed || null} name={name || '?'} size={72} />
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
{/* Avatar seed */}
|
|
64
|
+
<div className="mb-4">
|
|
65
|
+
<label className="block text-[12px] font-600 text-text-2 mb-1.5">Avatar</label>
|
|
66
|
+
<div className="flex items-center gap-2">
|
|
67
|
+
<input
|
|
68
|
+
type="text"
|
|
69
|
+
value={avatarSeed}
|
|
70
|
+
onChange={(e) => setAvatarSeed(e.target.value)}
|
|
71
|
+
placeholder="Avatar seed (any text)"
|
|
72
|
+
className="flex-1 px-3 py-2 rounded-[8px] bg-white/[0.06] border border-white/[0.08] text-[13px] text-text placeholder:text-text-3 focus:outline-none focus:border-accent-bright/40"
|
|
73
|
+
/>
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
onClick={() => setAvatarSeed(Math.random().toString(36).slice(2, 10))}
|
|
77
|
+
className="px-3 py-2 rounded-[8px] border border-white/[0.08] bg-transparent text-text-3 text-[12px] font-600 cursor-pointer transition-all hover:bg-white/[0.04] shrink-0"
|
|
78
|
+
>
|
|
79
|
+
Randomize
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{/* Name */}
|
|
85
|
+
<div className="mb-6">
|
|
86
|
+
<label className="block text-[12px] font-600 text-text-2 mb-1.5">Name</label>
|
|
87
|
+
<input
|
|
88
|
+
type="text"
|
|
89
|
+
value={name}
|
|
90
|
+
onChange={(e) => setName(e.target.value)}
|
|
91
|
+
placeholder="Your name"
|
|
92
|
+
className="w-full px-3 py-2 rounded-[8px] bg-white/[0.06] border border-white/[0.08] text-[13px] text-text placeholder:text-text-3 focus:outline-none focus:border-accent-bright/40"
|
|
93
|
+
/>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
{/* Save */}
|
|
97
|
+
<button
|
|
98
|
+
onClick={handleSave}
|
|
99
|
+
disabled={!name.trim() || saving}
|
|
100
|
+
className="w-full py-2.5 rounded-[8px] text-[13px] font-600 bg-accent-bright text-white hover:bg-accent-bright/90 transition-all disabled:opacity-50 cursor-pointer mb-4"
|
|
101
|
+
>
|
|
102
|
+
{saving ? 'Saving...' : 'Save'}
|
|
103
|
+
</button>
|
|
104
|
+
|
|
105
|
+
{/* Sign out */}
|
|
106
|
+
<button
|
|
107
|
+
onClick={handleSignOut}
|
|
108
|
+
className="w-full text-center text-[12px] text-text-3 hover:text-text-2 transition-all cursor-pointer bg-transparent border-none"
|
|
109
|
+
>
|
|
110
|
+
Sign in as different user
|
|
111
|
+
</button>
|
|
112
|
+
</div>
|
|
113
|
+
</BottomSheet>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
interface Props {
|
|
4
|
+
senderName: string
|
|
5
|
+
text: string
|
|
6
|
+
onClick?: () => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function ReplyQuote({ senderName, text, onClick }: Props) {
|
|
10
|
+
const truncated = text.length > 120 ? text.slice(0, 120) + '...' : text
|
|
11
|
+
return (
|
|
12
|
+
<button
|
|
13
|
+
type="button"
|
|
14
|
+
onClick={onClick}
|
|
15
|
+
className="flex items-start gap-2 mb-1.5 text-left w-full bg-transparent border-none p-0 cursor-pointer group/reply"
|
|
16
|
+
>
|
|
17
|
+
<div className="w-0.5 shrink-0 self-stretch rounded-full bg-accent-bright/50" />
|
|
18
|
+
<div className="min-w-0 flex-1">
|
|
19
|
+
<span className="text-[11px] font-600 text-accent-bright">{senderName}</span>
|
|
20
|
+
<p className="text-[12px] text-text-3 leading-[1.4] break-words m-0 group-hover/reply:text-text-2 transition-colors">
|
|
21
|
+
{truncated}
|
|
22
|
+
</p>
|
|
23
|
+
</div>
|
|
24
|
+
</button>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
@@ -7,11 +7,12 @@ import { api } from '@/lib/api-client'
|
|
|
7
7
|
import type { AppView } from '@/types'
|
|
8
8
|
|
|
9
9
|
interface SearchResult {
|
|
10
|
-
type: 'task' | 'agent' | 'session' | 'schedule' | 'webhook' | 'skill'
|
|
10
|
+
type: 'task' | 'agent' | 'session' | 'schedule' | 'webhook' | 'skill' | 'message'
|
|
11
11
|
id: string
|
|
12
12
|
title: string
|
|
13
13
|
description?: string
|
|
14
14
|
status?: string
|
|
15
|
+
messageIndex?: number
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
const TYPE_ICONS: Record<SearchResult['type'], string> = {
|
|
@@ -21,11 +22,13 @@ const TYPE_ICONS: Record<SearchResult['type'], string> = {
|
|
|
21
22
|
schedule: 'M12 6v6l4 2',
|
|
22
23
|
webhook: 'M22 12h-4l-3 7L9 5l-3 7H2',
|
|
23
24
|
skill: 'M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z',
|
|
25
|
+
message: 'M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z',
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
const TYPE_EXTRA_PATHS: Partial<Record<SearchResult['type'], string>> = {
|
|
27
29
|
agent: 'M12 7a4 4 0 1 0 0-0.01',
|
|
28
30
|
schedule: 'M12 12a10 10 0 1 0 0-0.01',
|
|
31
|
+
message: 'M8 10h8',
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
const TYPE_VIEW_MAP: Record<SearchResult['type'], AppView> = {
|
|
@@ -35,15 +38,17 @@ const TYPE_VIEW_MAP: Record<SearchResult['type'], AppView> = {
|
|
|
35
38
|
schedule: 'schedules',
|
|
36
39
|
webhook: 'webhooks',
|
|
37
40
|
skill: 'skills',
|
|
41
|
+
message: 'agents',
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
const TYPE_LABELS: Record<SearchResult['type'], string> = {
|
|
41
45
|
agent: 'Agent',
|
|
42
46
|
task: 'Task',
|
|
43
|
-
session: '
|
|
47
|
+
session: 'Chat',
|
|
44
48
|
schedule: 'Schedule',
|
|
45
49
|
webhook: 'Webhook',
|
|
46
50
|
skill: 'Skill',
|
|
51
|
+
message: 'Message',
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
export function SearchDialog() {
|
|
@@ -68,7 +73,7 @@ export function SearchDialog() {
|
|
|
68
73
|
const setWebhookSheetOpen = useAppStore((s) => s.setWebhookSheetOpen)
|
|
69
74
|
const setEditingSkillId = useAppStore((s) => s.setEditingSkillId)
|
|
70
75
|
const setSkillSheetOpen = useAppStore((s) => s.setSkillSheetOpen)
|
|
71
|
-
const
|
|
76
|
+
const setCurrentSession = useAppStore((s) => s.setCurrentSession)
|
|
72
77
|
|
|
73
78
|
// Global Cmd+K / Ctrl+K listener
|
|
74
79
|
useEffect(() => {
|
|
@@ -91,12 +96,12 @@ export function SearchDialog() {
|
|
|
91
96
|
|
|
92
97
|
// Reset on open
|
|
93
98
|
useEffect(() => {
|
|
94
|
-
if (open)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
if (!open) return
|
|
100
|
+
setQuery('')
|
|
101
|
+
setResults([])
|
|
102
|
+
setSelectedIdx(0)
|
|
103
|
+
const timer = setTimeout(() => inputRef.current?.focus(), 50)
|
|
104
|
+
return () => clearTimeout(timer)
|
|
100
105
|
}, [open])
|
|
101
106
|
|
|
102
107
|
// Debounced search
|
|
@@ -141,8 +146,18 @@ export function SearchDialog() {
|
|
|
141
146
|
setTaskSheetOpen(true)
|
|
142
147
|
break
|
|
143
148
|
case 'session':
|
|
144
|
-
|
|
149
|
+
setCurrentSession(result.id)
|
|
150
|
+
setActiveView('agents')
|
|
151
|
+
break
|
|
152
|
+
case 'message':
|
|
153
|
+
setCurrentSession(result.id)
|
|
145
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
|
+
}
|
|
146
161
|
break
|
|
147
162
|
case 'schedule':
|
|
148
163
|
setEditingScheduleId(result.id)
|
|
@@ -185,7 +200,7 @@ export function SearchDialog() {
|
|
|
185
200
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
186
201
|
<DialogContent
|
|
187
202
|
showCloseButton={false}
|
|
188
|
-
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"
|
|
189
204
|
onKeyDown={handleKeyDown}
|
|
190
205
|
>
|
|
191
206
|
<DialogTitle className="sr-only">Search</DialogTitle>
|
|
@@ -199,6 +214,7 @@ export function SearchDialog() {
|
|
|
199
214
|
value={query}
|
|
200
215
|
onChange={(e) => handleQueryChange(e.target.value)}
|
|
201
216
|
placeholder="Search agents, tasks, schedules..."
|
|
217
|
+
aria-label="Search"
|
|
202
218
|
className="flex-1 bg-transparent border-none outline-none text-[14px] text-text placeholder:text-text-3/60 font-[inherit]"
|
|
203
219
|
autoFocus
|
|
204
220
|
/>
|
|
@@ -226,18 +242,18 @@ export function SearchDialog() {
|
|
|
226
242
|
)}
|
|
227
243
|
{results.map((result, idx) => (
|
|
228
244
|
<button
|
|
229
|
-
key={`${result.type}-${result.id}`}
|
|
245
|
+
key={result.type === 'message' ? `${result.type}-${result.id}-${result.messageIndex}` : `${result.type}-${result.id}`}
|
|
230
246
|
onClick={() => navigateTo(result)}
|
|
231
247
|
onMouseEnter={() => setSelectedIdx(idx)}
|
|
232
|
-
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
|
|
233
249
|
${idx === selectedIdx ? 'bg-white/[0.06]' : 'hover:bg-white/[0.04]'}`}
|
|
234
250
|
style={{ fontFamily: 'inherit' }}
|
|
235
251
|
>
|
|
236
252
|
{/* Type icon */}
|
|
237
253
|
<div className={`w-8 h-8 rounded-[8px] flex items-center justify-center shrink-0
|
|
238
|
-
${idx === selectedIdx ? 'bg-
|
|
254
|
+
${idx === selectedIdx ? 'bg-accent-bright/20' : 'bg-white/[0.04]'}`}>
|
|
239
255
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"
|
|
240
|
-
className={idx === selectedIdx ? 'text-
|
|
256
|
+
className={idx === selectedIdx ? 'text-accent-bright' : 'text-text-3'}>
|
|
241
257
|
<path d={TYPE_ICONS[result.type]} />
|
|
242
258
|
{TYPE_EXTRA_PATHS[result.type] && <path d={TYPE_EXTRA_PATHS[result.type]} />}
|
|
243
259
|
</svg>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
children: React.ReactNode
|
|
3
|
+
className?: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function SectionLabel({ children, className = '' }: Props) {
|
|
7
|
+
return (
|
|
8
|
+
<label className={`block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3 ${className}`}>
|
|
9
|
+
{children}
|
|
10
|
+
</label>
|
|
11
|
+
)
|
|
12
|
+
}
|
|
@@ -103,7 +103,7 @@ export function PluginManager() {
|
|
|
103
103
|
<div
|
|
104
104
|
onClick={() => togglePlugin(p.filename, !p.enabled)}
|
|
105
105
|
className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0
|
|
106
|
-
${p.enabled ? 'bg-
|
|
106
|
+
${p.enabled ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
|
|
107
107
|
>
|
|
108
108
|
<div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200
|
|
109
109
|
${p.enabled ? 'left-[22px]' : 'left-0.5'}`} />
|
|
@@ -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
|
</>
|