iris-chatbot 4.1.0 → 5.0.0
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/package.json +1 -1
- package/template/docs/plan-projects-sidebar-ui.md +11 -0
- package/template/next-env.d.ts +1 -1
- package/template/package-lock.json +2 -2
- package/template/package.json +1 -1
- package/template/src/app/globals.css +98 -9
- package/template/src/app/page.tsx +20 -5
- package/template/src/components/MapView.tsx +21 -2
- package/template/src/components/MessageCard.tsx +1 -1
- package/template/src/components/SearchModal.tsx +1 -1
- package/template/src/components/SettingsModal.tsx +130 -58
- package/template/src/components/Sidebar.tsx +2 -4
- package/template/src/components/TopBar.tsx +4 -10
- package/template/src/lib/data.ts +5 -0
- package/template/src/lib/model-presets.ts +29 -0
package/package.json
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Projects plan – Sidebar UI addendum
|
|
2
|
+
|
|
3
|
+
When implementing the Sidebar for Projects, apply these UI details:
|
|
4
|
+
|
|
5
|
+
1. **Folder icon**
|
|
6
|
+
- **Remove** the folder icon from chat/thread rows (individual chats).
|
|
7
|
+
- **Add** the folder icon next to **project names** (each project in the project list, including "Inbox" if shown as a project).
|
|
8
|
+
|
|
9
|
+
2. **"Your Chats" label**
|
|
10
|
+
- Use the exact label **'Your Chats'** (title case).
|
|
11
|
+
- Do **not** render it in all caps: remove the `uppercase` class from the section heading (currently in Sidebar around line 86: `text-[11px] uppercase tracking-[0.18em]` → use the same size/tracking but drop `uppercase`) so it displays as **Your Chats**, not "YOUR CHATS".
|
package/template/next-env.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="next" />
|
|
2
2
|
/// <reference types="next/image-types/global" />
|
|
3
|
-
import "./.next/
|
|
3
|
+
import "./.next/types/routes.d.ts";
|
|
4
4
|
|
|
5
5
|
// NOTE: This file should not be edited
|
|
6
6
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iris",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "iris",
|
|
9
|
-
"version": "
|
|
9
|
+
"version": "5.0.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.72.1",
|
|
12
12
|
"clsx": "^2.1.1",
|
package/template/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
:root {
|
|
5
5
|
--topbar-height: 64px;
|
|
6
|
-
--bg: #
|
|
6
|
+
--bg: #212121;
|
|
7
7
|
--bg-alt: #1b1c1d;
|
|
8
8
|
--sidebar: #181818;
|
|
9
9
|
--sidebar-border: #202020;
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
--text-muted: #b1b7bf;
|
|
16
16
|
--accent: #10a37f;
|
|
17
17
|
--accent-2: #0e8e6f;
|
|
18
|
+
--accent-ring: var(--accent-2);
|
|
18
19
|
--danger: #ef4444;
|
|
19
20
|
--border: #2a2a2a;
|
|
20
21
|
--border-strong: #3a3a3a;
|
|
@@ -48,6 +49,21 @@ a {
|
|
|
48
49
|
color: var(--accent);
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
input:focus,
|
|
53
|
+
select:focus,
|
|
54
|
+
textarea:focus,
|
|
55
|
+
button:focus {
|
|
56
|
+
outline: none;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
input:focus-visible,
|
|
60
|
+
select:focus-visible,
|
|
61
|
+
textarea:focus-visible,
|
|
62
|
+
button:focus-visible {
|
|
63
|
+
outline: none;
|
|
64
|
+
box-shadow: inset 0 0 0 1px var(--border-strong);
|
|
65
|
+
}
|
|
66
|
+
|
|
51
67
|
.chat-shell {
|
|
52
68
|
display: grid;
|
|
53
69
|
grid-template-columns: 248px minmax(0, 1fr);
|
|
@@ -82,10 +98,21 @@ a {
|
|
|
82
98
|
background: transparent !important;
|
|
83
99
|
backdrop-filter: none;
|
|
84
100
|
box-shadow: none;
|
|
85
|
-
position:
|
|
101
|
+
position: absolute;
|
|
86
102
|
top: 0;
|
|
103
|
+
left: 0;
|
|
104
|
+
right: 0;
|
|
87
105
|
z-index: 40;
|
|
88
106
|
height: var(--topbar-height);
|
|
107
|
+
pointer-events: none;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.topbar > *,
|
|
111
|
+
.topbar button,
|
|
112
|
+
.topbar a,
|
|
113
|
+
.topbar [role="listbox"],
|
|
114
|
+
.topbar [role="option"] {
|
|
115
|
+
pointer-events: auto;
|
|
89
116
|
}
|
|
90
117
|
|
|
91
118
|
|
|
@@ -249,7 +276,7 @@ a {
|
|
|
249
276
|
}
|
|
250
277
|
|
|
251
278
|
.assistant-card .message-content {
|
|
252
|
-
color:
|
|
279
|
+
color: var(--text-primary);
|
|
253
280
|
}
|
|
254
281
|
|
|
255
282
|
.message-loading-spinner {
|
|
@@ -271,7 +298,7 @@ a {
|
|
|
271
298
|
.composer-bar {
|
|
272
299
|
position: sticky;
|
|
273
300
|
bottom: 0;
|
|
274
|
-
background:
|
|
301
|
+
background: var(--bg);
|
|
275
302
|
z-index: 30;
|
|
276
303
|
}
|
|
277
304
|
|
|
@@ -299,13 +326,14 @@ a {
|
|
|
299
326
|
--sidebar: #ffffff;
|
|
300
327
|
--sidebar-border: #e5e5e5;
|
|
301
328
|
--panel: #ffffff;
|
|
302
|
-
--panel-2: #
|
|
303
|
-
--panel-3: #
|
|
329
|
+
--panel-2: #f6f6f6;
|
|
330
|
+
--panel-3: #f0f0f0;
|
|
304
331
|
--text-primary: #111111;
|
|
305
332
|
--text-secondary: #4a4a4a;
|
|
306
333
|
--text-muted: #6f6f6f;
|
|
307
334
|
--accent: #0e8e6f;
|
|
308
335
|
--accent-2: #0b7a5f;
|
|
336
|
+
--accent-ring: color-mix(in srgb, var(--accent) 55%, #000);
|
|
309
337
|
--danger: #dc2626;
|
|
310
338
|
--border: #d5d5d5;
|
|
311
339
|
--border-strong: #bdbdbd;
|
|
@@ -313,6 +341,51 @@ a {
|
|
|
313
341
|
--user-bubble: #d7f2ea;
|
|
314
342
|
}
|
|
315
343
|
|
|
344
|
+
/* Dark mode: white button background (no data-theme or data-theme="dark") */
|
|
345
|
+
:root .settings-tab-active,
|
|
346
|
+
:root .settings-btn-accent {
|
|
347
|
+
background: white !important;
|
|
348
|
+
color: #111111 !important;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
[data-theme="dark"] .settings-tab-active,
|
|
352
|
+
[data-theme="dark"] .settings-btn-accent {
|
|
353
|
+
background: white !important;
|
|
354
|
+
color: #111111 !important;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/* Light mode: black for selected tab and Save button */
|
|
358
|
+
[data-theme="light"] .settings-tab-active,
|
|
359
|
+
[data-theme="light"] .settings-btn-accent {
|
|
360
|
+
background: #111111 !important;
|
|
361
|
+
color: #ffffff !important;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
[data-theme="light"] .message-content pre,
|
|
365
|
+
[data-theme="light"] .message-content pre code {
|
|
366
|
+
color: #e5e7eb;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
[data-theme="light"] .message-content code {
|
|
370
|
+
background: rgba(0, 0, 0, 0.06);
|
|
371
|
+
color: var(--text-primary);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
[data-theme="light"] .message-loading-spinner {
|
|
375
|
+
border-color: rgba(0, 0, 0, 0.15);
|
|
376
|
+
border-top-color: #111111;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
[data-theme="light"] .send-button.active {
|
|
380
|
+
background: #111111;
|
|
381
|
+
color: #ffffff;
|
|
382
|
+
border-color: transparent;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
[data-theme="light"] .send-button.active svg {
|
|
386
|
+
color: #ffffff;
|
|
387
|
+
}
|
|
388
|
+
|
|
316
389
|
.message-content h1,
|
|
317
390
|
.message-content h2,
|
|
318
391
|
.message-content h3,
|
|
@@ -506,12 +579,16 @@ a {
|
|
|
506
579
|
}
|
|
507
580
|
|
|
508
581
|
.composer {
|
|
509
|
-
background:
|
|
582
|
+
background: #303030;
|
|
510
583
|
border: 1px solid var(--border);
|
|
511
584
|
border-radius: 18px;
|
|
512
585
|
padding: 10px 12px;
|
|
513
586
|
}
|
|
514
587
|
|
|
588
|
+
[data-theme="light"] .composer {
|
|
589
|
+
background: transparent;
|
|
590
|
+
}
|
|
591
|
+
|
|
515
592
|
.thread-shelf {
|
|
516
593
|
margin-top: 12px;
|
|
517
594
|
padding-top: 8px;
|
|
@@ -582,10 +659,22 @@ a {
|
|
|
582
659
|
line-height: 1.4;
|
|
583
660
|
padding-top: 7px;
|
|
584
661
|
padding-bottom: 9px;
|
|
662
|
+
padding-left: 6px;
|
|
585
663
|
/* One line by default; height grows with content in Composer.tsx */
|
|
586
664
|
min-height: calc(1.4em + 7px + 9px);
|
|
587
665
|
}
|
|
588
666
|
|
|
667
|
+
.composer-textarea:focus,
|
|
668
|
+
.composer-textarea:focus-visible {
|
|
669
|
+
box-shadow: none;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
.search-modal-input:focus,
|
|
673
|
+
.search-modal-input:focus-visible {
|
|
674
|
+
outline: none;
|
|
675
|
+
box-shadow: none;
|
|
676
|
+
}
|
|
677
|
+
|
|
589
678
|
.send-button {
|
|
590
679
|
height: 40px;
|
|
591
680
|
width: 40px;
|
|
@@ -800,9 +889,9 @@ a {
|
|
|
800
889
|
}
|
|
801
890
|
|
|
802
891
|
.chat-scroll {
|
|
803
|
-
padding-top: calc(var(--topbar-height)
|
|
892
|
+
padding-top: calc(var(--topbar-height) + 24px);
|
|
804
893
|
}
|
|
805
894
|
|
|
806
895
|
.chat-scroll.empty {
|
|
807
|
-
padding-top: var(--topbar-height);
|
|
896
|
+
padding-top: calc(var(--topbar-height) + 24px);
|
|
808
897
|
}
|
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
createNewThread,
|
|
13
13
|
deleteConversation,
|
|
14
14
|
deleteThread,
|
|
15
|
+
DEFAULT_ACCENT_DARK,
|
|
16
|
+
DEFAULT_ACCENT_LIGHT,
|
|
15
17
|
} from "../lib/data";
|
|
16
18
|
import { db } from "../lib/db";
|
|
17
19
|
import {
|
|
@@ -331,10 +333,23 @@ export default function Home() {
|
|
|
331
333
|
<div
|
|
332
334
|
className={`chat-shell ${sidebarCollapsed ? "collapsed" : ""}`}
|
|
333
335
|
style={
|
|
334
|
-
{
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
336
|
+
(() => {
|
|
337
|
+
const stored = settings?.accentColor || DEFAULT_ACCENT_DARK;
|
|
338
|
+
const isDefaultGray = stored === DEFAULT_ACCENT_DARK;
|
|
339
|
+
const isLight =
|
|
340
|
+
settings?.theme === "light" ||
|
|
341
|
+
(typeof document !== "undefined" &&
|
|
342
|
+
document.documentElement.dataset.theme === "light");
|
|
343
|
+
const effective =
|
|
344
|
+
isDefaultGray && isLight ? DEFAULT_ACCENT_LIGHT : stored;
|
|
345
|
+
return {
|
|
346
|
+
"--accent": effective,
|
|
347
|
+
"--accent-2": effective,
|
|
348
|
+
...(isLight && {
|
|
349
|
+
"--accent-ring": "color-mix(in srgb, var(--accent) 55%, #000)",
|
|
350
|
+
}),
|
|
351
|
+
} as CSSProperties;
|
|
352
|
+
})()
|
|
338
353
|
}
|
|
339
354
|
>
|
|
340
355
|
<Sidebar
|
|
@@ -387,7 +402,7 @@ export default function Home() {
|
|
|
387
402
|
onOpenSearch={() => setSearchOpen(true)}
|
|
388
403
|
/>
|
|
389
404
|
|
|
390
|
-
<div className="flex h-screen min-w-0 flex-col overflow-hidden">
|
|
405
|
+
<div className="relative flex h-screen min-w-0 flex-col overflow-hidden">
|
|
391
406
|
<TopBar
|
|
392
407
|
connectionId={connection?.id ?? ""}
|
|
393
408
|
connectionName={connection?.name ?? "No connection"}
|
|
@@ -16,6 +16,21 @@ import { useUIStore } from "../lib/store";
|
|
|
16
16
|
const NODE_WIDTH = 220;
|
|
17
17
|
const NODE_HEIGHT = 80;
|
|
18
18
|
|
|
19
|
+
/** Strip markdown syntax for a short plain-text preview (e.g. "## Hi there" → "Hi there"). */
|
|
20
|
+
function stripMarkdownForPreview(text: string): string {
|
|
21
|
+
return text
|
|
22
|
+
.replace(/^#+\s*/m, "") // headings: ## Title -> Title
|
|
23
|
+
.replace(/\*\*([^*]+)\*\*/g, "$1") // **bold**
|
|
24
|
+
.replace(/\*([^*]+)\*/g, "$1") // *italic*
|
|
25
|
+
.replace(/_([^_]+)_/g, "$1") // _italic_
|
|
26
|
+
.replace(/`([^`]+)`/g, "$1") // `code`
|
|
27
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") // [link](url) -> link
|
|
28
|
+
.replace(/^[-*]\s+/gm, "") // list bullets
|
|
29
|
+
.replace(/^>\s*/gm, "") // blockquote
|
|
30
|
+
.replace(/\s+/g, " ")
|
|
31
|
+
.trim();
|
|
32
|
+
}
|
|
33
|
+
|
|
19
34
|
const graph = new dagre.graphlib.Graph();
|
|
20
35
|
graph.setDefaultEdgeLabel(() => ({}));
|
|
21
36
|
|
|
@@ -120,7 +135,10 @@ export default function MapView({
|
|
|
120
135
|
|
|
121
136
|
const { nodes, edges } = useMemo(() => {
|
|
122
137
|
const nodes: Node[] = visibleMessages.map((message) => {
|
|
123
|
-
const
|
|
138
|
+
const raw = splitContentAndSources(message.content).content.trim();
|
|
139
|
+
const preview = raw
|
|
140
|
+
? stripMarkdownForPreview(raw).slice(0, 60)
|
|
141
|
+
: "(empty)";
|
|
124
142
|
const isActive = activePathIds.has(message.id);
|
|
125
143
|
const roleClass =
|
|
126
144
|
message.role === "user"
|
|
@@ -169,6 +187,7 @@ export default function MapView({
|
|
|
169
187
|
padding: 0.2,
|
|
170
188
|
duration,
|
|
171
189
|
includeHiddenNodes: false,
|
|
190
|
+
minZoom: 0.85,
|
|
172
191
|
});
|
|
173
192
|
}, []);
|
|
174
193
|
|
|
@@ -198,7 +217,7 @@ export default function MapView({
|
|
|
198
217
|
nodes={nodes}
|
|
199
218
|
edges={edges}
|
|
200
219
|
fitView
|
|
201
|
-
fitViewOptions={{ padding: 0.2, includeHiddenNodes: false }}
|
|
220
|
+
fitViewOptions={{ padding: 0.2, includeHiddenNodes: false, minZoom: 0.85 }}
|
|
202
221
|
onInit={(instance) => {
|
|
203
222
|
flowRef.current = instance;
|
|
204
223
|
refitView(0);
|
|
@@ -1009,7 +1009,7 @@ function MessageCard({
|
|
|
1009
1009
|
{canEditThreads ? (
|
|
1010
1010
|
<button
|
|
1011
1011
|
className={`flex items-center gap-2 rounded-full border px-4 py-2 text-xs transition ${threadEditMode
|
|
1012
|
-
? "border-[var(--accent)] text-[var(--text-primary)]"
|
|
1012
|
+
? "border-[var(--accent-ring)] text-[var(--text-primary)]"
|
|
1013
1013
|
: "border-[var(--border)] text-[var(--text-muted)] hover:border-[var(--border-strong)] hover:text-[var(--text-secondary)]"
|
|
1014
1014
|
}`}
|
|
1015
1015
|
onClick={() => setThreadEditMode((prev) => !prev)}
|
|
@@ -33,7 +33,7 @@ export default function SearchModal({
|
|
|
33
33
|
<div className="flex items-center gap-3 border-b border-[var(--border)] px-4 py-3">
|
|
34
34
|
<Search className="h-4 w-4 text-[var(--text-muted)]" />
|
|
35
35
|
<input
|
|
36
|
-
className="flex-1 bg-transparent text-sm text-[var(--text-primary)] outline-none"
|
|
36
|
+
className="search-modal-input flex-1 bg-transparent text-sm text-[var(--text-primary)] outline-none"
|
|
37
37
|
placeholder="Search chats..."
|
|
38
38
|
value={query}
|
|
39
39
|
onChange={(event) => setQuery(event.target.value)}
|
|
@@ -7,10 +7,11 @@ import {
|
|
|
7
7
|
ensureBuiltinConnections,
|
|
8
8
|
normalizeBaseUrl,
|
|
9
9
|
} from "../lib/connections";
|
|
10
|
+
import { DEFAULT_ACCENT_DARK, DEFAULT_ACCENT_LIGHT } from "../lib/data";
|
|
10
11
|
import { db } from "../lib/db";
|
|
11
12
|
import { useMemories } from "../lib/hooks";
|
|
12
13
|
import { normalizeMemoryKey } from "../lib/memory";
|
|
13
|
-
import { filterModelIdsForConnection, getConnectionModelPresets } from "../lib/model-presets";
|
|
14
|
+
import { filterModelIdsForConnection, getConnectionModelPresets, getModelDisplayLabel } from "../lib/model-presets";
|
|
14
15
|
import {
|
|
15
16
|
DEFAULT_MEMORY_SETTINGS,
|
|
16
17
|
DEFAULT_LOCAL_TOOLS_SETTINGS,
|
|
@@ -24,7 +25,6 @@ import {
|
|
|
24
25
|
type ModelConnection,
|
|
25
26
|
type SafetyProfile,
|
|
26
27
|
type Settings,
|
|
27
|
-
type WebSearchBackend,
|
|
28
28
|
} from "../lib/types";
|
|
29
29
|
|
|
30
30
|
type TabId =
|
|
@@ -112,7 +112,7 @@ export default function SettingsModal({
|
|
|
112
112
|
const [showOpenAIKey, setShowOpenAIKey] = useState(false);
|
|
113
113
|
const [showAnthropicKey, setShowAnthropicKey] = useState(false);
|
|
114
114
|
const [showGeminiKey, setShowGeminiKey] = useState(false);
|
|
115
|
-
const [accentColor, setAccentColor] = useState(settings?.accentColor ||
|
|
115
|
+
const [accentColor, setAccentColor] = useState(settings?.accentColor || DEFAULT_ACCENT_DARK);
|
|
116
116
|
const [theme, setTheme] = useState<"dark" | "light">(settings?.theme || "dark");
|
|
117
117
|
const [font, setFont] = useState<"ibm" | "manrope" | "sora" | "space" | "poppins">(
|
|
118
118
|
settings?.font || "manrope",
|
|
@@ -132,8 +132,6 @@ export default function SettingsModal({
|
|
|
132
132
|
const [enableMail, setEnableMail] = useState(localTools.enableMail);
|
|
133
133
|
const [enableWorkflow, setEnableWorkflow] = useState(localTools.enableWorkflow);
|
|
134
134
|
const [enableSystem, setEnableSystem] = useState(localTools.enableSystem);
|
|
135
|
-
const [webSearchBackend, setWebSearchBackend] = useState<WebSearchBackend>(localTools.webSearchBackend);
|
|
136
|
-
const [dryRun, setDryRun] = useState(localTools.dryRun);
|
|
137
135
|
const memory = settings?.memory ?? DEFAULT_MEMORY_SETTINGS;
|
|
138
136
|
const [memoryEnabled, setMemoryEnabled] = useState(memory.enabled);
|
|
139
137
|
const [memoryAutoCapture, setMemoryAutoCapture] = useState(memory.autoCapture);
|
|
@@ -216,6 +214,49 @@ export default function SettingsModal({
|
|
|
216
214
|
return text.includes(query);
|
|
217
215
|
});
|
|
218
216
|
|
|
217
|
+
const memoryKindLabel: Record<MemoryKind, string> = {
|
|
218
|
+
profile: "Profile",
|
|
219
|
+
preference: "Preference",
|
|
220
|
+
person_alias: "Person alias",
|
|
221
|
+
music_alias: "Music alias",
|
|
222
|
+
note: "Note",
|
|
223
|
+
};
|
|
224
|
+
const memoryScopeLabel: Record<MemoryScope, string> = {
|
|
225
|
+
global: "All chats",
|
|
226
|
+
conversation: "This conversation",
|
|
227
|
+
};
|
|
228
|
+
const memorySourceLabel: Record<MemorySource, string> = {
|
|
229
|
+
auto: "Auto-captured",
|
|
230
|
+
explicit: "Explicit",
|
|
231
|
+
manual: "Manual",
|
|
232
|
+
};
|
|
233
|
+
function formatMemoryValue(value: string): string {
|
|
234
|
+
try {
|
|
235
|
+
const parsed = JSON.parse(value) as Record<string, unknown>;
|
|
236
|
+
if (parsed && typeof parsed === "object") {
|
|
237
|
+
const title = typeof parsed.title === "string" ? parsed.title : "";
|
|
238
|
+
const artist = typeof parsed.artist === "string" ? parsed.artist : "";
|
|
239
|
+
const query = typeof parsed.query === "string" ? parsed.query : "";
|
|
240
|
+
if (title && artist) return `${title} by ${artist}`;
|
|
241
|
+
if (query) return query;
|
|
242
|
+
if (title) return title;
|
|
243
|
+
}
|
|
244
|
+
} catch {
|
|
245
|
+
// not JSON, use as-is
|
|
246
|
+
}
|
|
247
|
+
return value;
|
|
248
|
+
}
|
|
249
|
+
function formatMemoryMeta(entry: MemoryEntry): string {
|
|
250
|
+
const parts = [
|
|
251
|
+
memoryKindLabel[entry.kind],
|
|
252
|
+
entry.scope === "conversation" && entry.conversationId
|
|
253
|
+
? `${memoryScopeLabel[entry.scope]} (${entry.conversationId})`
|
|
254
|
+
: memoryScopeLabel[entry.scope],
|
|
255
|
+
memorySourceLabel[entry.source],
|
|
256
|
+
];
|
|
257
|
+
return parts.join(" · ");
|
|
258
|
+
}
|
|
259
|
+
|
|
219
260
|
const resetConnectionForm = () => {
|
|
220
261
|
setEditingConnectionId(null);
|
|
221
262
|
setConnectionFormName("");
|
|
@@ -486,7 +527,7 @@ export default function SettingsModal({
|
|
|
486
527
|
defaultModelByConnection: resolvedDefaultModelMap,
|
|
487
528
|
showExtendedOpenAIModels,
|
|
488
529
|
enableWebSources,
|
|
489
|
-
accentColor: accentColor ||
|
|
530
|
+
accentColor: accentColor || DEFAULT_ACCENT_DARK,
|
|
490
531
|
font,
|
|
491
532
|
theme,
|
|
492
533
|
localTools: {
|
|
@@ -504,8 +545,8 @@ export default function SettingsModal({
|
|
|
504
545
|
enableMail,
|
|
505
546
|
enableWorkflow,
|
|
506
547
|
enableSystem,
|
|
507
|
-
webSearchBackend,
|
|
508
|
-
dryRun,
|
|
548
|
+
webSearchBackend: DEFAULT_LOCAL_TOOLS_SETTINGS.webSearchBackend,
|
|
549
|
+
dryRun: localTools.dryRun,
|
|
509
550
|
},
|
|
510
551
|
memory: {
|
|
511
552
|
enabled: memoryEnabled,
|
|
@@ -518,7 +559,7 @@ export default function SettingsModal({
|
|
|
518
559
|
};
|
|
519
560
|
|
|
520
561
|
const accentPresets = [
|
|
521
|
-
{ id: "default", label: "Default", color:
|
|
562
|
+
{ id: "default", label: "Default", color: DEFAULT_ACCENT_DARK },
|
|
522
563
|
{ id: "blue", label: "Blue", color: "#2563eb" },
|
|
523
564
|
{ id: "green", label: "Green", color: "#16a34a" },
|
|
524
565
|
{ id: "yellow", label: "Yellow", color: "#f59e0b" },
|
|
@@ -545,9 +586,9 @@ export default function SettingsModal({
|
|
|
545
586
|
<button
|
|
546
587
|
key={tab.id}
|
|
547
588
|
onClick={() => setActiveTab(tab.id)}
|
|
548
|
-
className={`rounded-full px-3 py-1.5 text-xs ${
|
|
589
|
+
className={`rounded-full px-3 py-1.5 text-xs settings-tab ${
|
|
549
590
|
activeTab === tab.id
|
|
550
|
-
? "
|
|
591
|
+
? "settings-tab-active"
|
|
551
592
|
: "border border-[var(--border)] bg-[var(--panel-2)] text-[var(--text-secondary)]"
|
|
552
593
|
}`}
|
|
553
594
|
>
|
|
@@ -581,7 +622,7 @@ export default function SettingsModal({
|
|
|
581
622
|
<div className="text-xs uppercase tracking-[0.2em] text-[var(--text-muted)]">
|
|
582
623
|
Default model for {selectedDefaultConnection.name}
|
|
583
624
|
</div>
|
|
584
|
-
<
|
|
625
|
+
<select
|
|
585
626
|
className="w-full rounded-lg border border-[var(--border)] bg-[var(--panel)] px-3 py-2 text-sm"
|
|
586
627
|
value={modelInput || selectedDefaultModel}
|
|
587
628
|
onChange={(event) => {
|
|
@@ -592,10 +633,25 @@ export default function SettingsModal({
|
|
|
592
633
|
[selectedDefaultConnection.id]: value,
|
|
593
634
|
}));
|
|
594
635
|
}}
|
|
595
|
-
|
|
596
|
-
|
|
636
|
+
>
|
|
637
|
+
{selectableModels.length === 0 ? (
|
|
638
|
+
<option value="">No models — fetch from Connections tab</option>
|
|
639
|
+
) : (
|
|
640
|
+
(() => {
|
|
641
|
+
const current = modelInput || selectedDefaultModel;
|
|
642
|
+
const ids = current && !selectableModels.includes(current)
|
|
643
|
+
? [current, ...selectableModels]
|
|
644
|
+
: selectableModels;
|
|
645
|
+
return ids.map((modelId) => (
|
|
646
|
+
<option key={modelId} value={modelId}>
|
|
647
|
+
{getModelDisplayLabel(modelId, selectedDefaultConnection)}
|
|
648
|
+
</option>
|
|
649
|
+
));
|
|
650
|
+
})()
|
|
651
|
+
)}
|
|
652
|
+
</select>
|
|
597
653
|
<div className="flex flex-wrap gap-2">
|
|
598
|
-
{selectableModels.
|
|
654
|
+
{selectableModels.map((preset) => (
|
|
599
655
|
<button
|
|
600
656
|
key={preset}
|
|
601
657
|
className="rounded-full border border-[var(--border)] bg-[var(--panel)] px-3 py-1 text-xs text-[var(--text-secondary)]"
|
|
@@ -607,7 +663,7 @@ export default function SettingsModal({
|
|
|
607
663
|
}));
|
|
608
664
|
}}
|
|
609
665
|
>
|
|
610
|
-
{preset}
|
|
666
|
+
{getModelDisplayLabel(preset, selectedDefaultConnection)}
|
|
611
667
|
</button>
|
|
612
668
|
))}
|
|
613
669
|
{selectableModels.length === 0 ? (
|
|
@@ -618,7 +674,7 @@ export default function SettingsModal({
|
|
|
618
674
|
</div>
|
|
619
675
|
{selectedDefaultConnection?.kind === "builtin" &&
|
|
620
676
|
selectedDefaultConnection?.provider === "openai" ? (
|
|
621
|
-
<label className="
|
|
677
|
+
<label className="flex items-center gap-2 text-sm text-[var(--text-secondary)]">
|
|
622
678
|
<input
|
|
623
679
|
type="checkbox"
|
|
624
680
|
checked={showExtendedOpenAIModels}
|
|
@@ -852,7 +908,7 @@ export default function SettingsModal({
|
|
|
852
908
|
</div>
|
|
853
909
|
<div className="mt-3 flex gap-2">
|
|
854
910
|
<button
|
|
855
|
-
className="rounded-full
|
|
911
|
+
className="rounded-full px-4 py-2 text-xs settings-btn-accent"
|
|
856
912
|
onClick={upsertConnectionFromForm}
|
|
857
913
|
>
|
|
858
914
|
{editingConnectionId ? "Update Connection" : "Add Connection"}
|
|
@@ -930,7 +986,6 @@ export default function SettingsModal({
|
|
|
930
986
|
{ label: "Enable mail/messages tools", checked: enableMail, setter: setEnableMail },
|
|
931
987
|
{ label: "Enable workflow tool", checked: enableWorkflow, setter: setEnableWorkflow },
|
|
932
988
|
{ label: "Enable system controls", checked: enableSystem, setter: setEnableSystem },
|
|
933
|
-
{ label: "Dry-run only (no writes)", checked: dryRun, setter: setDryRun },
|
|
934
989
|
].map((item) => (
|
|
935
990
|
<label key={item.label} className="flex items-center gap-2 text-sm text-[var(--text-secondary)]">
|
|
936
991
|
<input
|
|
@@ -942,19 +997,6 @@ export default function SettingsModal({
|
|
|
942
997
|
</label>
|
|
943
998
|
))}
|
|
944
999
|
</div>
|
|
945
|
-
<div>
|
|
946
|
-
<label className="mb-2 block text-xs uppercase tracking-[0.2em] text-[var(--text-muted)]">
|
|
947
|
-
Web Search Backend
|
|
948
|
-
</label>
|
|
949
|
-
<select
|
|
950
|
-
className="w-full rounded-lg border border-[var(--border)] bg-[var(--panel)] px-3 py-2 text-sm"
|
|
951
|
-
value={webSearchBackend}
|
|
952
|
-
onChange={(event) => setWebSearchBackend(event.target.value as WebSearchBackend)}
|
|
953
|
-
>
|
|
954
|
-
<option value="no_key">No-key (default)</option>
|
|
955
|
-
<option value="hybrid">Hybrid</option>
|
|
956
|
-
</select>
|
|
957
|
-
</div>
|
|
958
1000
|
</div>
|
|
959
1001
|
) : null}
|
|
960
1002
|
|
|
@@ -1020,13 +1062,11 @@ export default function SettingsModal({
|
|
|
1020
1062
|
<div className="flex items-start justify-between gap-3">
|
|
1021
1063
|
<div>
|
|
1022
1064
|
<div className="text-sm font-medium text-[var(--text-primary)]">{entry.key}</div>
|
|
1023
|
-
<div className="mt-0.5 text-xs text-[var(--text-secondary)]">
|
|
1065
|
+
<div className="mt-0.5 text-xs text-[var(--text-secondary)]">
|
|
1066
|
+
{formatMemoryValue(entry.value)}
|
|
1067
|
+
</div>
|
|
1024
1068
|
<div className="mt-1 text-[11px] text-[var(--text-muted)]">
|
|
1025
|
-
{entry
|
|
1026
|
-
{entry.scope === "conversation" && entry.conversationId
|
|
1027
|
-
? ` | ${entry.conversationId}`
|
|
1028
|
-
: ""}
|
|
1029
|
-
{` | ${entry.source}`}
|
|
1069
|
+
{formatMemoryMeta(entry)}
|
|
1030
1070
|
</div>
|
|
1031
1071
|
</div>
|
|
1032
1072
|
<div className="flex shrink-0 gap-2">
|
|
@@ -1130,7 +1170,7 @@ export default function SettingsModal({
|
|
|
1130
1170
|
) : null}
|
|
1131
1171
|
<div className="flex gap-2">
|
|
1132
1172
|
<button
|
|
1133
|
-
className="rounded-full
|
|
1173
|
+
className="rounded-full px-4 py-2 text-xs settings-btn-accent"
|
|
1134
1174
|
onClick={() => {
|
|
1135
1175
|
void upsertMemoryEntry();
|
|
1136
1176
|
}}
|
|
@@ -1157,7 +1197,11 @@ export default function SettingsModal({
|
|
|
1157
1197
|
<select
|
|
1158
1198
|
className="w-full rounded-lg border border-[var(--border)] bg-[var(--panel-2)] px-3 py-2 text-sm"
|
|
1159
1199
|
value={theme}
|
|
1160
|
-
onChange={(event) =>
|
|
1200
|
+
onChange={(event) => {
|
|
1201
|
+
const value = event.target.value as "dark" | "light";
|
|
1202
|
+
setTheme(value);
|
|
1203
|
+
document.documentElement.dataset.theme = value;
|
|
1204
|
+
}}
|
|
1161
1205
|
>
|
|
1162
1206
|
<option value="dark">Dark</option>
|
|
1163
1207
|
<option value="light">Light</option>
|
|
@@ -1170,9 +1214,22 @@ export default function SettingsModal({
|
|
|
1170
1214
|
<select
|
|
1171
1215
|
className="w-full rounded-lg border border-[var(--border)] bg-[var(--panel-2)] px-3 py-2 text-sm"
|
|
1172
1216
|
value={font}
|
|
1173
|
-
onChange={(event) =>
|
|
1174
|
-
|
|
1175
|
-
|
|
1217
|
+
onChange={(event) => {
|
|
1218
|
+
const value = event.target.value as "ibm" | "manrope" | "sora" | "space" | "poppins";
|
|
1219
|
+
setFont(value);
|
|
1220
|
+
const fontVar =
|
|
1221
|
+
value === "manrope"
|
|
1222
|
+
? "var(--font-manrope)"
|
|
1223
|
+
: value === "poppins"
|
|
1224
|
+
? "var(--font-poppins)"
|
|
1225
|
+
: value === "sora"
|
|
1226
|
+
? "var(--font-sora)"
|
|
1227
|
+
: value === "space"
|
|
1228
|
+
? "var(--font-space)"
|
|
1229
|
+
: "var(--font-sans)";
|
|
1230
|
+
document.documentElement.style.setProperty("--app-font", fontVar);
|
|
1231
|
+
document.body.style.fontFamily = fontVar;
|
|
1232
|
+
}}
|
|
1176
1233
|
>
|
|
1177
1234
|
<option value="ibm">IBM Plex Sans (Default)</option>
|
|
1178
1235
|
<option value="manrope">Manrope</option>
|
|
@@ -1186,20 +1243,35 @@ export default function SettingsModal({
|
|
|
1186
1243
|
Accent Color
|
|
1187
1244
|
</label>
|
|
1188
1245
|
<div className="grid grid-cols-4 gap-2">
|
|
1189
|
-
{accentPresets.map((preset) =>
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
<
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1246
|
+
{accentPresets.map((preset) => {
|
|
1247
|
+
const isDefault = preset.id === "default";
|
|
1248
|
+
const displayColor =
|
|
1249
|
+
isDefault && theme === "light"
|
|
1250
|
+
? DEFAULT_ACCENT_LIGHT
|
|
1251
|
+
: preset.color;
|
|
1252
|
+
const isSelected = isDefault
|
|
1253
|
+
? accentColor === DEFAULT_ACCENT_DARK
|
|
1254
|
+
: accentColor === preset.color;
|
|
1255
|
+
return (
|
|
1256
|
+
<button
|
|
1257
|
+
key={preset.id}
|
|
1258
|
+
className={`flex items-center gap-2 rounded-lg border px-3 py-2 text-xs ${
|
|
1259
|
+
isSelected
|
|
1260
|
+
? "border-[var(--accent-ring)] text-[var(--text-primary)]"
|
|
1261
|
+
: "border-[var(--border)] text-[var(--text-secondary)]"
|
|
1262
|
+
}`}
|
|
1263
|
+
onClick={() =>
|
|
1264
|
+
setAccentColor(isDefault ? DEFAULT_ACCENT_DARK : preset.color)
|
|
1265
|
+
}
|
|
1266
|
+
>
|
|
1267
|
+
<span
|
|
1268
|
+
className="h-3 w-3 rounded-full"
|
|
1269
|
+
style={{ background: displayColor }}
|
|
1270
|
+
/>
|
|
1271
|
+
{preset.label}
|
|
1272
|
+
</button>
|
|
1273
|
+
);
|
|
1274
|
+
})}
|
|
1203
1275
|
</div>
|
|
1204
1276
|
</div>
|
|
1205
1277
|
</div>
|
|
@@ -1248,7 +1320,7 @@ export default function SettingsModal({
|
|
|
1248
1320
|
Cancel
|
|
1249
1321
|
</button>
|
|
1250
1322
|
<button
|
|
1251
|
-
className="rounded-full
|
|
1323
|
+
className="rounded-full px-4 py-2 text-xs settings-btn-accent"
|
|
1252
1324
|
onClick={handleSave}
|
|
1253
1325
|
>
|
|
1254
1326
|
Save
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import { useMemo } from "react";
|
|
4
4
|
import {
|
|
5
|
-
Folder,
|
|
6
5
|
PanelLeftClose,
|
|
7
6
|
PenSquare,
|
|
8
7
|
Search,
|
|
@@ -83,8 +82,8 @@ export default function Sidebar({
|
|
|
83
82
|
</div>
|
|
84
83
|
|
|
85
84
|
<div className="flex-1 overflow-y-auto px-2">
|
|
86
|
-
<div className="sidebar-text px-2 py-2 text-
|
|
87
|
-
Your
|
|
85
|
+
<div className="sidebar-text px-2 py-2 text-sm text-[var(--text-muted)]">
|
|
86
|
+
Your Chats
|
|
88
87
|
</div>
|
|
89
88
|
<div className="space-y-2">
|
|
90
89
|
{groups.map(({ root }) => (
|
|
@@ -106,7 +105,6 @@ export default function Sidebar({
|
|
|
106
105
|
}`}
|
|
107
106
|
>
|
|
108
107
|
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
109
|
-
<Folder className="h-4 w-4 shrink-0 text-[var(--text-muted)]" />
|
|
110
108
|
<div className="sidebar-text min-w-0 truncate">
|
|
111
109
|
{root.title || "Main chat"}
|
|
112
110
|
</div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useEffect, useRef, useState } from "react";
|
|
4
|
-
import { ChevronDown, MessageSquare,
|
|
4
|
+
import { ChevronDown, MessageSquare, Network, Settings } from "lucide-react";
|
|
5
5
|
import type { ModelConnection } from "../lib/types";
|
|
6
6
|
import { getModelDisplayLabel } from "../lib/model-presets";
|
|
7
7
|
|
|
@@ -67,7 +67,7 @@ export default function TopBar({
|
|
|
67
67
|
<button
|
|
68
68
|
type="button"
|
|
69
69
|
onClick={() => setConnectionMenuOpen((current) => !current)}
|
|
70
|
-
className="inline-flex max-w-[42vw] items-center gap-2 rounded-md px-1.5 py-1 text-left sm:max-w-[240px] sm:px-2"
|
|
70
|
+
className="inline-flex max-w-[42vw] items-center gap-2 rounded-md px-1.5 py-1 text-left text-[var(--text-primary)] sm:max-w-[240px] sm:px-2"
|
|
71
71
|
aria-haspopup="listbox"
|
|
72
72
|
aria-expanded={connectionMenuOpen}
|
|
73
73
|
aria-label="Model provider"
|
|
@@ -144,15 +144,9 @@ export default function TopBar({
|
|
|
144
144
|
onClick={onToggleView}
|
|
145
145
|
>
|
|
146
146
|
{viewMode === "chat" ? (
|
|
147
|
-
|
|
148
|
-
<Waypoints className="h-5 w-5" />
|
|
149
|
-
<span className="hidden sm:inline">Map</span>
|
|
150
|
-
</>
|
|
147
|
+
<Network className="h-5 w-5" />
|
|
151
148
|
) : (
|
|
152
|
-
|
|
153
|
-
<MessageSquare className="h-5 w-5" />
|
|
154
|
-
<span className="hidden sm:inline">Chat</span>
|
|
155
|
-
</>
|
|
149
|
+
<MessageSquare className="h-5 w-5" />
|
|
156
150
|
)}
|
|
157
151
|
</button>
|
|
158
152
|
) : null}
|
package/template/src/lib/data.ts
CHANGED
|
@@ -30,6 +30,11 @@ const DEFAULT_SETTINGS: Settings = {
|
|
|
30
30
|
memory: { ...DEFAULT_MEMORY_SETTINGS },
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
+
/** Default accent gray in dark mode (chat bubble, etc.). */
|
|
34
|
+
export const DEFAULT_ACCENT_DARK = "#66706e";
|
|
35
|
+
/** Default accent gray in light mode (light gray so bubble isn’t too dark on white). */
|
|
36
|
+
export const DEFAULT_ACCENT_LIGHT = "#e4e6e5";
|
|
37
|
+
|
|
33
38
|
const LEGACY_DEFAULT_LOCAL_TOOLS_SETTINGS: LocalToolsSettings = {
|
|
34
39
|
...DEFAULT_LOCAL_TOOLS_SETTINGS,
|
|
35
40
|
approvalMode: "always_confirm_writes",
|
|
@@ -136,6 +136,32 @@ export function filterModelIdsForConnection(params: {
|
|
|
136
136
|
return ids.slice(0, Math.min(ids.length, OPENAI_FRONTIER_MODEL_PRESETS.length));
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
const OPENAI_MODEL_LABELS: Record<string, string> = {
|
|
140
|
+
"gpt-5.2": "GPT 5.2",
|
|
141
|
+
"gpt-5.2-pro": "GPT 5.2 Pro",
|
|
142
|
+
"gpt-5.2-chat-latest": "GPT 5.2 Chat (Latest)",
|
|
143
|
+
"gpt-5.2-codex": "GPT 5.2 Codex",
|
|
144
|
+
"gpt-5.1": "GPT 5.1",
|
|
145
|
+
"gpt-5.1-chat-latest": "GPT 5.1 Chat (Latest)",
|
|
146
|
+
"gpt-5.1-codex": "GPT 5.1 Codex",
|
|
147
|
+
"gpt-5.1-codex-max": "GPT 5.1 Codex Max",
|
|
148
|
+
"gpt-5": "GPT 5",
|
|
149
|
+
"gpt-5-chat-latest": "GPT 5 Chat (Latest)",
|
|
150
|
+
"gpt-5-mini": "GPT 5 Mini",
|
|
151
|
+
"gpt-5-nano": "GPT 5 Nano",
|
|
152
|
+
"gpt-5-codex": "GPT 5 Codex",
|
|
153
|
+
"gpt-5-pro": "GPT 5 Pro",
|
|
154
|
+
"gpt-4.1": "GPT 4.1",
|
|
155
|
+
"gpt-4.1-mini": "GPT 4.1 Mini",
|
|
156
|
+
"gpt-4.1-nano": "GPT 4.1 Nano",
|
|
157
|
+
"gpt-4o": "GPT 4o",
|
|
158
|
+
"gpt-4o-mini": "GPT 4o Mini",
|
|
159
|
+
"o3-pro": "O3 Pro",
|
|
160
|
+
"o3": "O3",
|
|
161
|
+
"o4-mini": "O4 Mini",
|
|
162
|
+
"o3-mini": "O3 Mini",
|
|
163
|
+
};
|
|
164
|
+
|
|
139
165
|
const ANTHROPIC_MODEL_LABELS: Record<string, string> = {
|
|
140
166
|
"claude-opus-4-61": "Claude Opus 4.6",
|
|
141
167
|
"claude-sonnet-4-51": "Claude Sonnet 4.5",
|
|
@@ -209,6 +235,9 @@ export function getModelDisplayLabel(modelId: string, connection: ModelConnectio
|
|
|
209
235
|
if (connection.provider === "google") {
|
|
210
236
|
return GOOGLE_MODEL_LABELS[modelId] ?? humanizeModelId(modelId);
|
|
211
237
|
}
|
|
238
|
+
if (connection.provider === "openai") {
|
|
239
|
+
return OPENAI_MODEL_LABELS[modelId] ?? humanizeModelId(modelId);
|
|
240
|
+
}
|
|
212
241
|
return modelId;
|
|
213
242
|
}
|
|
214
243
|
|