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,324 @@
|
|
|
1
|
+
import { nanoid } from "nanoid";
|
|
2
|
+
import { db } from "./db";
|
|
3
|
+
import type {
|
|
4
|
+
LocalToolsSettings,
|
|
5
|
+
MemorySettings,
|
|
6
|
+
MessageNode,
|
|
7
|
+
Settings,
|
|
8
|
+
Thread,
|
|
9
|
+
} from "./types";
|
|
10
|
+
import { DEFAULT_LOCAL_TOOLS_SETTINGS, DEFAULT_MEMORY_SETTINGS } from "./types";
|
|
11
|
+
import { deriveTitle } from "./utils";
|
|
12
|
+
import { BUILTIN_CONNECTION_IDS, migrateLegacyConnections } from "./connections";
|
|
13
|
+
|
|
14
|
+
const DEFAULT_SETTINGS: Settings = {
|
|
15
|
+
id: "settings",
|
|
16
|
+
openaiKey: undefined,
|
|
17
|
+
anthropicKey: undefined,
|
|
18
|
+
geminiKey: undefined,
|
|
19
|
+
defaultProvider: "openai",
|
|
20
|
+
defaultModel: "gpt-5.2",
|
|
21
|
+
connections: [],
|
|
22
|
+
defaultConnectionId: BUILTIN_CONNECTION_IDS.openai,
|
|
23
|
+
defaultModelByConnection: {},
|
|
24
|
+
showExtendedOpenAIModels: false,
|
|
25
|
+
enableWebSources: true,
|
|
26
|
+
accentColor: "#66706e",
|
|
27
|
+
font: "manrope",
|
|
28
|
+
theme: "dark",
|
|
29
|
+
localTools: { ...DEFAULT_LOCAL_TOOLS_SETTINGS },
|
|
30
|
+
memory: { ...DEFAULT_MEMORY_SETTINGS },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const LEGACY_DEFAULT_LOCAL_TOOLS_SETTINGS: LocalToolsSettings = {
|
|
34
|
+
...DEFAULT_LOCAL_TOOLS_SETTINGS,
|
|
35
|
+
approvalMode: "always_confirm_writes",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function sameStringArray(a: string[], b: readonly string[]): boolean {
|
|
39
|
+
if (a.length !== b.length) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
return a.every((value, index) => value === b[index]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isLegacyDefaultLocalToolsSettings(localTools: LocalToolsSettings): boolean {
|
|
46
|
+
return (
|
|
47
|
+
localTools.enabled === LEGACY_DEFAULT_LOCAL_TOOLS_SETTINGS.enabled &&
|
|
48
|
+
localTools.approvalMode === LEGACY_DEFAULT_LOCAL_TOOLS_SETTINGS.approvalMode &&
|
|
49
|
+
localTools.safetyProfile === LEGACY_DEFAULT_LOCAL_TOOLS_SETTINGS.safetyProfile &&
|
|
50
|
+
sameStringArray(localTools.allowedRoots, LEGACY_DEFAULT_LOCAL_TOOLS_SETTINGS.allowedRoots) &&
|
|
51
|
+
localTools.enableNotes === LEGACY_DEFAULT_LOCAL_TOOLS_SETTINGS.enableNotes &&
|
|
52
|
+
localTools.enableApps === LEGACY_DEFAULT_LOCAL_TOOLS_SETTINGS.enableApps &&
|
|
53
|
+
localTools.enableNumbers === LEGACY_DEFAULT_LOCAL_TOOLS_SETTINGS.enableNumbers &&
|
|
54
|
+
localTools.enableWeb === LEGACY_DEFAULT_LOCAL_TOOLS_SETTINGS.enableWeb &&
|
|
55
|
+
localTools.enableMusic === LEGACY_DEFAULT_LOCAL_TOOLS_SETTINGS.enableMusic &&
|
|
56
|
+
localTools.enableCalendar === LEGACY_DEFAULT_LOCAL_TOOLS_SETTINGS.enableCalendar &&
|
|
57
|
+
localTools.enableMail === LEGACY_DEFAULT_LOCAL_TOOLS_SETTINGS.enableMail &&
|
|
58
|
+
localTools.enableWorkflow === LEGACY_DEFAULT_LOCAL_TOOLS_SETTINGS.enableWorkflow &&
|
|
59
|
+
localTools.enableSystem === LEGACY_DEFAULT_LOCAL_TOOLS_SETTINGS.enableSystem &&
|
|
60
|
+
localTools.webSearchBackend === LEGACY_DEFAULT_LOCAL_TOOLS_SETTINGS.webSearchBackend &&
|
|
61
|
+
localTools.dryRun === LEGACY_DEFAULT_LOCAL_TOOLS_SETTINGS.dryRun
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizeLocalToolsSettings(
|
|
66
|
+
localTools: LocalToolsSettings | undefined,
|
|
67
|
+
): LocalToolsSettings {
|
|
68
|
+
if (!localTools) {
|
|
69
|
+
return { ...DEFAULT_LOCAL_TOOLS_SETTINGS };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const normalized: LocalToolsSettings = {
|
|
73
|
+
enabled:
|
|
74
|
+
typeof localTools.enabled === "boolean"
|
|
75
|
+
? localTools.enabled
|
|
76
|
+
: DEFAULT_LOCAL_TOOLS_SETTINGS.enabled,
|
|
77
|
+
approvalMode:
|
|
78
|
+
localTools.approvalMode ?? DEFAULT_LOCAL_TOOLS_SETTINGS.approvalMode,
|
|
79
|
+
safetyProfile:
|
|
80
|
+
localTools.safetyProfile ?? DEFAULT_LOCAL_TOOLS_SETTINGS.safetyProfile,
|
|
81
|
+
allowedRoots:
|
|
82
|
+
Array.isArray(localTools.allowedRoots) && localTools.allowedRoots.length > 0
|
|
83
|
+
? localTools.allowedRoots.filter((root) => typeof root === "string" && root.trim())
|
|
84
|
+
: [...DEFAULT_LOCAL_TOOLS_SETTINGS.allowedRoots],
|
|
85
|
+
enableNotes:
|
|
86
|
+
typeof localTools.enableNotes === "boolean"
|
|
87
|
+
? localTools.enableNotes
|
|
88
|
+
: DEFAULT_LOCAL_TOOLS_SETTINGS.enableNotes,
|
|
89
|
+
enableApps:
|
|
90
|
+
typeof localTools.enableApps === "boolean"
|
|
91
|
+
? localTools.enableApps
|
|
92
|
+
: DEFAULT_LOCAL_TOOLS_SETTINGS.enableApps,
|
|
93
|
+
enableNumbers:
|
|
94
|
+
typeof localTools.enableNumbers === "boolean"
|
|
95
|
+
? localTools.enableNumbers
|
|
96
|
+
: DEFAULT_LOCAL_TOOLS_SETTINGS.enableNumbers,
|
|
97
|
+
enableWeb:
|
|
98
|
+
typeof localTools.enableWeb === "boolean"
|
|
99
|
+
? localTools.enableWeb
|
|
100
|
+
: DEFAULT_LOCAL_TOOLS_SETTINGS.enableWeb,
|
|
101
|
+
enableMusic:
|
|
102
|
+
typeof localTools.enableMusic === "boolean"
|
|
103
|
+
? localTools.enableMusic
|
|
104
|
+
: DEFAULT_LOCAL_TOOLS_SETTINGS.enableMusic,
|
|
105
|
+
enableCalendar:
|
|
106
|
+
typeof localTools.enableCalendar === "boolean"
|
|
107
|
+
? localTools.enableCalendar
|
|
108
|
+
: DEFAULT_LOCAL_TOOLS_SETTINGS.enableCalendar,
|
|
109
|
+
enableMail:
|
|
110
|
+
typeof localTools.enableMail === "boolean"
|
|
111
|
+
? localTools.enableMail
|
|
112
|
+
: DEFAULT_LOCAL_TOOLS_SETTINGS.enableMail,
|
|
113
|
+
enableWorkflow:
|
|
114
|
+
typeof localTools.enableWorkflow === "boolean"
|
|
115
|
+
? localTools.enableWorkflow
|
|
116
|
+
: DEFAULT_LOCAL_TOOLS_SETTINGS.enableWorkflow,
|
|
117
|
+
enableSystem:
|
|
118
|
+
typeof localTools.enableSystem === "boolean"
|
|
119
|
+
? localTools.enableSystem
|
|
120
|
+
: DEFAULT_LOCAL_TOOLS_SETTINGS.enableSystem,
|
|
121
|
+
webSearchBackend:
|
|
122
|
+
localTools.webSearchBackend ?? DEFAULT_LOCAL_TOOLS_SETTINGS.webSearchBackend,
|
|
123
|
+
dryRun:
|
|
124
|
+
typeof localTools.dryRun === "boolean"
|
|
125
|
+
? localTools.dryRun
|
|
126
|
+
: DEFAULT_LOCAL_TOOLS_SETTINGS.dryRun,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Migrate legacy defaults so approvals are opt-in by mode instead of always-on.
|
|
130
|
+
if (isLegacyDefaultLocalToolsSettings(normalized)) {
|
|
131
|
+
normalized.approvalMode = DEFAULT_LOCAL_TOOLS_SETTINGS.approvalMode;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return normalized;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function normalizeMemorySettings(memory: MemorySettings | undefined): MemorySettings {
|
|
138
|
+
return {
|
|
139
|
+
enabled:
|
|
140
|
+
typeof memory?.enabled === "boolean"
|
|
141
|
+
? memory.enabled
|
|
142
|
+
: DEFAULT_MEMORY_SETTINGS.enabled,
|
|
143
|
+
autoCapture:
|
|
144
|
+
typeof memory?.autoCapture === "boolean"
|
|
145
|
+
? memory.autoCapture
|
|
146
|
+
: DEFAULT_MEMORY_SETTINGS.autoCapture,
|
|
147
|
+
toolInfluence:
|
|
148
|
+
typeof memory?.toolInfluence === "boolean"
|
|
149
|
+
? memory.toolInfluence
|
|
150
|
+
: DEFAULT_MEMORY_SETTINGS.toolInfluence,
|
|
151
|
+
extractionMode: "conservative",
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function ensureDefaults() {
|
|
156
|
+
const settings = await db.settings.get("settings");
|
|
157
|
+
if (!settings) {
|
|
158
|
+
const migrated = migrateLegacyConnections(DEFAULT_SETTINGS);
|
|
159
|
+
await db.settings.put({
|
|
160
|
+
...DEFAULT_SETTINGS,
|
|
161
|
+
...migrated,
|
|
162
|
+
});
|
|
163
|
+
} else {
|
|
164
|
+
const updates: Partial<Settings> = {};
|
|
165
|
+
if (!settings.accentColor) {
|
|
166
|
+
updates.accentColor = DEFAULT_SETTINGS.accentColor;
|
|
167
|
+
}
|
|
168
|
+
if (!settings.font) {
|
|
169
|
+
updates.font = DEFAULT_SETTINGS.font;
|
|
170
|
+
}
|
|
171
|
+
if (!settings.theme) {
|
|
172
|
+
updates.theme = DEFAULT_SETTINGS.theme;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const normalizedLocalTools = normalizeLocalToolsSettings(settings.localTools);
|
|
176
|
+
const localToolsChanged = JSON.stringify(normalizedLocalTools) !== JSON.stringify(settings.localTools);
|
|
177
|
+
if (localToolsChanged) {
|
|
178
|
+
updates.localTools = normalizedLocalTools;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const normalizedMemory = normalizeMemorySettings(settings.memory);
|
|
182
|
+
const memoryChanged = JSON.stringify(normalizedMemory) !== JSON.stringify(settings.memory);
|
|
183
|
+
if (memoryChanged) {
|
|
184
|
+
updates.memory = normalizedMemory;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const migratedConnections = migrateLegacyConnections(settings);
|
|
188
|
+
if (JSON.stringify(settings.connections ?? []) !== JSON.stringify(migratedConnections.connections)) {
|
|
189
|
+
updates.connections = migratedConnections.connections;
|
|
190
|
+
}
|
|
191
|
+
if (settings.defaultConnectionId !== migratedConnections.defaultConnectionId) {
|
|
192
|
+
updates.defaultConnectionId = migratedConnections.defaultConnectionId;
|
|
193
|
+
}
|
|
194
|
+
if (
|
|
195
|
+
JSON.stringify(settings.defaultModelByConnection ?? {}) !==
|
|
196
|
+
JSON.stringify(migratedConnections.defaultModelByConnection)
|
|
197
|
+
) {
|
|
198
|
+
updates.defaultModelByConnection = migratedConnections.defaultModelByConnection;
|
|
199
|
+
}
|
|
200
|
+
if (!settings.defaultProvider) {
|
|
201
|
+
updates.defaultProvider = "openai";
|
|
202
|
+
}
|
|
203
|
+
if (!settings.defaultModel) {
|
|
204
|
+
updates.defaultModel = "gpt-5.2";
|
|
205
|
+
}
|
|
206
|
+
if (typeof settings.showExtendedOpenAIModels !== "boolean") {
|
|
207
|
+
updates.showExtendedOpenAIModels = false;
|
|
208
|
+
}
|
|
209
|
+
if (typeof settings.enableWebSources !== "boolean") {
|
|
210
|
+
updates.enableWebSources = true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (Object.keys(updates).length > 0) {
|
|
214
|
+
await db.settings.update("settings", updates);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const threadCount = await db.threads.count();
|
|
219
|
+
if (threadCount === 0) {
|
|
220
|
+
await createNewThread();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export async function createNewThread() {
|
|
225
|
+
const now = Date.now();
|
|
226
|
+
const conversationId = nanoid();
|
|
227
|
+
const thread: Thread = {
|
|
228
|
+
id: nanoid(),
|
|
229
|
+
conversationId,
|
|
230
|
+
headMessageId: null,
|
|
231
|
+
title: "New chat",
|
|
232
|
+
forkedFromMessageId: null,
|
|
233
|
+
updatedAt: now,
|
|
234
|
+
};
|
|
235
|
+
await db.threads.add(thread);
|
|
236
|
+
return thread;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export async function createThreadFromMessage(messageId: string, conversationId: string) {
|
|
240
|
+
const existing = await db.threads
|
|
241
|
+
.where("forkedFromMessageId")
|
|
242
|
+
.equals(messageId)
|
|
243
|
+
.toArray();
|
|
244
|
+
const count = existing.filter(
|
|
245
|
+
(thread) => thread.conversationId === conversationId
|
|
246
|
+
).length;
|
|
247
|
+
// Base path is Thread 1, so only 4 additional forked threads are allowed.
|
|
248
|
+
if (count >= 4) {
|
|
249
|
+
throw new Error("Maximum of 5 threads reached for this output.");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const now = Date.now();
|
|
253
|
+
const thread: Thread = {
|
|
254
|
+
id: nanoid(),
|
|
255
|
+
conversationId,
|
|
256
|
+
headMessageId: messageId,
|
|
257
|
+
title: `Thread ${count + 2}`,
|
|
258
|
+
forkedFromMessageId: messageId,
|
|
259
|
+
updatedAt: now,
|
|
260
|
+
};
|
|
261
|
+
await db.threads.add(thread);
|
|
262
|
+
return thread;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export async function deleteThread(threadId: string) {
|
|
266
|
+
await db.threads.delete(threadId);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export async function deleteConversation(conversationId: string) {
|
|
270
|
+
await db.transaction(
|
|
271
|
+
"rw",
|
|
272
|
+
[db.threads, db.messages, db.toolEvents, db.toolApprovals, db.memories],
|
|
273
|
+
async () => {
|
|
274
|
+
await db.threads.where("conversationId").equals(conversationId).delete();
|
|
275
|
+
await db.messages.where("conversationId").equals(conversationId).delete();
|
|
276
|
+
await db.toolEvents.where("conversationId").equals(conversationId).delete();
|
|
277
|
+
await db.toolApprovals.where("conversationId").equals(conversationId).delete();
|
|
278
|
+
await db.memories.where("conversationId").equals(conversationId).delete();
|
|
279
|
+
},
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export async function appendUserAndAssistant(params: {
|
|
284
|
+
thread: Thread;
|
|
285
|
+
content: string;
|
|
286
|
+
provider: string;
|
|
287
|
+
model: string;
|
|
288
|
+
}) {
|
|
289
|
+
const { thread, content, provider, model } = params;
|
|
290
|
+
const now = Date.now();
|
|
291
|
+
const userId = nanoid();
|
|
292
|
+
const assistantId = nanoid();
|
|
293
|
+
|
|
294
|
+
const userMessage: MessageNode = {
|
|
295
|
+
id: userId,
|
|
296
|
+
conversationId: thread.conversationId,
|
|
297
|
+
parentId: thread.headMessageId,
|
|
298
|
+
role: "user",
|
|
299
|
+
content,
|
|
300
|
+
createdAt: now,
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const assistantMessage: MessageNode = {
|
|
304
|
+
id: assistantId,
|
|
305
|
+
conversationId: thread.conversationId,
|
|
306
|
+
parentId: userId,
|
|
307
|
+
role: "assistant",
|
|
308
|
+
content: "",
|
|
309
|
+
createdAt: now + 1,
|
|
310
|
+
provider,
|
|
311
|
+
model,
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
await db.messages.bulkAdd([userMessage, assistantMessage]);
|
|
315
|
+
|
|
316
|
+
const title = thread.title === "New chat" ? deriveTitle(content) : thread.title;
|
|
317
|
+
await db.threads.update(thread.id, {
|
|
318
|
+
headMessageId: assistantId,
|
|
319
|
+
updatedAt: Date.now(),
|
|
320
|
+
title,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
return { userMessage, assistantMessage };
|
|
324
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import Dexie, { Table } from "dexie";
|
|
2
|
+
import type { MemoryEntry, MessageNode, Settings, Thread, ToolApproval, ToolEvent } from "./types";
|
|
3
|
+
|
|
4
|
+
class ZenithDB extends Dexie {
|
|
5
|
+
messages!: Table<MessageNode, string>;
|
|
6
|
+
threads!: Table<Thread, string>;
|
|
7
|
+
settings!: Table<Settings, string>;
|
|
8
|
+
toolEvents!: Table<ToolEvent, string>;
|
|
9
|
+
toolApprovals!: Table<ToolApproval, string>;
|
|
10
|
+
memories!: Table<MemoryEntry, string>;
|
|
11
|
+
|
|
12
|
+
constructor() {
|
|
13
|
+
super("zenith-chat");
|
|
14
|
+
this.version(1).stores({
|
|
15
|
+
messages: "id, conversationId, parentId, createdAt",
|
|
16
|
+
threads: "id, conversationId, headMessageId, updatedAt",
|
|
17
|
+
settings: "id",
|
|
18
|
+
});
|
|
19
|
+
this.version(2).stores({
|
|
20
|
+
messages: "id, conversationId, parentId, createdAt",
|
|
21
|
+
threads: "id, conversationId, headMessageId, forkedFromMessageId, updatedAt",
|
|
22
|
+
settings: "id",
|
|
23
|
+
});
|
|
24
|
+
this.version(3).stores({
|
|
25
|
+
messages: "id, conversationId, parentId, createdAt",
|
|
26
|
+
threads: "id, conversationId, headMessageId, forkedFromMessageId, updatedAt",
|
|
27
|
+
settings: "id",
|
|
28
|
+
toolEvents: "id, conversationId, threadId, assistantMessageId, toolCallId, createdAt",
|
|
29
|
+
toolApprovals: "id, conversationId, threadId, assistantMessageId, toolCallId, status, requestedAt",
|
|
30
|
+
});
|
|
31
|
+
this.version(4).stores({
|
|
32
|
+
messages: "id, conversationId, parentId, createdAt",
|
|
33
|
+
threads: "id, conversationId, headMessageId, forkedFromMessageId, updatedAt",
|
|
34
|
+
settings: "id",
|
|
35
|
+
toolEvents: "id, conversationId, threadId, assistantMessageId, toolCallId, createdAt",
|
|
36
|
+
toolApprovals: "id, conversationId, threadId, assistantMessageId, toolCallId, status, requestedAt",
|
|
37
|
+
});
|
|
38
|
+
this.version(5).stores({
|
|
39
|
+
messages: "id, conversationId, parentId, createdAt",
|
|
40
|
+
threads: "id, conversationId, headMessageId, forkedFromMessageId, updatedAt",
|
|
41
|
+
settings: "id",
|
|
42
|
+
toolEvents: "id, conversationId, threadId, assistantMessageId, toolCallId, createdAt",
|
|
43
|
+
toolApprovals: "id, conversationId, threadId, assistantMessageId, toolCallId, status, requestedAt",
|
|
44
|
+
memories: "id, kind, scope, conversationId, normalizedKey, updatedAt, lastUsedAt",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const db = new ZenithDB();
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { useLiveQuery } from "dexie-react-hooks";
|
|
2
|
+
import { db } from "./db";
|
|
3
|
+
import type { MemoryEntry, MessageNode, Settings, Thread, ToolApproval, ToolEvent } from "./types";
|
|
4
|
+
|
|
5
|
+
export function useThreads(): Thread[] | undefined {
|
|
6
|
+
return useLiveQuery<Thread[], Thread[]>(
|
|
7
|
+
() => db.threads.orderBy("updatedAt").reverse().toArray(),
|
|
8
|
+
[],
|
|
9
|
+
[] as Thread[]
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function useSettings(): Settings | null | undefined {
|
|
14
|
+
return useLiveQuery<Settings | null, Settings | null>(
|
|
15
|
+
() => db.settings.get("settings").then((value) => value ?? null),
|
|
16
|
+
[],
|
|
17
|
+
null
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function useConversationMessages(
|
|
22
|
+
conversationId: string | null
|
|
23
|
+
): MessageNode[] | undefined {
|
|
24
|
+
return useLiveQuery<MessageNode[], MessageNode[]>(
|
|
25
|
+
() =>
|
|
26
|
+
conversationId
|
|
27
|
+
? db.messages.where("conversationId").equals(conversationId).toArray()
|
|
28
|
+
: Promise.resolve([] as MessageNode[]),
|
|
29
|
+
[conversationId],
|
|
30
|
+
[] as MessageNode[]
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function useToolEvents(
|
|
35
|
+
conversationId: string | null
|
|
36
|
+
): ToolEvent[] | undefined {
|
|
37
|
+
return useLiveQuery<ToolEvent[], ToolEvent[]>(
|
|
38
|
+
() =>
|
|
39
|
+
conversationId
|
|
40
|
+
? db.toolEvents.where("conversationId").equals(conversationId).sortBy("createdAt")
|
|
41
|
+
: Promise.resolve([] as ToolEvent[]),
|
|
42
|
+
[conversationId],
|
|
43
|
+
[] as ToolEvent[]
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function useToolApprovals(
|
|
48
|
+
conversationId: string | null
|
|
49
|
+
): ToolApproval[] | undefined {
|
|
50
|
+
return useLiveQuery<ToolApproval[], ToolApproval[]>(
|
|
51
|
+
() =>
|
|
52
|
+
conversationId
|
|
53
|
+
? db.toolApprovals.where("conversationId").equals(conversationId).sortBy("requestedAt")
|
|
54
|
+
: Promise.resolve([] as ToolApproval[]),
|
|
55
|
+
[conversationId],
|
|
56
|
+
[] as ToolApproval[]
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function useMemories(
|
|
61
|
+
conversationId?: string | null
|
|
62
|
+
): MemoryEntry[] | undefined {
|
|
63
|
+
return useLiveQuery<MemoryEntry[], MemoryEntry[]>(
|
|
64
|
+
async () => {
|
|
65
|
+
const all = await db.memories.orderBy("updatedAt").reverse().toArray();
|
|
66
|
+
if (!conversationId) {
|
|
67
|
+
return all;
|
|
68
|
+
}
|
|
69
|
+
return all.filter(
|
|
70
|
+
(entry) => entry.scope === "global" || entry.conversationId === conversationId,
|
|
71
|
+
);
|
|
72
|
+
},
|
|
73
|
+
[conversationId],
|
|
74
|
+
[] as MemoryEntry[],
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { db } from "./db";
|
|
4
|
+
import type {
|
|
5
|
+
MemoryEntry,
|
|
6
|
+
MessageNode,
|
|
7
|
+
Settings,
|
|
8
|
+
Thread,
|
|
9
|
+
ToolApproval,
|
|
10
|
+
ToolEvent,
|
|
11
|
+
} from "./types";
|
|
12
|
+
|
|
13
|
+
export type SyncPayload = {
|
|
14
|
+
threads: Thread[];
|
|
15
|
+
messages: MessageNode[];
|
|
16
|
+
settings: Settings[];
|
|
17
|
+
toolEvents: ToolEvent[];
|
|
18
|
+
toolApprovals: ToolApproval[];
|
|
19
|
+
memories: MemoryEntry[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function isLocalhost(): boolean {
|
|
23
|
+
if (typeof window === "undefined") {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
const h = window.location.hostname;
|
|
27
|
+
return h === "localhost" || h === "127.0.0.1";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function exportAll(): Promise<SyncPayload> {
|
|
31
|
+
const [threads, messages, settings, toolEvents, toolApprovals, memories] =
|
|
32
|
+
await Promise.all([
|
|
33
|
+
db.threads.toArray(),
|
|
34
|
+
db.messages.toArray(),
|
|
35
|
+
db.settings.toArray(),
|
|
36
|
+
db.toolEvents.toArray(),
|
|
37
|
+
db.toolApprovals.toArray(),
|
|
38
|
+
db.memories.toArray(),
|
|
39
|
+
]);
|
|
40
|
+
return {
|
|
41
|
+
threads,
|
|
42
|
+
messages,
|
|
43
|
+
settings,
|
|
44
|
+
toolEvents,
|
|
45
|
+
toolApprovals,
|
|
46
|
+
memories,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function importAll(payload: SyncPayload): Promise<void> {
|
|
51
|
+
await db.transaction(
|
|
52
|
+
"rw",
|
|
53
|
+
[
|
|
54
|
+
db.threads,
|
|
55
|
+
db.messages,
|
|
56
|
+
db.settings,
|
|
57
|
+
db.toolEvents,
|
|
58
|
+
db.toolApprovals,
|
|
59
|
+
db.memories,
|
|
60
|
+
],
|
|
61
|
+
async () => {
|
|
62
|
+
await db.toolEvents.clear();
|
|
63
|
+
await db.toolApprovals.clear();
|
|
64
|
+
await db.memories.clear();
|
|
65
|
+
await db.messages.clear();
|
|
66
|
+
await db.threads.clear();
|
|
67
|
+
await db.settings.clear();
|
|
68
|
+
|
|
69
|
+
if (payload.settings.length > 0) {
|
|
70
|
+
await db.settings.bulkPut(payload.settings);
|
|
71
|
+
}
|
|
72
|
+
if (payload.threads.length > 0) {
|
|
73
|
+
await db.threads.bulkPut(payload.threads);
|
|
74
|
+
}
|
|
75
|
+
if (payload.messages.length > 0) {
|
|
76
|
+
await db.messages.bulkPut(payload.messages);
|
|
77
|
+
}
|
|
78
|
+
if (payload.toolEvents.length > 0) {
|
|
79
|
+
await db.toolEvents.bulkPut(payload.toolEvents);
|
|
80
|
+
}
|
|
81
|
+
if (payload.toolApprovals.length > 0) {
|
|
82
|
+
await db.toolApprovals.bulkPut(payload.toolApprovals);
|
|
83
|
+
}
|
|
84
|
+
if (payload.memories.length > 0) {
|
|
85
|
+
await db.memories.bulkPut(payload.memories);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function loadFromServer(): Promise<void> {
|
|
92
|
+
if (!isLocalhost()) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const res = await fetch("/api/local-sync");
|
|
96
|
+
if (!res.ok) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const data = (await res.json()) as SyncPayload;
|
|
100
|
+
const hasData =
|
|
101
|
+
data.threads?.length > 0 ||
|
|
102
|
+
data.messages?.length > 0 ||
|
|
103
|
+
(data.settings?.length ?? 0) > 0 ||
|
|
104
|
+
(data.toolEvents?.length ?? 0) > 0 ||
|
|
105
|
+
(data.toolApprovals?.length ?? 0) > 0 ||
|
|
106
|
+
(data.memories?.length ?? 0) > 0;
|
|
107
|
+
if (!hasData) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
await importAll({
|
|
111
|
+
threads: Array.isArray(data.threads) ? data.threads : [],
|
|
112
|
+
messages: Array.isArray(data.messages) ? data.messages : [],
|
|
113
|
+
settings: Array.isArray(data.settings) ? data.settings : [],
|
|
114
|
+
toolEvents: Array.isArray(data.toolEvents) ? data.toolEvents : [],
|
|
115
|
+
toolApprovals: Array.isArray(data.toolApprovals) ? data.toolApprovals : [],
|
|
116
|
+
memories: Array.isArray(data.memories) ? data.memories : [],
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const SAVE_DEBOUNCE_MS = 700;
|
|
121
|
+
let saveTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
122
|
+
|
|
123
|
+
function scheduleSave(): void {
|
|
124
|
+
if (!isLocalhost()) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (saveTimeout) {
|
|
128
|
+
clearTimeout(saveTimeout);
|
|
129
|
+
}
|
|
130
|
+
saveTimeout = setTimeout(async () => {
|
|
131
|
+
saveTimeout = null;
|
|
132
|
+
try {
|
|
133
|
+
const payload = await exportAll();
|
|
134
|
+
await fetch("/api/local-sync", {
|
|
135
|
+
method: "POST",
|
|
136
|
+
headers: { "Content-Type": "application/json" },
|
|
137
|
+
body: JSON.stringify(payload),
|
|
138
|
+
});
|
|
139
|
+
} catch {
|
|
140
|
+
// Ignore sync errors (e.g. server down, offline)
|
|
141
|
+
}
|
|
142
|
+
}, SAVE_DEBOUNCE_MS);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function saveToServer(): Promise<void> {
|
|
146
|
+
if (!isLocalhost()) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (saveTimeout) {
|
|
150
|
+
clearTimeout(saveTimeout);
|
|
151
|
+
saveTimeout = null;
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
const payload = await exportAll();
|
|
155
|
+
await fetch("/api/local-sync", {
|
|
156
|
+
method: "POST",
|
|
157
|
+
headers: { "Content-Type": "application/json" },
|
|
158
|
+
body: JSON.stringify(payload),
|
|
159
|
+
});
|
|
160
|
+
} catch {
|
|
161
|
+
// Ignore
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function registerTableHooks(): void {
|
|
166
|
+
const tables = [
|
|
167
|
+
db.threads,
|
|
168
|
+
db.messages,
|
|
169
|
+
db.settings,
|
|
170
|
+
db.toolEvents,
|
|
171
|
+
db.toolApprovals,
|
|
172
|
+
db.memories,
|
|
173
|
+
] as const;
|
|
174
|
+
for (const table of tables) {
|
|
175
|
+
table.hook("creating", () => {
|
|
176
|
+
scheduleSave();
|
|
177
|
+
});
|
|
178
|
+
table.hook("updating", () => {
|
|
179
|
+
scheduleSave();
|
|
180
|
+
});
|
|
181
|
+
table.hook("deleting", () => {
|
|
182
|
+
scheduleSave();
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function registerSyncHooks(): void {
|
|
188
|
+
if (!isLocalhost()) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
registerTableHooks();
|
|
192
|
+
}
|