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.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +49 -0
  3. package/bin/iris.mjs +267 -0
  4. package/package.json +61 -0
  5. package/template/LICENSE +21 -0
  6. package/template/README.md +49 -0
  7. package/template/eslint.config.mjs +18 -0
  8. package/template/next.config.ts +7 -0
  9. package/template/package-lock.json +9193 -0
  10. package/template/package.json +46 -0
  11. package/template/postcss.config.mjs +7 -0
  12. package/template/public/file.svg +1 -0
  13. package/template/public/globe.svg +1 -0
  14. package/template/public/next.svg +1 -0
  15. package/template/public/vercel.svg +1 -0
  16. package/template/public/window.svg +1 -0
  17. package/template/src/app/api/chat/route.ts +2445 -0
  18. package/template/src/app/api/connections/models/route.ts +255 -0
  19. package/template/src/app/api/connections/test/route.ts +124 -0
  20. package/template/src/app/api/local-sync/route.ts +74 -0
  21. package/template/src/app/api/tool-approval/route.ts +47 -0
  22. package/template/src/app/favicon.ico +0 -0
  23. package/template/src/app/globals.css +808 -0
  24. package/template/src/app/layout.tsx +74 -0
  25. package/template/src/app/page.tsx +444 -0
  26. package/template/src/components/ChatView.tsx +1537 -0
  27. package/template/src/components/Composer.tsx +160 -0
  28. package/template/src/components/MapView.tsx +244 -0
  29. package/template/src/components/MessageCard.tsx +955 -0
  30. package/template/src/components/SearchModal.tsx +72 -0
  31. package/template/src/components/SettingsModal.tsx +1257 -0
  32. package/template/src/components/Sidebar.tsx +153 -0
  33. package/template/src/components/TopBar.tsx +164 -0
  34. package/template/src/lib/connections.ts +275 -0
  35. package/template/src/lib/data.ts +324 -0
  36. package/template/src/lib/db.ts +49 -0
  37. package/template/src/lib/hooks.ts +76 -0
  38. package/template/src/lib/local-sync.ts +192 -0
  39. package/template/src/lib/memory.ts +695 -0
  40. package/template/src/lib/model-presets.ts +251 -0
  41. package/template/src/lib/store.ts +36 -0
  42. package/template/src/lib/tooling/approvals.ts +78 -0
  43. package/template/src/lib/tooling/providers/anthropic.ts +155 -0
  44. package/template/src/lib/tooling/providers/ollama.ts +73 -0
  45. package/template/src/lib/tooling/providers/openai.ts +267 -0
  46. package/template/src/lib/tooling/providers/openai_compatible.ts +16 -0
  47. package/template/src/lib/tooling/providers/types.ts +44 -0
  48. package/template/src/lib/tooling/registry.ts +103 -0
  49. package/template/src/lib/tooling/runtime.ts +189 -0
  50. package/template/src/lib/tooling/safety.ts +165 -0
  51. package/template/src/lib/tooling/tools/apps.ts +108 -0
  52. package/template/src/lib/tooling/tools/apps_plus.ts +153 -0
  53. package/template/src/lib/tooling/tools/communication.ts +883 -0
  54. package/template/src/lib/tooling/tools/files.ts +395 -0
  55. package/template/src/lib/tooling/tools/music.ts +988 -0
  56. package/template/src/lib/tooling/tools/notes.ts +461 -0
  57. package/template/src/lib/tooling/tools/notes_plus.ts +294 -0
  58. package/template/src/lib/tooling/tools/numbers.ts +175 -0
  59. package/template/src/lib/tooling/tools/schedule.ts +579 -0
  60. package/template/src/lib/tooling/tools/system.ts +142 -0
  61. package/template/src/lib/tooling/tools/web.ts +212 -0
  62. package/template/src/lib/tooling/tools/workflow.ts +218 -0
  63. package/template/src/lib/tooling/types.ts +27 -0
  64. package/template/src/lib/types.ts +309 -0
  65. package/template/src/lib/utils.ts +108 -0
  66. package/template/tsconfig.json +34 -0
@@ -0,0 +1,74 @@
1
+ import type { Metadata } from "next";
2
+ import {
3
+ IBM_Plex_Mono,
4
+ IBM_Plex_Sans,
5
+ Manrope,
6
+ Poppins,
7
+ Sora,
8
+ Space_Grotesk,
9
+ } from "next/font/google";
10
+ import "katex/dist/katex.min.css";
11
+ import "./globals.css";
12
+
13
+ const plexSans = IBM_Plex_Sans({
14
+ variable: "--font-sans",
15
+ subsets: ["latin"],
16
+ weight: ["400", "500", "600", "700"],
17
+ display: "swap",
18
+ });
19
+
20
+ const manrope = Manrope({
21
+ variable: "--font-manrope",
22
+ subsets: ["latin"],
23
+ weight: ["400", "500", "600", "700"],
24
+ display: "swap",
25
+ });
26
+
27
+ const sora = Sora({
28
+ variable: "--font-sora",
29
+ subsets: ["latin"],
30
+ weight: ["400", "500", "600", "700"],
31
+ display: "swap",
32
+ });
33
+
34
+ const poppins = Poppins({
35
+ variable: "--font-poppins",
36
+ subsets: ["latin"],
37
+ weight: ["400", "500", "600", "700"],
38
+ display: "swap",
39
+ });
40
+
41
+ const spaceGrotesk = Space_Grotesk({
42
+ variable: "--font-space",
43
+ subsets: ["latin"],
44
+ weight: ["400", "500", "600", "700"],
45
+ display: "swap",
46
+ });
47
+
48
+ const plexMono = IBM_Plex_Mono({
49
+ variable: "--font-mono",
50
+ subsets: ["latin"],
51
+ weight: ["400", "500"],
52
+ display: "swap",
53
+ });
54
+
55
+ export const metadata: Metadata = {
56
+ title: "Zenith Chat",
57
+ description: "Local ChatGPT-style client with branching threads",
58
+ };
59
+
60
+ export default function RootLayout({
61
+ children,
62
+ }: Readonly<{
63
+ children: React.ReactNode;
64
+ }>) {
65
+ return (
66
+ <html lang="en">
67
+ <body
68
+ className={`${plexSans.variable} ${manrope.variable} ${sora.variable} ${poppins.variable} ${spaceGrotesk.variable} ${plexMono.variable} antialiased`}
69
+ >
70
+ {children}
71
+ </body>
72
+ </html>
73
+ );
74
+ }
@@ -0,0 +1,444 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useMemo, useState, type CSSProperties } from "react";
4
+ import dynamic from "next/dynamic";
5
+ import Sidebar from "../components/Sidebar";
6
+ import TopBar from "../components/TopBar";
7
+ import ChatView from "../components/ChatView";
8
+ import SettingsModal from "../components/SettingsModal";
9
+ import SearchModal from "../components/SearchModal";
10
+ import {
11
+ ensureDefaults,
12
+ createNewThread,
13
+ deleteConversation,
14
+ deleteThread,
15
+ } from "../lib/data";
16
+ import { db } from "../lib/db";
17
+ import {
18
+ useConversationMessages,
19
+ useSettings,
20
+ useThreads,
21
+ useToolApprovals,
22
+ useToolEvents,
23
+ } from "../lib/hooks";
24
+ import { useUIStore } from "../lib/store";
25
+ import {
26
+ filterModelIdsForConnection,
27
+ getConnectionModelPresets,
28
+ sortModelIdsForConnection,
29
+ } from "../lib/model-presets";
30
+ import {
31
+ DEFAULT_LOCAL_TOOLS_SETTINGS,
32
+ } from "../lib/types";
33
+ import { getEnabledConnections, getConnectionById, toChatConnectionPayload } from "../lib/connections";
34
+ import { isLocalhost, loadFromServer, registerSyncHooks } from "../lib/local-sync";
35
+
36
+ const MapView = dynamic(() => import("../components/MapView"), {
37
+ ssr: false,
38
+ });
39
+
40
+ export default function Home() {
41
+ const threads = useThreads();
42
+ const settings = useSettings();
43
+
44
+ const viewMode = useUIStore((state) => state.viewMode);
45
+ const setViewMode = useUIStore((state) => state.setViewMode);
46
+ const activeThreadId = useUIStore((state) => state.activeThreadId);
47
+ const setActiveThreadId = useUIStore((state) => state.setActiveThreadId);
48
+ const settingsOpen = useUIStore((state) => state.settingsOpen);
49
+ const setSettingsOpen = useUIStore((state) => state.setSettingsOpen);
50
+ const sidebarCollapsed = useUIStore((state) => state.sidebarCollapsed);
51
+ const setSidebarCollapsed = useUIStore((state) => state.setSidebarCollapsed);
52
+ const [searchOpen, setSearchOpen] = useState(false);
53
+ const connectionOverrideId = useUIStore((state) => state.connectionOverrideId);
54
+ const modelOverride = useUIStore((state) => state.modelOverride);
55
+ const setConnectionOverrideId = useUIStore((state) => state.setConnectionOverrideId);
56
+ const setModelOverride = useUIStore((state) => state.setModelOverride);
57
+ const [remoteModelsByConnection, setRemoteModelsByConnection] = useState<Record<string, string[]>>({});
58
+
59
+ const ensureThreadSelection = useCallback(() => {
60
+ if (!threads || threads.length === 0) {
61
+ return null;
62
+ }
63
+
64
+ if (activeThreadId) {
65
+ const activeExists = threads.some((thread) => thread.id === activeThreadId);
66
+ return activeExists ? activeThreadId : null;
67
+ }
68
+
69
+ const selectedId = threads[0].id;
70
+ if (selectedId) {
71
+ setActiveThreadId(selectedId);
72
+ }
73
+
74
+ return selectedId;
75
+ }, [threads, activeThreadId, setActiveThreadId]);
76
+
77
+ useEffect(() => {
78
+ if (isLocalhost()) {
79
+ loadFromServer()
80
+ .then(() => ensureDefaults())
81
+ .then(() => registerSyncHooks());
82
+ } else {
83
+ ensureDefaults();
84
+ }
85
+ }, []);
86
+
87
+ useEffect(() => {
88
+ if (typeof window === "undefined") {
89
+ return;
90
+ }
91
+ const media = window.matchMedia("(max-width: 980px)");
92
+ const syncSidebarMode = () => {
93
+ if (media.matches) {
94
+ setSidebarCollapsed(true);
95
+ }
96
+ };
97
+ syncSidebarMode();
98
+ media.addEventListener("change", syncSidebarMode);
99
+ return () => {
100
+ media.removeEventListener("change", syncSidebarMode);
101
+ };
102
+ }, [setSidebarCollapsed]);
103
+
104
+ useEffect(() => {
105
+ ensureThreadSelection();
106
+ }, [ensureThreadSelection]);
107
+
108
+ useEffect(() => {
109
+ if (!settings) return;
110
+ if (!connectionOverrideId) {
111
+ setConnectionOverrideId(settings.defaultConnectionId);
112
+ }
113
+ if (settings.theme) {
114
+ document.documentElement.dataset.theme = settings.theme;
115
+ }
116
+ const fontVar =
117
+ settings.font === "manrope"
118
+ ? "var(--font-manrope)"
119
+ : settings.font === "poppins"
120
+ ? "var(--font-poppins)"
121
+ : settings.font === "sora"
122
+ ? "var(--font-sora)"
123
+ : settings.font === "space"
124
+ ? "var(--font-space)"
125
+ : "var(--font-sans)";
126
+ document.documentElement.style.setProperty("--app-font", fontVar);
127
+ document.body.style.fontFamily = fontVar;
128
+ }, [settings, connectionOverrideId, setConnectionOverrideId]);
129
+
130
+ const activeThread = useMemo(
131
+ () => threads?.find((thread) => thread.id === activeThreadId) || null,
132
+ [threads, activeThreadId]
133
+ );
134
+
135
+ const conversationMessages = useConversationMessages(
136
+ activeThread?.conversationId || null
137
+ );
138
+ const toolEvents = useToolEvents(activeThread?.conversationId || null);
139
+ const toolApprovals = useToolApprovals(activeThread?.conversationId || null);
140
+
141
+ const enabledConnections = useMemo(() => getEnabledConnections(settings ?? null), [settings]);
142
+ const connectionId =
143
+ connectionOverrideId ||
144
+ settings?.defaultConnectionId ||
145
+ enabledConnections[0]?.id ||
146
+ null;
147
+ const connection =
148
+ getConnectionById(settings ?? null, connectionId) ??
149
+ enabledConnections[0] ??
150
+ null;
151
+ const fallbackModelPresets = getConnectionModelPresets(connection);
152
+ const remoteModelPresets = connection ? remoteModelsByConnection[connection.id] ?? [] : [];
153
+ const includeExtendedOpenAI = Boolean(settings?.showExtendedOpenAIModels);
154
+ const sortedModelPresets = sortModelIdsForConnection(
155
+ connection,
156
+ remoteModelPresets.length > 0 ? remoteModelPresets : fallbackModelPresets,
157
+ );
158
+ const modelPresets = filterModelIdsForConnection({
159
+ connection,
160
+ modelIds: sortedModelPresets,
161
+ includeExtendedOpenAI,
162
+ });
163
+
164
+ useEffect(() => {
165
+ if (!settings || !connection) {
166
+ return;
167
+ }
168
+ let cancelled = false;
169
+ const payload = toChatConnectionPayload(connection, settings);
170
+
171
+ const loadModels = async () => {
172
+ try {
173
+ const response = await fetch("/api/connections/models", {
174
+ method: "POST",
175
+ headers: { "Content-Type": "application/json" },
176
+ body: JSON.stringify({ connection: payload }),
177
+ });
178
+ const body = (await response.json()) as { ok?: boolean; models?: string[] };
179
+ if (!response.ok || body.ok === false) {
180
+ throw new Error("Could not load models");
181
+ }
182
+ const models = Array.isArray(body.models)
183
+ ? [...new Set(body.models.map((value) => value.trim()).filter(Boolean))]
184
+ : [];
185
+ if (cancelled) {
186
+ return;
187
+ }
188
+ setRemoteModelsByConnection((current) => ({
189
+ ...current,
190
+ [connection.id]: models,
191
+ }));
192
+ } catch {
193
+ if (cancelled) {
194
+ return;
195
+ }
196
+ setRemoteModelsByConnection((current) => ({
197
+ ...current,
198
+ [connection.id]: [],
199
+ }));
200
+ }
201
+ };
202
+
203
+ void loadModels();
204
+ return () => {
205
+ cancelled = true;
206
+ };
207
+ }, [connection, settings]);
208
+ const mappedDefaultModel =
209
+ connection && settings?.defaultModelByConnection
210
+ ? settings.defaultModelByConnection[connection.id]
211
+ : undefined;
212
+ const preferredSupportedModel =
213
+ [modelOverride.trim(), mappedDefaultModel?.trim(), settings?.defaultModel?.trim()].find(
214
+ (value) => Boolean(value) && modelPresets.includes(value as string),
215
+ ) ?? "";
216
+ const model =
217
+ preferredSupportedModel ||
218
+ modelPresets[0] ||
219
+ modelOverride.trim() ||
220
+ mappedDefaultModel?.trim() ||
221
+ settings?.defaultModel?.trim() ||
222
+ "";
223
+ const handleConnectionChange = async (value: string) => {
224
+ const selected =
225
+ enabledConnections.find((item) => item.id === value) ??
226
+ settings?.connections.find((item) => item.id === value) ??
227
+ null;
228
+ if (!selected) {
229
+ return;
230
+ }
231
+ const options = remoteModelsByConnection[selected.id]?.length
232
+ ? remoteModelsByConnection[selected.id]
233
+ : getConnectionModelPresets(selected);
234
+ const filteredOptions = filterModelIdsForConnection({
235
+ connection: selected,
236
+ modelIds: sortModelIdsForConnection(selected, options),
237
+ includeExtendedOpenAI,
238
+ });
239
+ const persistedModel = settings?.defaultModelByConnection?.[selected.id];
240
+ const nextModel =
241
+ (persistedModel?.trim() && filteredOptions.includes(persistedModel.trim())
242
+ ? persistedModel.trim()
243
+ : "") ||
244
+ (model.trim() && filteredOptions.includes(model.trim()) ? model.trim() : "") ||
245
+ filteredOptions[0] ||
246
+ "";
247
+ setConnectionOverrideId(selected.id);
248
+ setModelOverride(nextModel);
249
+ const updated = await db.settings.update("settings", {
250
+ defaultConnectionId: selected.id,
251
+ defaultModel: nextModel,
252
+ defaultProvider: selected.provider ?? settings?.defaultProvider ?? "openai",
253
+ defaultModelByConnection: {
254
+ ...(settings?.defaultModelByConnection ?? {}),
255
+ [selected.id]: nextModel,
256
+ },
257
+ });
258
+ if (updated === 0) {
259
+ await db.settings.put({
260
+ id: "settings",
261
+ defaultProvider: selected.provider ?? "openai",
262
+ defaultModel: nextModel,
263
+ connections: settings?.connections ?? [],
264
+ defaultConnectionId: selected.id,
265
+ defaultModelByConnection: {
266
+ ...(settings?.defaultModelByConnection ?? {}),
267
+ [selected.id]: nextModel,
268
+ },
269
+ showExtendedOpenAIModels: settings?.showExtendedOpenAIModels ?? false,
270
+ enableWebSources: settings?.enableWebSources ?? true,
271
+ localTools: settings?.localTools ?? DEFAULT_LOCAL_TOOLS_SETTINGS,
272
+ });
273
+ }
274
+ };
275
+
276
+ const handleModelChange = async (value: string) => {
277
+ if (!connection) {
278
+ return;
279
+ }
280
+ setModelOverride(value);
281
+ const updated = await db.settings.update("settings", {
282
+ defaultProvider: connection.provider ?? settings?.defaultProvider ?? "openai",
283
+ defaultModel: value,
284
+ defaultConnectionId: connection.id,
285
+ defaultModelByConnection: {
286
+ ...(settings?.defaultModelByConnection ?? {}),
287
+ [connection.id]: value,
288
+ },
289
+ });
290
+ if (updated === 0) {
291
+ await db.settings.put({
292
+ id: "settings",
293
+ defaultProvider: connection.provider ?? "openai",
294
+ defaultModel: value,
295
+ connections: settings?.connections ?? [],
296
+ defaultConnectionId: connection.id,
297
+ defaultModelByConnection: {
298
+ ...(settings?.defaultModelByConnection ?? {}),
299
+ [connection.id]: value,
300
+ },
301
+ showExtendedOpenAIModels: settings?.showExtendedOpenAIModels ?? false,
302
+ enableWebSources: settings?.enableWebSources ?? true,
303
+ localTools: settings?.localTools ?? DEFAULT_LOCAL_TOOLS_SETTINGS,
304
+ });
305
+ }
306
+ };
307
+
308
+ const handleEnableLocalTools = async () => {
309
+ if (!settings) {
310
+ setSettingsOpen(true);
311
+ return;
312
+ }
313
+
314
+ const existing = settings.localTools ?? DEFAULT_LOCAL_TOOLS_SETTINGS;
315
+ await db.settings.update("settings", {
316
+ localTools: {
317
+ ...DEFAULT_LOCAL_TOOLS_SETTINGS,
318
+ ...existing,
319
+ enabled: true,
320
+ },
321
+ });
322
+ setSettingsOpen(true);
323
+ };
324
+
325
+ return (
326
+ <div
327
+ className={`chat-shell ${sidebarCollapsed ? "collapsed" : ""}`}
328
+ style={
329
+ {
330
+ "--accent": settings?.accentColor || "#66706e",
331
+ "--accent-2": settings?.accentColor || "#66706e",
332
+ } as CSSProperties
333
+ }
334
+ >
335
+ <Sidebar
336
+ threads={threads || []}
337
+ activeThreadId={activeThreadId}
338
+ collapsed={sidebarCollapsed}
339
+ onSelect={(id) => setActiveThreadId(id)}
340
+ onNewChat={async () => {
341
+ const thread = await createNewThread();
342
+ setViewMode("chat");
343
+ setActiveThreadId(thread.id);
344
+ }}
345
+ onDeleteThread={async (thread) => {
346
+ const allThreads = threads || [];
347
+ const deletedIds = new Set<string>();
348
+
349
+ if (!thread.forkedFromMessageId) {
350
+ allThreads
351
+ .filter((item) => item.conversationId === thread.conversationId)
352
+ .forEach((item) => deletedIds.add(item.id));
353
+ await deleteConversation(thread.conversationId);
354
+ } else {
355
+ deletedIds.add(thread.id);
356
+ await deleteThread(thread.id);
357
+ }
358
+
359
+ if (activeThreadId && deletedIds.has(activeThreadId)) {
360
+ const remaining = allThreads.filter((item) => !deletedIds.has(item.id));
361
+ if (remaining.length > 0) {
362
+ setActiveThreadId(remaining[0].id);
363
+ } else {
364
+ const newThread = await createNewThread();
365
+ setActiveThreadId(newThread.id);
366
+ }
367
+ }
368
+ }}
369
+ onToggleCollapse={() => setSidebarCollapsed(!sidebarCollapsed)}
370
+ onOpenSearch={() => setSearchOpen(true)}
371
+ />
372
+
373
+ <div className="flex h-full min-h-screen min-w-0 flex-col">
374
+ <TopBar
375
+ connectionId={connection?.id ?? ""}
376
+ connectionName={connection?.name ?? "No connection"}
377
+ connection={connection}
378
+ connections={enabledConnections}
379
+ model={model}
380
+ localToolsEnabled={Boolean(settings?.localTools?.enabled)}
381
+ modelPresets={modelPresets}
382
+ viewMode={viewMode}
383
+ onConnectionChange={handleConnectionChange}
384
+ onModelChange={handleModelChange}
385
+ onToggleView={() => {
386
+ const nextViewMode = viewMode === "chat" ? "map" : "chat";
387
+ if (nextViewMode === "chat") {
388
+ ensureThreadSelection();
389
+ }
390
+ setViewMode(nextViewMode);
391
+ }}
392
+ onEnableLocalTools={handleEnableLocalTools}
393
+ onOpenSettings={() => setSettingsOpen(true)}
394
+ />
395
+
396
+ <div className="flex-1 min-w-0">
397
+ {viewMode === "map" ? (
398
+ <MapView
399
+ threads={threads || []}
400
+ activeThreadId={activeThreadId}
401
+ messages={conversationMessages || []}
402
+ onSelectThread={(id) => setActiveThreadId(id)}
403
+ onNavigateToChat={() => {
404
+ ensureThreadSelection();
405
+ setViewMode("chat");
406
+ }}
407
+ />
408
+ ) : (
409
+ <ChatView
410
+ thread={activeThread}
411
+ messages={conversationMessages || []}
412
+ toolEvents={toolEvents || []}
413
+ toolApprovals={toolApprovals || []}
414
+ settings={settings ?? null}
415
+ connection={connection}
416
+ model={model}
417
+ threads={threads || []}
418
+ activeThreadId={activeThreadId}
419
+ onSelectThread={(id) => setActiveThreadId(id)}
420
+ onOpenSettings={() => setSettingsOpen(true)}
421
+ />
422
+ )}
423
+ </div>
424
+ </div>
425
+
426
+ {settingsOpen ? (
427
+ <SettingsModal
428
+ settings={settings ?? null}
429
+ onClose={() => setSettingsOpen(false)}
430
+ />
431
+ ) : null}
432
+ {searchOpen ? (
433
+ <SearchModal
434
+ threads={threads || []}
435
+ onClose={() => setSearchOpen(false)}
436
+ onSelect={(id) => {
437
+ setActiveThreadId(id);
438
+ setSearchOpen(false);
439
+ }}
440
+ />
441
+ ) : null}
442
+ </div>
443
+ );
444
+ }