iris-chatbot 0.2.4
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/LICENSE +21 -0
- package/README.md +49 -0
- package/bin/iris.mjs +267 -0
- package/package.json +61 -0
- package/template/LICENSE +21 -0
- package/template/README.md +49 -0
- package/template/eslint.config.mjs +18 -0
- package/template/next.config.ts +7 -0
- package/template/package-lock.json +9193 -0
- package/template/package.json +46 -0
- package/template/postcss.config.mjs +7 -0
- package/template/public/file.svg +1 -0
- package/template/public/globe.svg +1 -0
- package/template/public/next.svg +1 -0
- package/template/public/vercel.svg +1 -0
- package/template/public/window.svg +1 -0
- package/template/src/app/api/chat/route.ts +2445 -0
- package/template/src/app/api/connections/models/route.ts +255 -0
- package/template/src/app/api/connections/test/route.ts +124 -0
- package/template/src/app/api/local-sync/route.ts +74 -0
- package/template/src/app/api/tool-approval/route.ts +47 -0
- package/template/src/app/favicon.ico +0 -0
- package/template/src/app/globals.css +808 -0
- package/template/src/app/layout.tsx +74 -0
- package/template/src/app/page.tsx +444 -0
- package/template/src/components/ChatView.tsx +1537 -0
- package/template/src/components/Composer.tsx +160 -0
- package/template/src/components/MapView.tsx +244 -0
- package/template/src/components/MessageCard.tsx +955 -0
- package/template/src/components/SearchModal.tsx +72 -0
- package/template/src/components/SettingsModal.tsx +1257 -0
- package/template/src/components/Sidebar.tsx +153 -0
- package/template/src/components/TopBar.tsx +164 -0
- package/template/src/lib/connections.ts +275 -0
- package/template/src/lib/data.ts +324 -0
- package/template/src/lib/db.ts +49 -0
- package/template/src/lib/hooks.ts +76 -0
- package/template/src/lib/local-sync.ts +192 -0
- package/template/src/lib/memory.ts +695 -0
- package/template/src/lib/model-presets.ts +251 -0
- package/template/src/lib/store.ts +36 -0
- package/template/src/lib/tooling/approvals.ts +78 -0
- package/template/src/lib/tooling/providers/anthropic.ts +155 -0
- package/template/src/lib/tooling/providers/ollama.ts +73 -0
- package/template/src/lib/tooling/providers/openai.ts +267 -0
- package/template/src/lib/tooling/providers/openai_compatible.ts +16 -0
- package/template/src/lib/tooling/providers/types.ts +44 -0
- package/template/src/lib/tooling/registry.ts +103 -0
- package/template/src/lib/tooling/runtime.ts +189 -0
- package/template/src/lib/tooling/safety.ts +165 -0
- package/template/src/lib/tooling/tools/apps.ts +108 -0
- package/template/src/lib/tooling/tools/apps_plus.ts +153 -0
- package/template/src/lib/tooling/tools/communication.ts +883 -0
- package/template/src/lib/tooling/tools/files.ts +395 -0
- package/template/src/lib/tooling/tools/music.ts +988 -0
- package/template/src/lib/tooling/tools/notes.ts +461 -0
- package/template/src/lib/tooling/tools/notes_plus.ts +294 -0
- package/template/src/lib/tooling/tools/numbers.ts +175 -0
- package/template/src/lib/tooling/tools/schedule.ts +579 -0
- package/template/src/lib/tooling/tools/system.ts +142 -0
- package/template/src/lib/tooling/tools/web.ts +212 -0
- package/template/src/lib/tooling/tools/workflow.ts +218 -0
- package/template/src/lib/tooling/types.ts +27 -0
- package/template/src/lib/types.ts +309 -0
- package/template/src/lib/utils.ts +108 -0
- package/template/tsconfig.json +34 -0
|
@@ -0,0 +1,1257 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { Eye, EyeOff, RefreshCw, X } from "lucide-react";
|
|
5
|
+
import {
|
|
6
|
+
BUILTIN_CONNECTION_IDS,
|
|
7
|
+
ensureBuiltinConnections,
|
|
8
|
+
normalizeBaseUrl,
|
|
9
|
+
} from "../lib/connections";
|
|
10
|
+
import { db } from "../lib/db";
|
|
11
|
+
import { useMemories } from "../lib/hooks";
|
|
12
|
+
import { normalizeMemoryKey } from "../lib/memory";
|
|
13
|
+
import { filterModelIdsForConnection, getConnectionModelPresets } from "../lib/model-presets";
|
|
14
|
+
import {
|
|
15
|
+
DEFAULT_MEMORY_SETTINGS,
|
|
16
|
+
DEFAULT_LOCAL_TOOLS_SETTINGS,
|
|
17
|
+
type ApprovalMode,
|
|
18
|
+
type ChatConnectionPayload,
|
|
19
|
+
type ConnectionKind,
|
|
20
|
+
type MemoryEntry,
|
|
21
|
+
type MemoryKind,
|
|
22
|
+
type MemoryScope,
|
|
23
|
+
type MemorySource,
|
|
24
|
+
type ModelConnection,
|
|
25
|
+
type SafetyProfile,
|
|
26
|
+
type Settings,
|
|
27
|
+
type WebSearchBackend,
|
|
28
|
+
} from "../lib/types";
|
|
29
|
+
|
|
30
|
+
type TabId =
|
|
31
|
+
| "models"
|
|
32
|
+
| "keys"
|
|
33
|
+
| "connections"
|
|
34
|
+
| "local-tools"
|
|
35
|
+
| "memory"
|
|
36
|
+
| "appearance"
|
|
37
|
+
| "advanced";
|
|
38
|
+
|
|
39
|
+
const TABS: Array<{ id: TabId; label: string }> = [
|
|
40
|
+
{ id: "models", label: "Models" },
|
|
41
|
+
{ id: "keys", label: "API Keys" },
|
|
42
|
+
{ id: "connections", label: "Connections" },
|
|
43
|
+
{ id: "local-tools", label: "Local Tools" },
|
|
44
|
+
{ id: "memory", label: "Memory" },
|
|
45
|
+
{ id: "appearance", label: "Appearance" },
|
|
46
|
+
{ id: "advanced", label: "Advanced" },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
function parseHeadersText(value: string): Array<{ key: string; value: string }> {
|
|
50
|
+
return value
|
|
51
|
+
.split("\n")
|
|
52
|
+
.map((line) => line.trim())
|
|
53
|
+
.filter(Boolean)
|
|
54
|
+
.map((line) => {
|
|
55
|
+
const separatorIndex = line.indexOf(":");
|
|
56
|
+
if (separatorIndex === -1) {
|
|
57
|
+
return { key: line, value: "" };
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
key: line.slice(0, separatorIndex).trim(),
|
|
61
|
+
value: line.slice(separatorIndex + 1).trim(),
|
|
62
|
+
};
|
|
63
|
+
})
|
|
64
|
+
.filter((header) => header.key.length > 0);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function formatHeadersText(headers: Array<{ key: string; value: string }> | undefined): string {
|
|
68
|
+
if (!Array.isArray(headers) || headers.length === 0) {
|
|
69
|
+
return "";
|
|
70
|
+
}
|
|
71
|
+
return headers.map((header) => `${header.key}: ${header.value}`).join("\n");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function makeConnectionPayload(
|
|
75
|
+
connection: ModelConnection,
|
|
76
|
+
keys: { openaiKey: string; anthropicKey: string; geminiKey: string },
|
|
77
|
+
): ChatConnectionPayload {
|
|
78
|
+
const keyFromProvider =
|
|
79
|
+
connection.kind === "builtin"
|
|
80
|
+
? connection.provider === "openai"
|
|
81
|
+
? keys.openaiKey
|
|
82
|
+
: connection.provider === "anthropic"
|
|
83
|
+
? keys.anthropicKey
|
|
84
|
+
: connection.provider === "google"
|
|
85
|
+
? keys.geminiKey
|
|
86
|
+
: ""
|
|
87
|
+
: "";
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
id: connection.id,
|
|
91
|
+
name: connection.name,
|
|
92
|
+
kind: connection.kind,
|
|
93
|
+
provider: connection.provider,
|
|
94
|
+
baseUrl: connection.baseUrl,
|
|
95
|
+
apiKey: connection.apiKey || keyFromProvider || undefined,
|
|
96
|
+
headers: connection.headers ?? [],
|
|
97
|
+
supportsTools: connection.supportsTools,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export default function SettingsModal({
|
|
102
|
+
settings,
|
|
103
|
+
onClose,
|
|
104
|
+
}: {
|
|
105
|
+
settings: Settings | null;
|
|
106
|
+
onClose: () => void;
|
|
107
|
+
}) {
|
|
108
|
+
const [activeTab, setActiveTab] = useState<TabId>("models");
|
|
109
|
+
const [openaiKey, setOpenaiKey] = useState(settings?.openaiKey || "");
|
|
110
|
+
const [anthropicKey, setAnthropicKey] = useState(settings?.anthropicKey || "");
|
|
111
|
+
const [geminiKey, setGeminiKey] = useState(settings?.geminiKey || "");
|
|
112
|
+
const [showOpenAIKey, setShowOpenAIKey] = useState(false);
|
|
113
|
+
const [showAnthropicKey, setShowAnthropicKey] = useState(false);
|
|
114
|
+
const [showGeminiKey, setShowGeminiKey] = useState(false);
|
|
115
|
+
const [accentColor, setAccentColor] = useState(settings?.accentColor || "#66706e");
|
|
116
|
+
const [theme, setTheme] = useState<"dark" | "light">(settings?.theme || "dark");
|
|
117
|
+
const [font, setFont] = useState<"ibm" | "manrope" | "sora" | "space" | "poppins">(
|
|
118
|
+
settings?.font || "manrope",
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const localTools = settings?.localTools ?? DEFAULT_LOCAL_TOOLS_SETTINGS;
|
|
122
|
+
const [toolsEnabled, setToolsEnabled] = useState(localTools.enabled);
|
|
123
|
+
const [approvalMode, setApprovalMode] = useState<ApprovalMode>(localTools.approvalMode);
|
|
124
|
+
const [safetyProfile, setSafetyProfile] = useState<SafetyProfile>(localTools.safetyProfile);
|
|
125
|
+
const [allowedRootsText, setAllowedRootsText] = useState((localTools.allowedRoots ?? []).join("\n"));
|
|
126
|
+
const [enableNotes, setEnableNotes] = useState(localTools.enableNotes);
|
|
127
|
+
const [enableApps, setEnableApps] = useState(localTools.enableApps);
|
|
128
|
+
const [enableNumbers, setEnableNumbers] = useState(localTools.enableNumbers);
|
|
129
|
+
const [enableWeb, setEnableWeb] = useState(localTools.enableWeb);
|
|
130
|
+
const [enableMusic, setEnableMusic] = useState(localTools.enableMusic);
|
|
131
|
+
const [enableCalendar, setEnableCalendar] = useState(localTools.enableCalendar);
|
|
132
|
+
const [enableMail, setEnableMail] = useState(localTools.enableMail);
|
|
133
|
+
const [enableWorkflow, setEnableWorkflow] = useState(localTools.enableWorkflow);
|
|
134
|
+
const [enableSystem, setEnableSystem] = useState(localTools.enableSystem);
|
|
135
|
+
const [webSearchBackend, setWebSearchBackend] = useState<WebSearchBackend>(localTools.webSearchBackend);
|
|
136
|
+
const [dryRun, setDryRun] = useState(localTools.dryRun);
|
|
137
|
+
const memory = settings?.memory ?? DEFAULT_MEMORY_SETTINGS;
|
|
138
|
+
const [memoryEnabled, setMemoryEnabled] = useState(memory.enabled);
|
|
139
|
+
const [memoryAutoCapture, setMemoryAutoCapture] = useState(memory.autoCapture);
|
|
140
|
+
const [memoryToolInfluence, setMemoryToolInfluence] = useState(memory.toolInfluence);
|
|
141
|
+
|
|
142
|
+
const [connections, setConnections] = useState<ModelConnection[]>(settings?.connections ?? []);
|
|
143
|
+
const [defaultConnectionId, setDefaultConnectionId] = useState(
|
|
144
|
+
settings?.defaultConnectionId ?? settings?.connections?.[0]?.id ?? BUILTIN_CONNECTION_IDS.openai,
|
|
145
|
+
);
|
|
146
|
+
const [defaultModelByConnection, setDefaultModelByConnection] = useState<Record<string, string>>(
|
|
147
|
+
settings?.defaultModelByConnection ?? {},
|
|
148
|
+
);
|
|
149
|
+
const [showExtendedOpenAIModels, setShowExtendedOpenAIModels] = useState(
|
|
150
|
+
Boolean(settings?.showExtendedOpenAIModels),
|
|
151
|
+
);
|
|
152
|
+
const [enableWebSources, setEnableWebSources] = useState(settings?.enableWebSources ?? true);
|
|
153
|
+
|
|
154
|
+
const [editingConnectionId, setEditingConnectionId] = useState<string | null>(null);
|
|
155
|
+
const [connectionFormName, setConnectionFormName] = useState("");
|
|
156
|
+
const [connectionFormKind, setConnectionFormKind] = useState<ConnectionKind>("openai_compatible");
|
|
157
|
+
const [connectionFormBaseUrl, setConnectionFormBaseUrl] = useState("");
|
|
158
|
+
const [connectionFormApiKey, setConnectionFormApiKey] = useState("");
|
|
159
|
+
const [connectionFormSupportsTools, setConnectionFormSupportsTools] = useState(true);
|
|
160
|
+
const [connectionFormHeadersText, setConnectionFormHeadersText] = useState("");
|
|
161
|
+
|
|
162
|
+
const [modelInput, setModelInput] = useState("");
|
|
163
|
+
const [connectionStatus, setConnectionStatus] = useState<Record<string, string>>({});
|
|
164
|
+
const [connectionStatusBusy, setConnectionStatusBusy] = useState<Record<string, boolean>>({});
|
|
165
|
+
const [fetchedModels, setFetchedModels] = useState<Record<string, string[]>>({});
|
|
166
|
+
const memories = useMemories();
|
|
167
|
+
const [memorySearch, setMemorySearch] = useState("");
|
|
168
|
+
const [editingMemoryId, setEditingMemoryId] = useState<string | null>(null);
|
|
169
|
+
const [memoryFormKind, setMemoryFormKind] = useState<MemoryKind>("note");
|
|
170
|
+
const [memoryFormScope, setMemoryFormScope] = useState<MemoryScope>("global");
|
|
171
|
+
const [memoryFormConversationId, setMemoryFormConversationId] = useState("");
|
|
172
|
+
const [memoryFormSource, setMemoryFormSource] = useState<MemorySource>("manual");
|
|
173
|
+
const [memoryFormKey, setMemoryFormKey] = useState("");
|
|
174
|
+
const [memoryFormValue, setMemoryFormValue] = useState("");
|
|
175
|
+
const [memoryStatus, setMemoryStatus] = useState<string | null>(null);
|
|
176
|
+
const enabledConnections = connections.filter((connection) => connection.enabled);
|
|
177
|
+
const selectedDefaultConnection =
|
|
178
|
+
connections.find((connection) => connection.id === defaultConnectionId) ?? enabledConnections[0] ?? null;
|
|
179
|
+
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
if (!selectedDefaultConnection) {
|
|
182
|
+
setModelInput("");
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
setModelInput(defaultModelByConnection[selectedDefaultConnection.id] || "");
|
|
186
|
+
}, [selectedDefaultConnection, defaultModelByConnection]);
|
|
187
|
+
|
|
188
|
+
if (!settings) {
|
|
189
|
+
return (
|
|
190
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-6">
|
|
191
|
+
<div className="w-full max-w-md rounded-2xl border border-[var(--border)] bg-[var(--panel)] p-6 text-sm text-[var(--text-muted)]">
|
|
192
|
+
Loading settings...
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const selectedDefaultModel = selectedDefaultConnection
|
|
199
|
+
? defaultModelByConnection[selectedDefaultConnection.id] ||
|
|
200
|
+
getConnectionModelPresets(selectedDefaultConnection)[0] ||
|
|
201
|
+
""
|
|
202
|
+
: "";
|
|
203
|
+
const selectedPresets = selectedDefaultConnection ? getConnectionModelPresets(selectedDefaultConnection) : [];
|
|
204
|
+
const selectedFetchedModels = selectedDefaultConnection ? fetchedModels[selectedDefaultConnection.id] ?? [] : [];
|
|
205
|
+
const selectableModels = filterModelIdsForConnection({
|
|
206
|
+
connection: selectedDefaultConnection,
|
|
207
|
+
modelIds: [...new Set([...selectedFetchedModels, ...selectedPresets])],
|
|
208
|
+
includeExtendedOpenAI: showExtendedOpenAIModels,
|
|
209
|
+
});
|
|
210
|
+
const filteredMemories = (memories ?? []).filter((entry) => {
|
|
211
|
+
const query = memorySearch.trim().toLowerCase();
|
|
212
|
+
if (!query) {
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
const text = `${entry.kind} ${entry.scope} ${entry.key} ${entry.value} ${entry.conversationId ?? ""}`.toLowerCase();
|
|
216
|
+
return text.includes(query);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const resetConnectionForm = () => {
|
|
220
|
+
setEditingConnectionId(null);
|
|
221
|
+
setConnectionFormName("");
|
|
222
|
+
setConnectionFormKind("openai_compatible");
|
|
223
|
+
setConnectionFormBaseUrl("");
|
|
224
|
+
setConnectionFormApiKey("");
|
|
225
|
+
setConnectionFormSupportsTools(true);
|
|
226
|
+
setConnectionFormHeadersText("");
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const resetMemoryForm = () => {
|
|
230
|
+
setEditingMemoryId(null);
|
|
231
|
+
setMemoryFormKind("note");
|
|
232
|
+
setMemoryFormScope("global");
|
|
233
|
+
setMemoryFormConversationId("");
|
|
234
|
+
setMemoryFormSource("manual");
|
|
235
|
+
setMemoryFormKey("");
|
|
236
|
+
setMemoryFormValue("");
|
|
237
|
+
setMemoryStatus(null);
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const loadMemoryIntoForm = (entry: MemoryEntry) => {
|
|
241
|
+
setEditingMemoryId(entry.id);
|
|
242
|
+
setMemoryFormKind(entry.kind);
|
|
243
|
+
setMemoryFormScope(entry.scope);
|
|
244
|
+
setMemoryFormConversationId(entry.conversationId ?? "");
|
|
245
|
+
setMemoryFormSource(entry.source);
|
|
246
|
+
setMemoryFormKey(entry.key);
|
|
247
|
+
setMemoryFormValue(entry.value);
|
|
248
|
+
setMemoryStatus(null);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const upsertMemoryEntry = async () => {
|
|
252
|
+
const key = memoryFormKey.trim();
|
|
253
|
+
const value = memoryFormValue.trim();
|
|
254
|
+
if (!key || !value) {
|
|
255
|
+
setMemoryStatus("Key and value are required.");
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const normalizedKey = normalizeMemoryKey(key);
|
|
259
|
+
if (!normalizedKey) {
|
|
260
|
+
setMemoryStatus("Key must include letters or numbers.");
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (memoryFormScope === "conversation" && !memoryFormConversationId.trim()) {
|
|
264
|
+
setMemoryStatus("Conversation ID is required for conversation-scoped memory.");
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const now = Date.now();
|
|
269
|
+
const existing = editingMemoryId
|
|
270
|
+
? (memories ?? []).find((entry) => entry.id === editingMemoryId)
|
|
271
|
+
: null;
|
|
272
|
+
const entry: MemoryEntry = {
|
|
273
|
+
id: editingMemoryId ?? `memory-${now}`,
|
|
274
|
+
kind: memoryFormKind,
|
|
275
|
+
scope: memoryFormScope,
|
|
276
|
+
conversationId:
|
|
277
|
+
memoryFormScope === "conversation"
|
|
278
|
+
? memoryFormConversationId.trim()
|
|
279
|
+
: undefined,
|
|
280
|
+
key,
|
|
281
|
+
value,
|
|
282
|
+
normalizedKey,
|
|
283
|
+
source: memoryFormSource,
|
|
284
|
+
confidence:
|
|
285
|
+
memoryFormSource === "auto"
|
|
286
|
+
? 0.75
|
|
287
|
+
: 1,
|
|
288
|
+
createdAt: existing?.createdAt ?? now,
|
|
289
|
+
updatedAt: now,
|
|
290
|
+
lastUsedAt: existing?.lastUsedAt ?? now,
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
await db.memories.put(entry);
|
|
294
|
+
resetMemoryForm();
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const deleteMemoryEntry = async (entryId: string) => {
|
|
298
|
+
await db.memories.delete(entryId);
|
|
299
|
+
if (editingMemoryId === entryId) {
|
|
300
|
+
resetMemoryForm();
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const clearAllMemories = async () => {
|
|
305
|
+
if (!window.confirm("Delete all saved memory entries?")) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
await db.memories.clear();
|
|
309
|
+
resetMemoryForm();
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const upsertConnectionFromForm = () => {
|
|
313
|
+
const normalizedKind: ConnectionKind =
|
|
314
|
+
connectionFormKind === "ollama" ? "ollama" : "openai_compatible";
|
|
315
|
+
const normalizedBaseUrl = normalizeBaseUrl(connectionFormBaseUrl);
|
|
316
|
+
const baseUrl =
|
|
317
|
+
normalizedKind === "ollama"
|
|
318
|
+
? normalizedBaseUrl || "http://localhost:11434"
|
|
319
|
+
: normalizedBaseUrl;
|
|
320
|
+
if (normalizedKind === "openai_compatible" && !baseUrl) {
|
|
321
|
+
setConnectionStatus((current) => ({
|
|
322
|
+
...current,
|
|
323
|
+
form: "Base URL is required for OpenAI-compatible connections.",
|
|
324
|
+
}));
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const now = Date.now();
|
|
328
|
+
const headers = parseHeadersText(connectionFormHeadersText);
|
|
329
|
+
const id = editingConnectionId || `conn-${now}`;
|
|
330
|
+
const nextConnection: ModelConnection = {
|
|
331
|
+
id,
|
|
332
|
+
name: connectionFormName.trim() || id,
|
|
333
|
+
kind: normalizedKind,
|
|
334
|
+
provider: undefined,
|
|
335
|
+
baseUrl,
|
|
336
|
+
apiKey: connectionFormApiKey.trim() || undefined,
|
|
337
|
+
headers,
|
|
338
|
+
supportsTools: connectionFormSupportsTools,
|
|
339
|
+
enabled: true,
|
|
340
|
+
createdAt: editingConnectionId
|
|
341
|
+
? connections.find((connection) => connection.id === editingConnectionId)?.createdAt ?? now
|
|
342
|
+
: now,
|
|
343
|
+
updatedAt: now,
|
|
344
|
+
};
|
|
345
|
+
setConnections((current) => {
|
|
346
|
+
const existingIndex = current.findIndex((item) => item.id === id);
|
|
347
|
+
if (existingIndex === -1) {
|
|
348
|
+
return [...current, nextConnection];
|
|
349
|
+
}
|
|
350
|
+
const next = [...current];
|
|
351
|
+
next[existingIndex] = nextConnection;
|
|
352
|
+
return next;
|
|
353
|
+
});
|
|
354
|
+
setDefaultConnectionId((current) => current || id);
|
|
355
|
+
setConnectionStatus((current) => {
|
|
356
|
+
if (!current.form) {
|
|
357
|
+
return current;
|
|
358
|
+
}
|
|
359
|
+
const next = { ...current };
|
|
360
|
+
delete next.form;
|
|
361
|
+
return next;
|
|
362
|
+
});
|
|
363
|
+
resetConnectionForm();
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const testConnection = async (connection: ModelConnection) => {
|
|
367
|
+
setConnectionStatusBusy((current) => ({ ...current, [connection.id]: true }));
|
|
368
|
+
setConnectionStatus((current) => ({ ...current, [connection.id]: "Testing..." }));
|
|
369
|
+
try {
|
|
370
|
+
const payload = makeConnectionPayload(connection, { openaiKey, anthropicKey, geminiKey });
|
|
371
|
+
const response = await fetch("/api/connections/test", {
|
|
372
|
+
method: "POST",
|
|
373
|
+
headers: { "Content-Type": "application/json" },
|
|
374
|
+
body: JSON.stringify({ connection: payload }),
|
|
375
|
+
});
|
|
376
|
+
const body = (await response.json()) as { ok?: boolean; message?: string; error?: string };
|
|
377
|
+
if (!response.ok || body.ok === false) {
|
|
378
|
+
throw new Error(body.error || "Connection test failed.");
|
|
379
|
+
}
|
|
380
|
+
setConnectionStatus((current) => ({ ...current, [connection.id]: body.message || "Connected." }));
|
|
381
|
+
} catch (error) {
|
|
382
|
+
const message = error instanceof Error ? error.message : "Connection test failed.";
|
|
383
|
+
setConnectionStatus((current) => ({ ...current, [connection.id]: message }));
|
|
384
|
+
} finally {
|
|
385
|
+
setConnectionStatusBusy((current) => ({ ...current, [connection.id]: false }));
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const fetchModelsForConnection = async (connection: ModelConnection) => {
|
|
390
|
+
setConnectionStatusBusy((current) => ({ ...current, [connection.id]: true }));
|
|
391
|
+
setConnectionStatus((current) => ({ ...current, [connection.id]: "Fetching models..." }));
|
|
392
|
+
try {
|
|
393
|
+
const payload = makeConnectionPayload(connection, { openaiKey, anthropicKey, geminiKey });
|
|
394
|
+
const response = await fetch("/api/connections/models", {
|
|
395
|
+
method: "POST",
|
|
396
|
+
headers: { "Content-Type": "application/json" },
|
|
397
|
+
body: JSON.stringify({ connection: payload }),
|
|
398
|
+
});
|
|
399
|
+
const body = (await response.json()) as { ok?: boolean; models?: string[]; error?: string };
|
|
400
|
+
if (!response.ok || body.ok === false) {
|
|
401
|
+
throw new Error(body.error || "Could not fetch models.");
|
|
402
|
+
}
|
|
403
|
+
const models = Array.isArray(body.models) ? body.models.filter(Boolean) : [];
|
|
404
|
+
setFetchedModels((current) => ({ ...current, [connection.id]: models }));
|
|
405
|
+
setConnectionStatus((current) => ({
|
|
406
|
+
...current,
|
|
407
|
+
[connection.id]: models.length > 0 ? `Fetched ${models.length} models.` : "No models returned.",
|
|
408
|
+
}));
|
|
409
|
+
if (models.length > 0) {
|
|
410
|
+
setDefaultModelByConnection((current) => ({
|
|
411
|
+
...current,
|
|
412
|
+
[connection.id]: current[connection.id] || models[0],
|
|
413
|
+
}));
|
|
414
|
+
}
|
|
415
|
+
} catch (error) {
|
|
416
|
+
const message = error instanceof Error ? error.message : "Could not fetch models.";
|
|
417
|
+
setConnectionStatus((current) => ({ ...current, [connection.id]: message }));
|
|
418
|
+
} finally {
|
|
419
|
+
setConnectionStatusBusy((current) => ({ ...current, [connection.id]: false }));
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const handleSave = async () => {
|
|
424
|
+
const allowedRoots = allowedRootsText
|
|
425
|
+
.split("\n")
|
|
426
|
+
.map((root) => root.trim())
|
|
427
|
+
.filter(Boolean);
|
|
428
|
+
|
|
429
|
+
const syncedConnections = connections.map((connection) => {
|
|
430
|
+
if (connection.kind !== "builtin") {
|
|
431
|
+
return connection;
|
|
432
|
+
}
|
|
433
|
+
if (connection.provider === "openai") {
|
|
434
|
+
return { ...connection, apiKey: openaiKey.trim() || undefined, updatedAt: Date.now() };
|
|
435
|
+
}
|
|
436
|
+
if (connection.provider === "anthropic") {
|
|
437
|
+
return { ...connection, apiKey: anthropicKey.trim() || undefined, updatedAt: Date.now() };
|
|
438
|
+
}
|
|
439
|
+
if (connection.provider === "google") {
|
|
440
|
+
return { ...connection, apiKey: geminiKey.trim() || undefined, updatedAt: Date.now() };
|
|
441
|
+
}
|
|
442
|
+
return connection;
|
|
443
|
+
});
|
|
444
|
+
const normalizedConnections = ensureBuiltinConnections(
|
|
445
|
+
syncedConnections,
|
|
446
|
+
{
|
|
447
|
+
openaiKey: openaiKey.trim() || undefined,
|
|
448
|
+
anthropicKey: anthropicKey.trim() || undefined,
|
|
449
|
+
geminiKey: geminiKey.trim() || undefined,
|
|
450
|
+
},
|
|
451
|
+
Date.now(),
|
|
452
|
+
);
|
|
453
|
+
const enabled = normalizedConnections.filter((connection) => connection.enabled);
|
|
454
|
+
const resolvedDefaultConnection =
|
|
455
|
+
(enabled.find((connection) => connection.id === defaultConnectionId)?.id ?? enabled[0]?.id) ||
|
|
456
|
+
normalizedConnections[0]?.id ||
|
|
457
|
+
BUILTIN_CONNECTION_IDS.openai;
|
|
458
|
+
|
|
459
|
+
const defaultConnection =
|
|
460
|
+
normalizedConnections.find((connection) => connection.id === resolvedDefaultConnection) ?? null;
|
|
461
|
+
const resolvedDefaultModelMap: Record<string, string> = {};
|
|
462
|
+
for (const connection of normalizedConnections) {
|
|
463
|
+
const model =
|
|
464
|
+
defaultModelByConnection[connection.id]?.trim() ||
|
|
465
|
+
getConnectionModelPresets(connection)[0] ||
|
|
466
|
+
"";
|
|
467
|
+
if (model) {
|
|
468
|
+
resolvedDefaultModelMap[connection.id] = model;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
const legacyProvider =
|
|
472
|
+
defaultConnection?.kind === "builtin" && defaultConnection.provider
|
|
473
|
+
? defaultConnection.provider
|
|
474
|
+
: "openai";
|
|
475
|
+
const legacyModel = resolvedDefaultModelMap[resolvedDefaultConnection] || settings.defaultModel || "gpt-5.2";
|
|
476
|
+
|
|
477
|
+
await db.settings.put({
|
|
478
|
+
id: "settings",
|
|
479
|
+
openaiKey: openaiKey.trim() || undefined,
|
|
480
|
+
anthropicKey: anthropicKey.trim() || undefined,
|
|
481
|
+
geminiKey: geminiKey.trim() || undefined,
|
|
482
|
+
defaultProvider: legacyProvider,
|
|
483
|
+
defaultModel: legacyModel,
|
|
484
|
+
connections: normalizedConnections,
|
|
485
|
+
defaultConnectionId: resolvedDefaultConnection,
|
|
486
|
+
defaultModelByConnection: resolvedDefaultModelMap,
|
|
487
|
+
showExtendedOpenAIModels,
|
|
488
|
+
enableWebSources,
|
|
489
|
+
accentColor: accentColor || "#66706e",
|
|
490
|
+
font,
|
|
491
|
+
theme,
|
|
492
|
+
localTools: {
|
|
493
|
+
enabled: toolsEnabled,
|
|
494
|
+
approvalMode,
|
|
495
|
+
safetyProfile,
|
|
496
|
+
allowedRoots:
|
|
497
|
+
allowedRoots.length > 0 ? allowedRoots : [...DEFAULT_LOCAL_TOOLS_SETTINGS.allowedRoots],
|
|
498
|
+
enableNotes,
|
|
499
|
+
enableApps,
|
|
500
|
+
enableNumbers,
|
|
501
|
+
enableWeb,
|
|
502
|
+
enableMusic,
|
|
503
|
+
enableCalendar,
|
|
504
|
+
enableMail,
|
|
505
|
+
enableWorkflow,
|
|
506
|
+
enableSystem,
|
|
507
|
+
webSearchBackend,
|
|
508
|
+
dryRun,
|
|
509
|
+
},
|
|
510
|
+
memory: {
|
|
511
|
+
enabled: memoryEnabled,
|
|
512
|
+
autoCapture: memoryAutoCapture,
|
|
513
|
+
toolInfluence: memoryToolInfluence,
|
|
514
|
+
extractionMode: "conservative",
|
|
515
|
+
},
|
|
516
|
+
});
|
|
517
|
+
onClose();
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
const accentPresets = [
|
|
521
|
+
{ id: "default", label: "Default", color: "#66706e" },
|
|
522
|
+
{ id: "blue", label: "Blue", color: "#2563eb" },
|
|
523
|
+
{ id: "green", label: "Green", color: "#16a34a" },
|
|
524
|
+
{ id: "yellow", label: "Yellow", color: "#f59e0b" },
|
|
525
|
+
{ id: "pink", label: "Pink", color: "#ec4899" },
|
|
526
|
+
{ id: "orange", label: "Orange", color: "#f97316" },
|
|
527
|
+
{ id: "red", label: "Red", color: "#dc2626" },
|
|
528
|
+
];
|
|
529
|
+
|
|
530
|
+
return (
|
|
531
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-6">
|
|
532
|
+
<div className="w-full max-w-4xl rounded-2xl border border-[var(--border)] bg-[var(--panel)] p-6 shadow-[var(--shadow)]">
|
|
533
|
+
<div className="flex items-center justify-between">
|
|
534
|
+
<h2 className="text-lg font-semibold">Settings</h2>
|
|
535
|
+
<button
|
|
536
|
+
onClick={onClose}
|
|
537
|
+
className="rounded-full border border-[var(--border)] bg-[var(--panel-2)] p-2"
|
|
538
|
+
>
|
|
539
|
+
<X className="h-4 w-4" />
|
|
540
|
+
</button>
|
|
541
|
+
</div>
|
|
542
|
+
|
|
543
|
+
<div className="mt-4 flex flex-wrap gap-2 border-b border-[var(--border)] pb-3">
|
|
544
|
+
{TABS.map((tab) => (
|
|
545
|
+
<button
|
|
546
|
+
key={tab.id}
|
|
547
|
+
onClick={() => setActiveTab(tab.id)}
|
|
548
|
+
className={`rounded-full px-3 py-1.5 text-xs ${
|
|
549
|
+
activeTab === tab.id
|
|
550
|
+
? "bg-[var(--accent)] text-white"
|
|
551
|
+
: "border border-[var(--border)] bg-[var(--panel-2)] text-[var(--text-secondary)]"
|
|
552
|
+
}`}
|
|
553
|
+
>
|
|
554
|
+
{tab.label}
|
|
555
|
+
</button>
|
|
556
|
+
))}
|
|
557
|
+
</div>
|
|
558
|
+
|
|
559
|
+
<div className="mt-5 max-h-[68vh] overflow-y-auto pr-1 text-sm">
|
|
560
|
+
{activeTab === "models" ? (
|
|
561
|
+
<div className="space-y-4">
|
|
562
|
+
<label className="flex items-center gap-2 rounded-xl border border-[var(--border)] bg-[var(--panel-2)] p-3 text-sm text-[var(--text-secondary)]">
|
|
563
|
+
<input
|
|
564
|
+
type="checkbox"
|
|
565
|
+
checked={showExtendedOpenAIModels}
|
|
566
|
+
onChange={(event) => setShowExtendedOpenAIModels(event.target.checked)}
|
|
567
|
+
/>
|
|
568
|
+
Show extended OpenAI model list (non-frontier models)
|
|
569
|
+
</label>
|
|
570
|
+
<div>
|
|
571
|
+
<label className="mb-2 block text-xs uppercase tracking-[0.2em] text-[var(--text-muted)]">
|
|
572
|
+
Default Connection
|
|
573
|
+
</label>
|
|
574
|
+
<select
|
|
575
|
+
className="w-full rounded-lg border border-[var(--border)] bg-[var(--panel-2)] px-3 py-2 text-sm"
|
|
576
|
+
value={defaultConnectionId}
|
|
577
|
+
onChange={(event) => setDefaultConnectionId(event.target.value)}
|
|
578
|
+
>
|
|
579
|
+
{enabledConnections.map((connection) => (
|
|
580
|
+
<option key={connection.id} value={connection.id}>
|
|
581
|
+
{connection.name}
|
|
582
|
+
</option>
|
|
583
|
+
))}
|
|
584
|
+
</select>
|
|
585
|
+
</div>
|
|
586
|
+
|
|
587
|
+
{selectedDefaultConnection ? (
|
|
588
|
+
<div className="space-y-3 rounded-xl border border-[var(--border)] bg-[var(--panel-2)] p-4">
|
|
589
|
+
<div className="text-xs uppercase tracking-[0.2em] text-[var(--text-muted)]">
|
|
590
|
+
Default model for {selectedDefaultConnection.name}
|
|
591
|
+
</div>
|
|
592
|
+
<input
|
|
593
|
+
className="w-full rounded-lg border border-[var(--border)] bg-[var(--panel)] px-3 py-2 text-sm"
|
|
594
|
+
value={modelInput || selectedDefaultModel}
|
|
595
|
+
onChange={(event) => {
|
|
596
|
+
const value = event.target.value;
|
|
597
|
+
setModelInput(value);
|
|
598
|
+
setDefaultModelByConnection((current) => ({
|
|
599
|
+
...current,
|
|
600
|
+
[selectedDefaultConnection.id]: value,
|
|
601
|
+
}));
|
|
602
|
+
}}
|
|
603
|
+
placeholder="Enter model id"
|
|
604
|
+
/>
|
|
605
|
+
<div className="flex flex-wrap gap-2">
|
|
606
|
+
{selectableModels.slice(0, 20).map((preset) => (
|
|
607
|
+
<button
|
|
608
|
+
key={preset}
|
|
609
|
+
className="rounded-full border border-[var(--border)] bg-[var(--panel)] px-3 py-1 text-xs text-[var(--text-secondary)]"
|
|
610
|
+
onClick={() => {
|
|
611
|
+
setModelInput(preset);
|
|
612
|
+
setDefaultModelByConnection((current) => ({
|
|
613
|
+
...current,
|
|
614
|
+
[selectedDefaultConnection.id]: preset,
|
|
615
|
+
}));
|
|
616
|
+
}}
|
|
617
|
+
>
|
|
618
|
+
{preset}
|
|
619
|
+
</button>
|
|
620
|
+
))}
|
|
621
|
+
{selectableModels.length === 0 ? (
|
|
622
|
+
<div className="text-xs text-[var(--text-muted)]">
|
|
623
|
+
No presets yet. Fetch models from Connections tab.
|
|
624
|
+
</div>
|
|
625
|
+
) : null}
|
|
626
|
+
</div>
|
|
627
|
+
</div>
|
|
628
|
+
) : null}
|
|
629
|
+
</div>
|
|
630
|
+
) : null}
|
|
631
|
+
|
|
632
|
+
{activeTab === "keys" ? (
|
|
633
|
+
<div className="space-y-4">
|
|
634
|
+
{[
|
|
635
|
+
{
|
|
636
|
+
label: "OpenAI API Key",
|
|
637
|
+
value: openaiKey,
|
|
638
|
+
setValue: setOpenaiKey,
|
|
639
|
+
show: showOpenAIKey,
|
|
640
|
+
setShow: setShowOpenAIKey,
|
|
641
|
+
placeholder: "sk-...",
|
|
642
|
+
},
|
|
643
|
+
{
|
|
644
|
+
label: "Anthropic API Key",
|
|
645
|
+
value: anthropicKey,
|
|
646
|
+
setValue: setAnthropicKey,
|
|
647
|
+
show: showAnthropicKey,
|
|
648
|
+
setShow: setShowAnthropicKey,
|
|
649
|
+
placeholder: "sk-ant-...",
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
label: "Google Gemini API Key",
|
|
653
|
+
value: geminiKey,
|
|
654
|
+
setValue: setGeminiKey,
|
|
655
|
+
show: showGeminiKey,
|
|
656
|
+
setShow: setShowGeminiKey,
|
|
657
|
+
placeholder: "AIza...",
|
|
658
|
+
},
|
|
659
|
+
].map((item) => (
|
|
660
|
+
<div key={item.label}>
|
|
661
|
+
<label className="mb-2 block text-xs uppercase tracking-[0.2em] text-[var(--text-muted)]">
|
|
662
|
+
{item.label}
|
|
663
|
+
</label>
|
|
664
|
+
<div className="relative">
|
|
665
|
+
<input
|
|
666
|
+
type={item.show ? "text" : "password"}
|
|
667
|
+
className="w-full rounded-lg border border-[var(--border)] bg-[var(--panel-2)] px-3 py-2 pr-10 text-sm"
|
|
668
|
+
value={item.value}
|
|
669
|
+
onChange={(event) => item.setValue(event.target.value)}
|
|
670
|
+
placeholder={item.placeholder}
|
|
671
|
+
/>
|
|
672
|
+
<button
|
|
673
|
+
type="button"
|
|
674
|
+
onClick={() => item.setShow((prev: boolean) => !prev)}
|
|
675
|
+
className="absolute right-3 top-1/2 -translate-y-1/2 text-[var(--text-muted)]"
|
|
676
|
+
>
|
|
677
|
+
{item.show ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
|
678
|
+
</button>
|
|
679
|
+
</div>
|
|
680
|
+
</div>
|
|
681
|
+
))}
|
|
682
|
+
</div>
|
|
683
|
+
) : null}
|
|
684
|
+
|
|
685
|
+
{activeTab === "connections" ? (
|
|
686
|
+
<div className="space-y-4">
|
|
687
|
+
<div className="space-y-2">
|
|
688
|
+
{connections.map((connection) => (
|
|
689
|
+
<div
|
|
690
|
+
key={connection.id}
|
|
691
|
+
className="rounded-xl border border-[var(--border)] bg-[var(--panel-2)] p-3"
|
|
692
|
+
>
|
|
693
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
694
|
+
<div>
|
|
695
|
+
<div className="text-sm font-medium text-[var(--text-primary)]">{connection.name}</div>
|
|
696
|
+
<div className="text-xs text-[var(--text-muted)]">
|
|
697
|
+
{connection.kind === "builtin"
|
|
698
|
+
? `Built-in: ${connection.provider}`
|
|
699
|
+
: connection.kind === "ollama"
|
|
700
|
+
? `Ollama (${connection.baseUrl || "http://localhost:11434"})`
|
|
701
|
+
: `OpenAI-compatible (${connection.baseUrl || "No URL"})`}
|
|
702
|
+
</div>
|
|
703
|
+
</div>
|
|
704
|
+
<div className="flex flex-wrap gap-2">
|
|
705
|
+
{connection.kind !== "builtin" ? (
|
|
706
|
+
<button
|
|
707
|
+
className="rounded-full border border-[var(--border)] bg-[var(--panel)] px-3 py-1 text-xs"
|
|
708
|
+
onClick={() => {
|
|
709
|
+
setEditingConnectionId(connection.id);
|
|
710
|
+
setConnectionFormName(connection.name);
|
|
711
|
+
setConnectionFormKind(connection.kind);
|
|
712
|
+
setConnectionFormBaseUrl(connection.baseUrl || "");
|
|
713
|
+
setConnectionFormApiKey(connection.apiKey || "");
|
|
714
|
+
setConnectionFormSupportsTools(connection.supportsTools ?? true);
|
|
715
|
+
setConnectionFormHeadersText(formatHeadersText(connection.headers));
|
|
716
|
+
}}
|
|
717
|
+
>
|
|
718
|
+
Edit
|
|
719
|
+
</button>
|
|
720
|
+
) : null}
|
|
721
|
+
<button
|
|
722
|
+
className="rounded-full border border-[var(--border)] bg-[var(--panel)] px-3 py-1 text-xs"
|
|
723
|
+
onClick={() => testConnection(connection)}
|
|
724
|
+
disabled={Boolean(connectionStatusBusy[connection.id])}
|
|
725
|
+
>
|
|
726
|
+
Test
|
|
727
|
+
</button>
|
|
728
|
+
<button
|
|
729
|
+
className="rounded-full border border-[var(--border)] bg-[var(--panel)] px-3 py-1 text-xs"
|
|
730
|
+
onClick={() => fetchModelsForConnection(connection)}
|
|
731
|
+
disabled={Boolean(connectionStatusBusy[connection.id])}
|
|
732
|
+
>
|
|
733
|
+
Fetch Models
|
|
734
|
+
</button>
|
|
735
|
+
{connection.kind !== "builtin" ? (
|
|
736
|
+
<button
|
|
737
|
+
className="rounded-full border border-[var(--danger)]/40 bg-[var(--panel)] px-3 py-1 text-xs text-[var(--danger)]"
|
|
738
|
+
onClick={() =>
|
|
739
|
+
setConnections((current) =>
|
|
740
|
+
current.filter((item) => item.id !== connection.id),
|
|
741
|
+
)
|
|
742
|
+
}
|
|
743
|
+
>
|
|
744
|
+
Remove
|
|
745
|
+
</button>
|
|
746
|
+
) : null}
|
|
747
|
+
</div>
|
|
748
|
+
</div>
|
|
749
|
+
<div className="mt-2 flex flex-wrap items-center gap-3 text-xs text-[var(--text-secondary)]">
|
|
750
|
+
<label className="flex items-center gap-2">
|
|
751
|
+
<input
|
|
752
|
+
type="checkbox"
|
|
753
|
+
checked={connection.enabled}
|
|
754
|
+
onChange={(event) =>
|
|
755
|
+
setConnections((current) =>
|
|
756
|
+
current.map((item) =>
|
|
757
|
+
item.id === connection.id
|
|
758
|
+
? { ...item, enabled: event.target.checked, updatedAt: Date.now() }
|
|
759
|
+
: item,
|
|
760
|
+
),
|
|
761
|
+
)
|
|
762
|
+
}
|
|
763
|
+
/>
|
|
764
|
+
Enabled
|
|
765
|
+
</label>
|
|
766
|
+
<span>
|
|
767
|
+
Tools: {(connection.supportsTools ?? true) ? "supported" : "disabled"}
|
|
768
|
+
</span>
|
|
769
|
+
{connection.kind === "builtin" ? (
|
|
770
|
+
<span>Keys configured in API Keys tab</span>
|
|
771
|
+
) : null}
|
|
772
|
+
{connectionStatus[connection.id] ? <span>{connectionStatus[connection.id]}</span> : null}
|
|
773
|
+
</div>
|
|
774
|
+
</div>
|
|
775
|
+
))}
|
|
776
|
+
</div>
|
|
777
|
+
|
|
778
|
+
<div className="rounded-xl border border-[var(--border)] bg-[var(--panel-2)] p-4">
|
|
779
|
+
<div className="mb-3 text-xs uppercase tracking-[0.2em] text-[var(--text-muted)]">
|
|
780
|
+
{editingConnectionId ? "Edit Connection" : "Add Connection"}
|
|
781
|
+
</div>
|
|
782
|
+
<div className="grid gap-3 md:grid-cols-2">
|
|
783
|
+
<input
|
|
784
|
+
className="rounded-lg border border-[var(--border)] bg-[var(--panel)] px-3 py-2 text-sm"
|
|
785
|
+
placeholder="Connection name"
|
|
786
|
+
value={connectionFormName}
|
|
787
|
+
onChange={(event) => setConnectionFormName(event.target.value)}
|
|
788
|
+
/>
|
|
789
|
+
<select
|
|
790
|
+
className="rounded-lg border border-[var(--border)] bg-[var(--panel)] px-3 py-2 text-sm"
|
|
791
|
+
value={connectionFormKind}
|
|
792
|
+
onChange={(event) => setConnectionFormKind(event.target.value as ConnectionKind)}
|
|
793
|
+
>
|
|
794
|
+
<option value="openai_compatible">OpenAI-compatible</option>
|
|
795
|
+
<option value="ollama">Ollama</option>
|
|
796
|
+
</select>
|
|
797
|
+
<input
|
|
798
|
+
className="rounded-lg border border-[var(--border)] bg-[var(--panel)] px-3 py-2 text-sm md:col-span-2"
|
|
799
|
+
placeholder="Base URL (e.g. http://localhost:11434)"
|
|
800
|
+
value={connectionFormBaseUrl}
|
|
801
|
+
onChange={(event) => setConnectionFormBaseUrl(event.target.value)}
|
|
802
|
+
/>
|
|
803
|
+
<input
|
|
804
|
+
className="rounded-lg border border-[var(--border)] bg-[var(--panel)] px-3 py-2 text-sm md:col-span-2"
|
|
805
|
+
placeholder="Optional API key override"
|
|
806
|
+
value={connectionFormApiKey}
|
|
807
|
+
onChange={(event) => setConnectionFormApiKey(event.target.value)}
|
|
808
|
+
/>
|
|
809
|
+
<textarea
|
|
810
|
+
className="h-20 rounded-lg border border-[var(--border)] bg-[var(--panel)] px-3 py-2 text-xs md:col-span-2"
|
|
811
|
+
placeholder="Optional headers (one per line): Authorization: Bearer ... X-API-Key: ..."
|
|
812
|
+
value={connectionFormHeadersText}
|
|
813
|
+
onChange={(event) => setConnectionFormHeadersText(event.target.value)}
|
|
814
|
+
/>
|
|
815
|
+
</div>
|
|
816
|
+
{connectionStatus.form ? (
|
|
817
|
+
<div className="mt-2 text-xs text-[var(--danger)]">{connectionStatus.form}</div>
|
|
818
|
+
) : null}
|
|
819
|
+
<div className="mt-3 flex flex-wrap items-center gap-3">
|
|
820
|
+
<label className="flex items-center gap-2 text-sm text-[var(--text-secondary)]">
|
|
821
|
+
<input
|
|
822
|
+
type="checkbox"
|
|
823
|
+
checked={connectionFormSupportsTools}
|
|
824
|
+
onChange={(event) => setConnectionFormSupportsTools(event.target.checked)}
|
|
825
|
+
/>
|
|
826
|
+
Supports tool calls
|
|
827
|
+
</label>
|
|
828
|
+
<button
|
|
829
|
+
className="rounded-full border border-[var(--border)] bg-[var(--panel)] px-3 py-1 text-xs"
|
|
830
|
+
onClick={() => {
|
|
831
|
+
setConnectionFormKind("openai_compatible");
|
|
832
|
+
setConnectionFormName("Local OpenAI-compatible");
|
|
833
|
+
setConnectionFormBaseUrl("http://localhost:1234/v1");
|
|
834
|
+
}}
|
|
835
|
+
>
|
|
836
|
+
LM Studio / vLLM preset
|
|
837
|
+
</button>
|
|
838
|
+
<button
|
|
839
|
+
className="rounded-full border border-[var(--border)] bg-[var(--panel)] px-3 py-1 text-xs"
|
|
840
|
+
onClick={() => {
|
|
841
|
+
setConnectionFormKind("ollama");
|
|
842
|
+
setConnectionFormName("Ollama Local");
|
|
843
|
+
setConnectionFormBaseUrl("http://localhost:11434");
|
|
844
|
+
setConnectionFormSupportsTools(false);
|
|
845
|
+
}}
|
|
846
|
+
>
|
|
847
|
+
Ollama preset
|
|
848
|
+
</button>
|
|
849
|
+
</div>
|
|
850
|
+
<div className="mt-3 flex gap-2">
|
|
851
|
+
<button
|
|
852
|
+
className="rounded-full bg-[var(--accent)] px-4 py-2 text-xs text-white"
|
|
853
|
+
onClick={upsertConnectionFromForm}
|
|
854
|
+
>
|
|
855
|
+
{editingConnectionId ? "Update Connection" : "Add Connection"}
|
|
856
|
+
</button>
|
|
857
|
+
<button
|
|
858
|
+
className="rounded-full border border-[var(--border)] bg-[var(--panel)] px-4 py-2 text-xs"
|
|
859
|
+
onClick={resetConnectionForm}
|
|
860
|
+
>
|
|
861
|
+
Clear
|
|
862
|
+
</button>
|
|
863
|
+
</div>
|
|
864
|
+
</div>
|
|
865
|
+
</div>
|
|
866
|
+
) : null}
|
|
867
|
+
|
|
868
|
+
{activeTab === "local-tools" ? (
|
|
869
|
+
<div className="space-y-4 rounded-xl border border-[var(--border)] bg-[var(--panel-2)] p-4">
|
|
870
|
+
<label className="flex items-center gap-2 text-sm text-[var(--text-secondary)]">
|
|
871
|
+
<input
|
|
872
|
+
type="checkbox"
|
|
873
|
+
checked={toolsEnabled}
|
|
874
|
+
onChange={(event) => setToolsEnabled(event.target.checked)}
|
|
875
|
+
/>
|
|
876
|
+
Enable local computer tools
|
|
877
|
+
</label>
|
|
878
|
+
<div className="grid gap-3 md:grid-cols-2">
|
|
879
|
+
<div>
|
|
880
|
+
<label className="mb-2 block text-xs uppercase tracking-[0.2em] text-[var(--text-muted)]">
|
|
881
|
+
Safety Profile
|
|
882
|
+
</label>
|
|
883
|
+
<select
|
|
884
|
+
className="w-full rounded-lg border border-[var(--border)] bg-[var(--panel)] px-3 py-2 text-sm"
|
|
885
|
+
value={safetyProfile}
|
|
886
|
+
onChange={(event) => setSafetyProfile(event.target.value as SafetyProfile)}
|
|
887
|
+
>
|
|
888
|
+
<option value="strict">Strict</option>
|
|
889
|
+
<option value="balanced">Balanced</option>
|
|
890
|
+
<option value="auto">Auto</option>
|
|
891
|
+
</select>
|
|
892
|
+
</div>
|
|
893
|
+
<div>
|
|
894
|
+
<label className="mb-2 block text-xs uppercase tracking-[0.2em] text-[var(--text-muted)]">
|
|
895
|
+
Approval Mode
|
|
896
|
+
</label>
|
|
897
|
+
<select
|
|
898
|
+
className="w-full rounded-lg border border-[var(--border)] bg-[var(--panel)] px-3 py-2 text-sm"
|
|
899
|
+
value={approvalMode}
|
|
900
|
+
onChange={(event) => setApprovalMode(event.target.value as ApprovalMode)}
|
|
901
|
+
>
|
|
902
|
+
<option value="always_confirm_writes">Always confirm writes</option>
|
|
903
|
+
<option value="confirm_risky_only">Confirm risky only</option>
|
|
904
|
+
<option value="trusted_auto">Trusted auto</option>
|
|
905
|
+
</select>
|
|
906
|
+
</div>
|
|
907
|
+
</div>
|
|
908
|
+
<div>
|
|
909
|
+
<label className="mb-2 block text-xs uppercase tracking-[0.2em] text-[var(--text-muted)]">
|
|
910
|
+
Allowed Roots (one per line)
|
|
911
|
+
</label>
|
|
912
|
+
<textarea
|
|
913
|
+
className="h-24 w-full rounded-lg border border-[var(--border)] bg-[var(--panel)] px-3 py-2 text-xs"
|
|
914
|
+
value={allowedRootsText}
|
|
915
|
+
onChange={(event) => setAllowedRootsText(event.target.value)}
|
|
916
|
+
placeholder="/Users/evanalexander/zenith ~/Desktop"
|
|
917
|
+
/>
|
|
918
|
+
</div>
|
|
919
|
+
<div className="grid gap-2 md:grid-cols-2">
|
|
920
|
+
{[
|
|
921
|
+
{ label: "Enable Apple Notes tools", checked: enableNotes, setter: setEnableNotes },
|
|
922
|
+
{ label: "Enable app open/focus tools", checked: enableApps, setter: setEnableApps },
|
|
923
|
+
{ label: "Enable Numbers tools", checked: enableNumbers, setter: setEnableNumbers },
|
|
924
|
+
{ label: "Enable web search/open tools", checked: enableWeb, setter: setEnableWeb },
|
|
925
|
+
{ label: "Enable music controls", checked: enableMusic, setter: setEnableMusic },
|
|
926
|
+
{ label: "Enable calendar/reminders tools", checked: enableCalendar, setter: setEnableCalendar },
|
|
927
|
+
{ label: "Enable mail/messages tools", checked: enableMail, setter: setEnableMail },
|
|
928
|
+
{ label: "Enable workflow tool", checked: enableWorkflow, setter: setEnableWorkflow },
|
|
929
|
+
{ label: "Enable system controls", checked: enableSystem, setter: setEnableSystem },
|
|
930
|
+
{ label: "Dry-run only (no writes)", checked: dryRun, setter: setDryRun },
|
|
931
|
+
].map((item) => (
|
|
932
|
+
<label key={item.label} className="flex items-center gap-2 text-sm text-[var(--text-secondary)]">
|
|
933
|
+
<input
|
|
934
|
+
type="checkbox"
|
|
935
|
+
checked={item.checked}
|
|
936
|
+
onChange={(event) => item.setter(event.target.checked)}
|
|
937
|
+
/>
|
|
938
|
+
{item.label}
|
|
939
|
+
</label>
|
|
940
|
+
))}
|
|
941
|
+
</div>
|
|
942
|
+
<div>
|
|
943
|
+
<label className="mb-2 block text-xs uppercase tracking-[0.2em] text-[var(--text-muted)]">
|
|
944
|
+
Web Search Backend
|
|
945
|
+
</label>
|
|
946
|
+
<select
|
|
947
|
+
className="w-full rounded-lg border border-[var(--border)] bg-[var(--panel)] px-3 py-2 text-sm"
|
|
948
|
+
value={webSearchBackend}
|
|
949
|
+
onChange={(event) => setWebSearchBackend(event.target.value as WebSearchBackend)}
|
|
950
|
+
>
|
|
951
|
+
<option value="no_key">No-key (default)</option>
|
|
952
|
+
<option value="hybrid">Hybrid</option>
|
|
953
|
+
</select>
|
|
954
|
+
</div>
|
|
955
|
+
</div>
|
|
956
|
+
) : null}
|
|
957
|
+
|
|
958
|
+
{activeTab === "memory" ? (
|
|
959
|
+
<div className="space-y-4">
|
|
960
|
+
<div className="space-y-3 rounded-xl border border-[var(--border)] bg-[var(--panel-2)] p-4">
|
|
961
|
+
<label className="flex items-center gap-2 text-sm text-[var(--text-secondary)]">
|
|
962
|
+
<input
|
|
963
|
+
type="checkbox"
|
|
964
|
+
checked={memoryEnabled}
|
|
965
|
+
onChange={(event) => setMemoryEnabled(event.target.checked)}
|
|
966
|
+
/>
|
|
967
|
+
Enable memory personalization
|
|
968
|
+
</label>
|
|
969
|
+
<label className="flex items-center gap-2 text-sm text-[var(--text-secondary)]">
|
|
970
|
+
<input
|
|
971
|
+
type="checkbox"
|
|
972
|
+
checked={memoryAutoCapture}
|
|
973
|
+
onChange={(event) => setMemoryAutoCapture(event.target.checked)}
|
|
974
|
+
disabled={!memoryEnabled}
|
|
975
|
+
/>
|
|
976
|
+
Auto-capture stable memories
|
|
977
|
+
</label>
|
|
978
|
+
<label className="flex items-center gap-2 text-sm text-[var(--text-secondary)]">
|
|
979
|
+
<input
|
|
980
|
+
type="checkbox"
|
|
981
|
+
checked={memoryToolInfluence}
|
|
982
|
+
onChange={(event) => setMemoryToolInfluence(event.target.checked)}
|
|
983
|
+
disabled={!memoryEnabled}
|
|
984
|
+
/>
|
|
985
|
+
Allow memory to influence tool planning and fast-path aliases
|
|
986
|
+
</label>
|
|
987
|
+
<div className="text-xs text-[var(--text-muted)]">
|
|
988
|
+
Extraction mode: <span className="font-medium text-[var(--text-primary)]">Conservative</span>
|
|
989
|
+
</div>
|
|
990
|
+
</div>
|
|
991
|
+
|
|
992
|
+
<div className="space-y-3 rounded-xl border border-[var(--border)] bg-[var(--panel-2)] p-4">
|
|
993
|
+
<div className="flex items-center justify-between gap-3">
|
|
994
|
+
<div className="text-xs uppercase tracking-[0.2em] text-[var(--text-muted)]">
|
|
995
|
+
Saved Memory
|
|
996
|
+
</div>
|
|
997
|
+
<button
|
|
998
|
+
className="rounded-full border border-[var(--danger)]/40 bg-[var(--panel)] px-3 py-1 text-xs text-[var(--danger)]"
|
|
999
|
+
onClick={clearAllMemories}
|
|
1000
|
+
disabled={(memories?.length ?? 0) === 0}
|
|
1001
|
+
>
|
|
1002
|
+
Clear all
|
|
1003
|
+
</button>
|
|
1004
|
+
</div>
|
|
1005
|
+
<input
|
|
1006
|
+
className="w-full rounded-lg border border-[var(--border)] bg-[var(--panel)] px-3 py-2 text-sm"
|
|
1007
|
+
placeholder="Search memory entries"
|
|
1008
|
+
value={memorySearch}
|
|
1009
|
+
onChange={(event) => setMemorySearch(event.target.value)}
|
|
1010
|
+
/>
|
|
1011
|
+
<div className="max-h-64 space-y-2 overflow-y-auto pr-1">
|
|
1012
|
+
{filteredMemories.map((entry) => (
|
|
1013
|
+
<div
|
|
1014
|
+
key={entry.id}
|
|
1015
|
+
className="rounded-lg border border-[var(--border)] bg-[var(--panel)] px-3 py-2"
|
|
1016
|
+
>
|
|
1017
|
+
<div className="flex items-start justify-between gap-3">
|
|
1018
|
+
<div>
|
|
1019
|
+
<div className="text-sm font-medium text-[var(--text-primary)]">{entry.key}</div>
|
|
1020
|
+
<div className="mt-0.5 text-xs text-[var(--text-secondary)]">{entry.value}</div>
|
|
1021
|
+
<div className="mt-1 text-[11px] text-[var(--text-muted)]">
|
|
1022
|
+
{entry.kind} | {entry.scope}
|
|
1023
|
+
{entry.scope === "conversation" && entry.conversationId
|
|
1024
|
+
? ` | ${entry.conversationId}`
|
|
1025
|
+
: ""}
|
|
1026
|
+
{` | ${entry.source}`}
|
|
1027
|
+
</div>
|
|
1028
|
+
</div>
|
|
1029
|
+
<div className="flex shrink-0 gap-2">
|
|
1030
|
+
<button
|
|
1031
|
+
className="rounded-full border border-[var(--border)] bg-[var(--panel-2)] px-3 py-1 text-xs"
|
|
1032
|
+
onClick={() => loadMemoryIntoForm(entry)}
|
|
1033
|
+
>
|
|
1034
|
+
Edit
|
|
1035
|
+
</button>
|
|
1036
|
+
<button
|
|
1037
|
+
className="rounded-full border border-[var(--danger)]/40 bg-[var(--panel-2)] px-3 py-1 text-xs text-[var(--danger)]"
|
|
1038
|
+
onClick={() => {
|
|
1039
|
+
void deleteMemoryEntry(entry.id);
|
|
1040
|
+
}}
|
|
1041
|
+
>
|
|
1042
|
+
Delete
|
|
1043
|
+
</button>
|
|
1044
|
+
</div>
|
|
1045
|
+
</div>
|
|
1046
|
+
</div>
|
|
1047
|
+
))}
|
|
1048
|
+
{filteredMemories.length === 0 ? (
|
|
1049
|
+
<div className="rounded-lg border border-[var(--border)] bg-[var(--panel)] px-3 py-4 text-xs text-[var(--text-muted)]">
|
|
1050
|
+
No memory entries found.
|
|
1051
|
+
</div>
|
|
1052
|
+
) : null}
|
|
1053
|
+
</div>
|
|
1054
|
+
</div>
|
|
1055
|
+
|
|
1056
|
+
<div className="space-y-3 rounded-xl border border-[var(--border)] bg-[var(--panel-2)] p-4">
|
|
1057
|
+
<div className="text-xs uppercase tracking-[0.2em] text-[var(--text-muted)]">
|
|
1058
|
+
{editingMemoryId ? "Edit Memory Entry" : "Add Memory Entry"}
|
|
1059
|
+
</div>
|
|
1060
|
+
<div className="grid gap-3 md:grid-cols-2">
|
|
1061
|
+
<div>
|
|
1062
|
+
<label className="mb-2 block text-xs uppercase tracking-[0.2em] text-[var(--text-muted)]">
|
|
1063
|
+
Kind
|
|
1064
|
+
</label>
|
|
1065
|
+
<select
|
|
1066
|
+
className="w-full rounded-lg border border-[var(--border)] bg-[var(--panel)] px-3 py-2 text-sm"
|
|
1067
|
+
value={memoryFormKind}
|
|
1068
|
+
onChange={(event) => setMemoryFormKind(event.target.value as MemoryKind)}
|
|
1069
|
+
>
|
|
1070
|
+
<option value="profile">Profile</option>
|
|
1071
|
+
<option value="preference">Preference</option>
|
|
1072
|
+
<option value="person_alias">Person alias</option>
|
|
1073
|
+
<option value="music_alias">Music alias</option>
|
|
1074
|
+
<option value="note">Note</option>
|
|
1075
|
+
</select>
|
|
1076
|
+
</div>
|
|
1077
|
+
<div>
|
|
1078
|
+
<label className="mb-2 block text-xs uppercase tracking-[0.2em] text-[var(--text-muted)]">
|
|
1079
|
+
Scope
|
|
1080
|
+
</label>
|
|
1081
|
+
<select
|
|
1082
|
+
className="w-full rounded-lg border border-[var(--border)] bg-[var(--panel)] px-3 py-2 text-sm"
|
|
1083
|
+
value={memoryFormScope}
|
|
1084
|
+
onChange={(event) => setMemoryFormScope(event.target.value as MemoryScope)}
|
|
1085
|
+
>
|
|
1086
|
+
<option value="global">Global</option>
|
|
1087
|
+
<option value="conversation">Conversation</option>
|
|
1088
|
+
</select>
|
|
1089
|
+
</div>
|
|
1090
|
+
{memoryFormScope === "conversation" ? (
|
|
1091
|
+
<input
|
|
1092
|
+
className="rounded-lg border border-[var(--border)] bg-[var(--panel)] px-3 py-2 text-sm md:col-span-2"
|
|
1093
|
+
placeholder="Conversation ID"
|
|
1094
|
+
value={memoryFormConversationId}
|
|
1095
|
+
onChange={(event) => setMemoryFormConversationId(event.target.value)}
|
|
1096
|
+
/>
|
|
1097
|
+
) : null}
|
|
1098
|
+
<div>
|
|
1099
|
+
<label className="mb-2 block text-xs uppercase tracking-[0.2em] text-[var(--text-muted)]">
|
|
1100
|
+
Source
|
|
1101
|
+
</label>
|
|
1102
|
+
<select
|
|
1103
|
+
className="w-full rounded-lg border border-[var(--border)] bg-[var(--panel)] px-3 py-2 text-sm"
|
|
1104
|
+
value={memoryFormSource}
|
|
1105
|
+
onChange={(event) => setMemoryFormSource(event.target.value as MemorySource)}
|
|
1106
|
+
>
|
|
1107
|
+
<option value="manual">Manual</option>
|
|
1108
|
+
<option value="explicit">Explicit</option>
|
|
1109
|
+
<option value="auto">Auto</option>
|
|
1110
|
+
</select>
|
|
1111
|
+
</div>
|
|
1112
|
+
<input
|
|
1113
|
+
className="rounded-lg border border-[var(--border)] bg-[var(--panel)] px-3 py-2 text-sm md:col-span-2"
|
|
1114
|
+
placeholder="Memory key"
|
|
1115
|
+
value={memoryFormKey}
|
|
1116
|
+
onChange={(event) => setMemoryFormKey(event.target.value)}
|
|
1117
|
+
/>
|
|
1118
|
+
<textarea
|
|
1119
|
+
className="h-20 rounded-lg border border-[var(--border)] bg-[var(--panel)] px-3 py-2 text-sm md:col-span-2"
|
|
1120
|
+
placeholder="Memory value"
|
|
1121
|
+
value={memoryFormValue}
|
|
1122
|
+
onChange={(event) => setMemoryFormValue(event.target.value)}
|
|
1123
|
+
/>
|
|
1124
|
+
</div>
|
|
1125
|
+
{memoryStatus ? (
|
|
1126
|
+
<div className="text-xs text-[var(--danger)]">{memoryStatus}</div>
|
|
1127
|
+
) : null}
|
|
1128
|
+
<div className="flex gap-2">
|
|
1129
|
+
<button
|
|
1130
|
+
className="rounded-full bg-[var(--accent)] px-4 py-2 text-xs text-white"
|
|
1131
|
+
onClick={() => {
|
|
1132
|
+
void upsertMemoryEntry();
|
|
1133
|
+
}}
|
|
1134
|
+
>
|
|
1135
|
+
{editingMemoryId ? "Update Memory" : "Add Memory"}
|
|
1136
|
+
</button>
|
|
1137
|
+
<button
|
|
1138
|
+
className="rounded-full border border-[var(--border)] bg-[var(--panel)] px-4 py-2 text-xs"
|
|
1139
|
+
onClick={resetMemoryForm}
|
|
1140
|
+
>
|
|
1141
|
+
Clear
|
|
1142
|
+
</button>
|
|
1143
|
+
</div>
|
|
1144
|
+
</div>
|
|
1145
|
+
</div>
|
|
1146
|
+
) : null}
|
|
1147
|
+
|
|
1148
|
+
{activeTab === "appearance" ? (
|
|
1149
|
+
<div className="space-y-4">
|
|
1150
|
+
<div>
|
|
1151
|
+
<label className="mb-2 block text-xs uppercase tracking-[0.2em] text-[var(--text-muted)]">
|
|
1152
|
+
Theme
|
|
1153
|
+
</label>
|
|
1154
|
+
<select
|
|
1155
|
+
className="w-full rounded-lg border border-[var(--border)] bg-[var(--panel-2)] px-3 py-2 text-sm"
|
|
1156
|
+
value={theme}
|
|
1157
|
+
onChange={(event) => setTheme(event.target.value as "dark" | "light")}
|
|
1158
|
+
>
|
|
1159
|
+
<option value="dark">Dark</option>
|
|
1160
|
+
<option value="light">Light</option>
|
|
1161
|
+
</select>
|
|
1162
|
+
</div>
|
|
1163
|
+
<div>
|
|
1164
|
+
<label className="mb-2 block text-xs uppercase tracking-[0.2em] text-[var(--text-muted)]">
|
|
1165
|
+
Font
|
|
1166
|
+
</label>
|
|
1167
|
+
<select
|
|
1168
|
+
className="w-full rounded-lg border border-[var(--border)] bg-[var(--panel-2)] px-3 py-2 text-sm"
|
|
1169
|
+
value={font}
|
|
1170
|
+
onChange={(event) =>
|
|
1171
|
+
setFont(event.target.value as "ibm" | "manrope" | "sora" | "space" | "poppins")
|
|
1172
|
+
}
|
|
1173
|
+
>
|
|
1174
|
+
<option value="ibm">IBM Plex Sans (Default)</option>
|
|
1175
|
+
<option value="manrope">Manrope</option>
|
|
1176
|
+
<option value="poppins">Poppins</option>
|
|
1177
|
+
<option value="sora">Sora</option>
|
|
1178
|
+
<option value="space">Space Grotesk</option>
|
|
1179
|
+
</select>
|
|
1180
|
+
</div>
|
|
1181
|
+
<div>
|
|
1182
|
+
<label className="mb-2 block text-xs uppercase tracking-[0.2em] text-[var(--text-muted)]">
|
|
1183
|
+
Accent Color
|
|
1184
|
+
</label>
|
|
1185
|
+
<div className="grid grid-cols-4 gap-2">
|
|
1186
|
+
{accentPresets.map((preset) => (
|
|
1187
|
+
<button
|
|
1188
|
+
key={preset.id}
|
|
1189
|
+
className={`flex items-center gap-2 rounded-lg border px-3 py-2 text-xs ${
|
|
1190
|
+
accentColor === preset.color
|
|
1191
|
+
? "border-[var(--accent)] text-[var(--text-primary)]"
|
|
1192
|
+
: "border-[var(--border)] text-[var(--text-secondary)]"
|
|
1193
|
+
}`}
|
|
1194
|
+
onClick={() => setAccentColor(preset.color)}
|
|
1195
|
+
>
|
|
1196
|
+
<span className="h-3 w-3 rounded-full" style={{ background: preset.color }} />
|
|
1197
|
+
{preset.label}
|
|
1198
|
+
</button>
|
|
1199
|
+
))}
|
|
1200
|
+
</div>
|
|
1201
|
+
</div>
|
|
1202
|
+
</div>
|
|
1203
|
+
) : null}
|
|
1204
|
+
|
|
1205
|
+
{activeTab === "advanced" ? (
|
|
1206
|
+
<div className="space-y-4">
|
|
1207
|
+
<label className="flex items-center gap-2 rounded-xl border border-[var(--border)] bg-[var(--panel-2)] p-4 text-sm text-[var(--text-secondary)]">
|
|
1208
|
+
<input
|
|
1209
|
+
type="checkbox"
|
|
1210
|
+
checked={enableWebSources}
|
|
1211
|
+
onChange={(event) => setEnableWebSources(event.target.checked)}
|
|
1212
|
+
/>
|
|
1213
|
+
Enable OpenAI web sources (adds citations when web search is used)
|
|
1214
|
+
</label>
|
|
1215
|
+
<div className="rounded-xl border border-[var(--border)] bg-[var(--panel-2)] p-4 text-sm text-[var(--text-secondary)]">
|
|
1216
|
+
<div className="font-medium text-[var(--text-primary)]">Diagnostics</div>
|
|
1217
|
+
<div className="mt-2 text-xs">
|
|
1218
|
+
Connections: {connections.length} total, {enabledConnections.length} enabled
|
|
1219
|
+
</div>
|
|
1220
|
+
<div className="mt-1 text-xs">Default connection: {defaultConnectionId || "(none)"}</div>
|
|
1221
|
+
</div>
|
|
1222
|
+
<button
|
|
1223
|
+
className="inline-flex items-center gap-2 rounded-full border border-[var(--border)] bg-[var(--panel-2)] px-4 py-2 text-xs text-[var(--text-secondary)]"
|
|
1224
|
+
onClick={() => {
|
|
1225
|
+
const now = Date.now();
|
|
1226
|
+
setConnections(
|
|
1227
|
+
ensureBuiltinConnections([], { openaiKey, anthropicKey, geminiKey }, now),
|
|
1228
|
+
);
|
|
1229
|
+
setDefaultConnectionId(BUILTIN_CONNECTION_IDS.openai);
|
|
1230
|
+
setDefaultModelByConnection({});
|
|
1231
|
+
}}
|
|
1232
|
+
>
|
|
1233
|
+
<RefreshCw className="h-3.5 w-3.5" />
|
|
1234
|
+
Reset Connection Defaults
|
|
1235
|
+
</button>
|
|
1236
|
+
</div>
|
|
1237
|
+
) : null}
|
|
1238
|
+
</div>
|
|
1239
|
+
|
|
1240
|
+
<div className="mt-6 flex justify-end gap-2">
|
|
1241
|
+
<button
|
|
1242
|
+
onClick={onClose}
|
|
1243
|
+
className="rounded-full border border-[var(--border)] bg-[var(--panel-2)] px-4 py-2 text-xs"
|
|
1244
|
+
>
|
|
1245
|
+
Cancel
|
|
1246
|
+
</button>
|
|
1247
|
+
<button
|
|
1248
|
+
className="rounded-full bg-[var(--accent)] px-4 py-2 text-xs text-white"
|
|
1249
|
+
onClick={handleSave}
|
|
1250
|
+
>
|
|
1251
|
+
Save
|
|
1252
|
+
</button>
|
|
1253
|
+
</div>
|
|
1254
|
+
</div>
|
|
1255
|
+
</div>
|
|
1256
|
+
);
|
|
1257
|
+
}
|