iris-chatbot 4.0.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/api/chat/route.ts +81 -80
- package/template/src/app/globals.css +98 -9
- package/template/src/app/page.tsx +22 -7
- package/template/src/components/MapView.tsx +21 -2
- package/template/src/components/MessageCard.tsx +83 -20
- 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/template/src/lib/tooling/tools/files.ts +486 -34
- package/template/src/lib/tooling/tools/schedule.ts +27 -5
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
|
@@ -42,25 +42,26 @@ const LOCAL_TOOL_SYSTEM_INSTRUCTIONS = [
|
|
|
42
42
|
"When the user asks for file, app, notes, music, web, calendar, or system actions, call tools directly.",
|
|
43
43
|
"Default to execution instead of clarification. Ask at most one question only when a truly required value is missing.",
|
|
44
44
|
"Calendar: You have direct access to the user's Apple Calendar via calendar_list_events and related tools. You CAN see their events—call the tools; do not say you cannot access or see their calendar. Never ask the user to paste their calendar, export it, or send a screenshot; you already have access. When they ask what's on the calendar, what's happening this/next weekend/month, or say 'check' or 'you can see', call calendar_list_events immediately with from/to for the range (e.g. next month → from: 'today', to: 'end of next month'; specific day → YYYY-MM-DD for both). Always pass calendar: 'all'. from/to support 'today', 'tomorrow', 'next week', 'end of next week', 'next month', 'end of next month'. Report the exact queried range from the tool result. For move/delete/reschedule: call calendar_list_events first to get uids. For rescheduling to a new time on the same calendar use calendar_update_event (preserves recurrence); for delete use calendar_delete_event; use calendar_move_event only when update is not sufficient (e.g. user says move and you need delete+recreate).",
|
|
45
|
+
"Files and Folders: You HAVE direct access to the user's files via file_list and file_find. Never say you cannot see, access, or list their files—call the tools. When the user asks 'what files do I have,' 'files from the last 7 days,' 'recent files,' or similar: call file_list immediately with path set to a folder (e.g. ~/Downloads, ~/Desktop, ~/Documents—or call once per root if you want all) and modifiedInLastDays: 7 (or the number of days they said). You have full file system access via file_list, file_mkdir, file_move, file_copy, file_delete_to_trash, file_batch_move, and file_find. When the user gives a folder or file name (e.g. 'us debt clock'), use file_find with searchPath '~' and the name as given. When moving MULTIPLE files, use file_batch_move with { operations: [ ... ] } or { destination, sources }. For organizing: file_find if location unknown, then file_list (use modifiedInLastDays for 'recent'), then file_mkdir/file_batch_move as needed. Never refuse file tasks or ask the user to use Finder manually—you have the tools.",
|
|
45
46
|
"For iMessage/text requests, use messages_send; if contact matching is ambiguous, ask one concise clarification with candidate names.",
|
|
46
47
|
"For media requests, assume Apple Music unless the user specifies otherwise.",
|
|
47
48
|
"For note creation, write structured markdown with clear headings, bolded key labels, and tables when they improve clarity.",
|
|
48
49
|
"If the user requests multiple actions in one prompt, execute every requested action sequentially before your final response.",
|
|
49
50
|
"Do not stop after completing just one action when additional requested actions remain.",
|
|
50
51
|
"For multi-step requests, prefer a single workflow tool call when available.",
|
|
51
|
-
"When tools are enabled, never claim you cannot access the user's device or
|
|
52
|
+
"When tools are enabled, never claim you cannot access the user's device, calendar, or file system. You have the tools—use them; do not refuse or ask the user to do tasks manually that you can do with tools.",
|
|
52
53
|
"After tool results arrive, provide a brief final answer and stop.",
|
|
53
54
|
"Summarize tool activity in plain English and avoid raw JSON, stack traces, or script line numbers.",
|
|
54
55
|
"Respect tool errors and provide clear next steps.",
|
|
55
56
|
].join(" ");
|
|
56
57
|
|
|
57
58
|
const ACTION_VERB_PATTERN =
|
|
58
|
-
"create|make|write|play|set|add|remind|open|send|launch|start|pause|resume|focus";
|
|
59
|
+
"create|make|write|play|set|add|remind|open|send|launch|start|pause|resume|focus|move|copy|delete|organize|reorganize|restructure|split|separate|sort|categorize";
|
|
59
60
|
const SEQUENCE_MARKER_PATTERN = "first|second|third|fourth|fifth|next|then|finally|lastly";
|
|
60
61
|
const TOOL_INTENT_EXPLICIT_PATTERN =
|
|
61
62
|
/\b(use|run|call)\s+(?:the\s+)?(?:local\s+)?tools?\b|\b(on my (?:mac|computer|device)|locally)\b/i;
|
|
62
63
|
const TOOL_INTENT_LEADING_VERB_PATTERN =
|
|
63
|
-
/^(?:please\s+)?(?:(?:can|could|would)\s+you\s+)?(?:send|text|message|play|pause|resume|skip|next|previous|set|open|launch|focus|create|make|add|remind|search|find|list|move|copy|rename|delete|trash|append|write|read|start|stop)
|
|
64
|
+
/^(?:please\s+)?(?:(?:can|could|would)\s+you\s+)?(?:send|text|message|play|pause|resume|skip|next|previous|set|open|launch|focus|create|make|add|remind|search|find|list|move|copy|rename|delete|trash|append|write|read|start|stop|organize|reorganize|restructure|split|separate|sort|categorize|put|place)(?:\s+|$)/i;
|
|
64
65
|
const TOOL_INTENT_MESSAGING_PATTERN =
|
|
65
66
|
/\b(text|message|imessage|sms|mail|email)\b[\s\S]{0,80}\b(send|text|message|email|mail)\b|\b(send|text|message|email|mail)\b[\s\S]{0,80}\b(text|message|imessage|sms|mail|email)\b/i;
|
|
66
67
|
const TOOL_INTENT_NOTES_PATTERN =
|
|
@@ -70,7 +71,7 @@ const TOOL_INTENT_MUSIC_PATTERN =
|
|
|
70
71
|
const TOOL_INTENT_SCHEDULE_PATTERN =
|
|
71
72
|
/\b(calendar|event|reminder|schedule|events)\b[\s\S]{0,80}\b(create|add|list|show|set|remind|schedule|move|change|delete|reschedule|update|cancel|have|get|check)\b|\b(create|add|list|show|set|remind|schedule|move|change|delete|reschedule|update|cancel|have|get|check)\b[\s\S]{0,80}\b(calendar|event|reminder|schedule|events)\b|\b(move|reschedule|change|delete)\b[\s\S]{0,80}\b(event|meeting|appointment)\b|\b(what's happening|whats happening|what's on|whats on|next weekend|this weekend|you can see|just check)\b/i;
|
|
72
73
|
const TOOL_INTENT_FILES_PATTERN =
|
|
73
|
-
/\b(file|folder|directory|path|document)\b[\s\S]{0,80}\b(list|find|search|move|copy|rename|delete|trash|create|make|mkdir|open)\b|\b(list|find|search|move|copy|rename|delete|trash|create|make|mkdir|open)\b[\s\S]{0,80}\b(file|folder|directory|path|document)\b/i;
|
|
74
|
+
/\b(file|folder|directory|path|document|subfolder|subfolders)\b[\s\S]{0,80}\b(list|find|search|move|copy|rename|delete|trash|create|make|mkdir|open|organize|reorganize|restructure|split|separate|sort|categorize|put|place|have|show|see|get)\b|\b(list|find|search|move|copy|rename|delete|trash|create|make|mkdir|open|organize|reorganize|restructure|split|separate|sort|categorize|put|place|have|show|see|get)\b[\s\S]{0,80}\b(file|folder|directory|path|document|subfolder|subfolders)\b|\b(organize|reorganize|restructure|split|separate)\b[\s\S]{0,80}\b(into|by|by type|by date|by name)\b|\b(put|move|place)\b[\s\S]{0,40}\b(into|in|inside)\b[\s\S]{0,40}\b(folder|directory)\b|\b(create|make)\b[\s\S]{0,40}\b(folders?|directories?)\b[\s\S]{0,40}\b(for|called|named)\b|\b(recent|last \d+|past \d+)\b[\s\S]{0,40}\b(files?|days?|weeks?)\b|\b(files?)\b[\s\S]{0,40}\b(from|in|within|during)\b[\s\S]{0,40}\b(last|past|recent|\d+)\b|\b(what|which|show|list|my)\b[\s\S]{0,40}\b(files?|documents?)\b/i;
|
|
74
75
|
const TOOL_INTENT_APPS_PATTERN =
|
|
75
76
|
/\b(app|application|window|url|website|browser|system)\b[\s\S]{0,80}\b(open|launch|focus|quit|set|mute|unmute|browse)\b|\b(open|launch|focus|quit|set|mute|unmute|browse)\b[\s\S]{0,80}\b(app|application|window|url|website|browser|system)\b/i;
|
|
76
77
|
const TOOL_INTENT_NUMBERS_PATTERN =
|
|
@@ -86,8 +87,8 @@ function splitCompoundActionClauses(input: string): string[] {
|
|
|
86
87
|
const roughParts = normalized.split(
|
|
87
88
|
new RegExp(
|
|
88
89
|
`\\s*(?:,|;|\\n)+\\s*(?=(?:(?:${SEQUENCE_MARKER_PATTERN})\\b|${ACTION_VERB_PATTERN}\\b))|` +
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
`\\s+(?:and then|and also|as well as|after that|plus|then|also|next|finally|lastly)\\s+|` +
|
|
91
|
+
`\\s+and\\s+(?=(?:${ACTION_VERB_PATTERN})\\b)`,
|
|
91
92
|
"i",
|
|
92
93
|
),
|
|
93
94
|
);
|
|
@@ -234,9 +235,9 @@ function normalizeRuntimeConnection(connection: ChatConnectionPayload): RuntimeC
|
|
|
234
235
|
: "openai_compatible";
|
|
235
236
|
const provider =
|
|
236
237
|
kind === "builtin" &&
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
238
|
+
(connection.provider === "openai" ||
|
|
239
|
+
connection.provider === "anthropic" ||
|
|
240
|
+
connection.provider === "google")
|
|
240
241
|
? connection.provider
|
|
241
242
|
: undefined;
|
|
242
243
|
const baseUrl =
|
|
@@ -264,9 +265,9 @@ function normalizeRuntimeConnection(connection: ChatConnectionPayload): RuntimeC
|
|
|
264
265
|
apiKey,
|
|
265
266
|
headers: Array.isArray(connection.headers)
|
|
266
267
|
? connection.headers.filter(
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
268
|
+
(header): header is { key: string; value: string } =>
|
|
269
|
+
Boolean(header) && typeof header.key === "string" && typeof header.value === "string",
|
|
270
|
+
)
|
|
270
271
|
: [],
|
|
271
272
|
supportsTools,
|
|
272
273
|
};
|
|
@@ -435,47 +436,47 @@ function sanitizeMemoryContext(
|
|
|
435
436
|
|
|
436
437
|
const people = Array.isArray(aliases.people)
|
|
437
438
|
? aliases.people
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
439
|
+
.map((item) => {
|
|
440
|
+
const alias = typeof item?.alias === "string" ? item.alias.trim() : "";
|
|
441
|
+
const target = typeof item?.target === "string" ? item.target.trim() : "";
|
|
442
|
+
if (!alias || !target) {
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
return { alias, target };
|
|
446
|
+
})
|
|
447
|
+
.filter((item): item is { alias: string; target: string } => Boolean(item))
|
|
448
|
+
.slice(0, 16)
|
|
448
449
|
: [];
|
|
449
450
|
|
|
450
451
|
const music = Array.isArray(aliases.music)
|
|
451
452
|
? aliases.music
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
453
|
+
.map((item) => {
|
|
454
|
+
const alias = typeof item?.alias === "string" ? item.alias.trim() : "";
|
|
455
|
+
const query = typeof item?.query === "string" ? item.query.trim() : "";
|
|
456
|
+
if (!alias || !query) {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
const title =
|
|
460
|
+
typeof item?.title === "string" && item.title.trim()
|
|
461
|
+
? item.title.trim()
|
|
462
|
+
: undefined;
|
|
463
|
+
const artist =
|
|
464
|
+
typeof item?.artist === "string" && item.artist.trim()
|
|
465
|
+
? item.artist.trim()
|
|
466
|
+
: undefined;
|
|
467
|
+
return { alias, query, title, artist };
|
|
468
|
+
})
|
|
469
|
+
.filter(
|
|
470
|
+
(
|
|
471
|
+
item,
|
|
472
|
+
): item is {
|
|
473
|
+
alias: string;
|
|
474
|
+
query: string;
|
|
475
|
+
title: string | undefined;
|
|
476
|
+
artist: string | undefined;
|
|
477
|
+
} => Boolean(item),
|
|
478
|
+
)
|
|
479
|
+
.slice(0, 16)
|
|
479
480
|
: [];
|
|
480
481
|
|
|
481
482
|
return {
|
|
@@ -679,7 +680,7 @@ function humanizeToolErrorMessage(toolName: string, message: string): string {
|
|
|
679
680
|
}
|
|
680
681
|
|
|
681
682
|
if (normalized.includes("missing required")) {
|
|
682
|
-
const fieldMatch = cleaned.match(/missing required (?:string|numeric) field:\s*([a-zA-Z0-9_]+)/i);
|
|
683
|
+
const fieldMatch = cleaned.match(/missing required (?:string|numeric|array) field:\s*([a-zA-Z0-9_]+)/i);
|
|
683
684
|
if (fieldMatch?.[1]) {
|
|
684
685
|
return `Missing required field "${fieldMatch[1]}".`;
|
|
685
686
|
}
|
|
@@ -1407,8 +1408,8 @@ async function tryDirectAutomationFastPath(params: {
|
|
|
1407
1408
|
"Reminder";
|
|
1408
1409
|
const title = sanitizeReminderTitle(
|
|
1409
1410
|
rawTitle
|
|
1410
|
-
|
|
1411
|
-
|
|
1411
|
+
.replace(/^(that|i have to|to|for)\s+/i, "")
|
|
1412
|
+
.trim(),
|
|
1412
1413
|
);
|
|
1413
1414
|
|
|
1414
1415
|
return { title: title || "Reminder", due };
|
|
@@ -1715,18 +1716,18 @@ async function tryDirectAutomationFastPath(params: {
|
|
|
1715
1716
|
const playResult =
|
|
1716
1717
|
playOutput && typeof playOutput === "object"
|
|
1717
1718
|
? (playOutput as {
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1719
|
+
playing?: boolean;
|
|
1720
|
+
matched?: boolean;
|
|
1721
|
+
playlistName?: string | null;
|
|
1722
|
+
title?: string | null;
|
|
1723
|
+
artist?: string | null;
|
|
1724
|
+
album?: string | null;
|
|
1725
|
+
reason?: string | null;
|
|
1726
|
+
matchedTitle?: string | null;
|
|
1727
|
+
matchedArtist?: string | null;
|
|
1728
|
+
catalogTitle?: string | null;
|
|
1729
|
+
catalogArtist?: string | null;
|
|
1730
|
+
})
|
|
1730
1731
|
: null;
|
|
1731
1732
|
|
|
1732
1733
|
if (!playResult || playResult.playing !== true) {
|
|
@@ -1823,18 +1824,18 @@ async function tryDirectAutomationFastPath(params: {
|
|
|
1823
1824
|
}
|
|
1824
1825
|
|
|
1825
1826
|
params.send({
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1827
|
+
type: "token",
|
|
1828
|
+
value:
|
|
1829
|
+
hasNearbyMatch
|
|
1830
|
+
? `Started nearby match "${resolvedTitle}"${resolvedArtist ? ` by ${resolvedArtist}` : ""}. ${playResult.reason}`
|
|
1831
|
+
: resolvedPlaylistName && intent.volume !== null
|
|
1832
|
+
? `**Done.** Playing playlist "${resolvedPlaylistName}" in Apple Music and set volume to ${intent.volume}%.`
|
|
1833
|
+
: resolvedPlaylistName
|
|
1834
|
+
? `**Done.** Playing playlist "${resolvedPlaylistName}" in Apple Music.`
|
|
1835
|
+
: intent.volume !== null
|
|
1835
1836
|
? `**Done.** Playing "${resolvedTitle}"${resolvedArtist ? ` by ${resolvedArtist}` : ""} in Apple Music and set volume to ${intent.volume}%.`
|
|
1836
1837
|
: `**Done.** Playing "${resolvedTitle}"${resolvedArtist ? ` by ${resolvedArtist}` : ""} in Apple Music.`,
|
|
1837
|
-
|
|
1838
|
+
});
|
|
1838
1839
|
return true;
|
|
1839
1840
|
}
|
|
1840
1841
|
|
|
@@ -2159,12 +2160,12 @@ async function runToolOrchestrator(params: {
|
|
|
2159
2160
|
const approval = createApprovalRequest();
|
|
2160
2161
|
params.send({
|
|
2161
2162
|
type: "approval_requested",
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2163
|
+
approvalId: approval.approvalId,
|
|
2164
|
+
callId: call.id,
|
|
2165
|
+
name: call.name,
|
|
2166
|
+
args: callInput,
|
|
2167
|
+
reason: summarizeApprovalReason(call.name, callInput),
|
|
2168
|
+
});
|
|
2168
2169
|
|
|
2169
2170
|
emitThinkingUpdate(`Waiting for approval to run ${call.name}.`);
|
|
2170
2171
|
const decision = await approval.promise;
|
|
@@ -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
|
|
@@ -344,7 +359,7 @@ export default function Home() {
|
|
|
344
359
|
onSelect={async (id) => {
|
|
345
360
|
if (id !== activeThreadId) {
|
|
346
361
|
const prev = threads?.find((t) => t.id === activeThreadId);
|
|
347
|
-
if (prev
|
|
362
|
+
if (prev && prev.headMessageId == null) {
|
|
348
363
|
await deleteThread(prev.id);
|
|
349
364
|
}
|
|
350
365
|
}
|
|
@@ -352,7 +367,7 @@ export default function Home() {
|
|
|
352
367
|
}}
|
|
353
368
|
onNewChat={async () => {
|
|
354
369
|
const prev = threads?.find((t) => t.id === activeThreadId);
|
|
355
|
-
if (prev
|
|
370
|
+
if (prev && prev.headMessageId == null) {
|
|
356
371
|
await deleteThread(prev.id);
|
|
357
372
|
}
|
|
358
373
|
const thread = await createNewThread();
|
|
@@ -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);
|