@schlenkr/quassel 0.0.0-stage → 0.5.1
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/ChatActionDock.d.ts +2 -0
- package/ChatInputToolbar.d.ts +61 -0
- package/ChatMessages.d.ts +51 -0
- package/ChatPanel.d.ts +17 -0
- package/ChatSendContext.d.ts +7 -0
- package/DetailModeSwitch.d.ts +16 -0
- package/LICENSE +21 -0
- package/Markdown.d.ts +19 -0
- package/MessageActions.d.ts +8 -0
- package/PendingActionCard.d.ts +13 -0
- package/QuasselProvider.d.ts +89 -0
- package/README.md +135 -2
- package/StepPopover.d.ts +16 -0
- package/TimestampSwitch.d.ts +7 -0
- package/TranscriptModeSwitch.d.ts +8 -0
- package/WorkingScenes.d.ts +12 -0
- package/announce.d.ts +2 -0
- package/attachments.d.ts +24 -0
- package/chat-attachments.d.ts +6 -0
- package/chat-scroll.d.ts +12 -0
- package/chat.css +2080 -0
- package/chunk-3VEL4HEO.js +183 -0
- package/chunk-3VEL4HEO.js.map +7 -0
- package/elapsed.d.ts +16 -0
- package/events.d.ts +145 -0
- package/events.js +21 -0
- package/events.js.map +7 -0
- package/index.d.ts +25 -0
- package/index.js +2586 -0
- package/index.js.map +7 -0
- package/options.d.ts +48 -0
- package/package.json +53 -4
- package/texts.d.ts +71 -0
- package/timestamps.d.ts +7 -0
- package/types.d.ts +23 -0
- package/ui/button.d.ts +8 -0
- package/ui/card.d.ts +5 -0
- package/ui/cn.d.ts +2 -0
- package/ui/popover.d.ts +6 -0
- package/ui/spinner.d.ts +2 -0
- package/ui/stop-button.d.ts +10 -0
- package/ui/toggle.d.ts +8 -0
- package/ui/useFileInput.d.ts +20 -0
- package/useChatAction.d.ts +6 -0
package/index.js
ADDED
|
@@ -0,0 +1,2586 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MAX_CHAT_ATTACHMENTS,
|
|
3
|
+
MAX_CHAT_ATTACHMENT_BYTES,
|
|
4
|
+
applyEvent,
|
|
5
|
+
chatAttachmentMediaType,
|
|
6
|
+
compactToolLine,
|
|
7
|
+
prettyJson
|
|
8
|
+
} from "./chunk-3VEL4HEO.js";
|
|
9
|
+
|
|
10
|
+
// packages/quassel/src/types.ts
|
|
11
|
+
function stepState(message) {
|
|
12
|
+
if (message.tool?.isError) {
|
|
13
|
+
return "error";
|
|
14
|
+
}
|
|
15
|
+
if (message.role === "thinking") {
|
|
16
|
+
return message.closed ? "done" : "thinking";
|
|
17
|
+
}
|
|
18
|
+
return message.tool !== void 0 && message.tool.result === void 0 ? "running" : "done";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// packages/quassel/src/texts.ts
|
|
22
|
+
var fillText = (text, values) => text.replace(/\{(\w+)\}/g, (match, name) => name in values ? String(values[name]) : match);
|
|
23
|
+
var englishTexts = {
|
|
24
|
+
working: "Working ...",
|
|
25
|
+
toolRunning: "running ...",
|
|
26
|
+
toolStillRunning: "Tool running ...",
|
|
27
|
+
toolElapsedSeconds: "{seconds} s",
|
|
28
|
+
currentTool: "Tool running",
|
|
29
|
+
thinkingChip: "Thinking",
|
|
30
|
+
toolChip: "Tool",
|
|
31
|
+
stepGroupOne: "1 step",
|
|
32
|
+
stepGroupMany: "{count} steps",
|
|
33
|
+
stepGroupErrorOne: "1 error",
|
|
34
|
+
stepGroupErrorMany: "{count} errors",
|
|
35
|
+
stepGroupCollapse: "Collapse",
|
|
36
|
+
thinkingTitle: "Thinking trace",
|
|
37
|
+
toolTitle: "Tool call",
|
|
38
|
+
argumentsLabel: "Arguments",
|
|
39
|
+
resultLabel: "Result",
|
|
40
|
+
close: "Close",
|
|
41
|
+
jumpToEnd: "Jump to end",
|
|
42
|
+
send: "Send",
|
|
43
|
+
sendIntoRun: "Feed into the running turn",
|
|
44
|
+
stop: "Stop work",
|
|
45
|
+
placeholder: "Write a message ...",
|
|
46
|
+
steeringPlaceholder: "Interject ...",
|
|
47
|
+
steered: "Fed into the running turn",
|
|
48
|
+
detailModeTitle: "Step detail level",
|
|
49
|
+
detailModeOff: "answers only",
|
|
50
|
+
detailModeCurrent: "current",
|
|
51
|
+
detailModeIcons: "icons",
|
|
52
|
+
detailModeChips: "compact",
|
|
53
|
+
detailModeGrouped: "grouped",
|
|
54
|
+
detailModeCompact: "single line",
|
|
55
|
+
detailModeFull: "everything",
|
|
56
|
+
showAllMessages: "Show intermediate replies",
|
|
57
|
+
showLatestExchange: "Show latest reply between inputs",
|
|
58
|
+
showTimestamps: "Show timestamps",
|
|
59
|
+
hideTimestamps: "Hide timestamps",
|
|
60
|
+
copyMessage: "Copy message",
|
|
61
|
+
editMessage: "Edit message",
|
|
62
|
+
retryMessage: "Request answer again",
|
|
63
|
+
copyCode: "Copy code",
|
|
64
|
+
copied: "Copied",
|
|
65
|
+
pendingAction: "waiting for input",
|
|
66
|
+
dismissAction: "Dismiss",
|
|
67
|
+
actionDismissed: "dismissed",
|
|
68
|
+
actionFailed: "Action failed",
|
|
69
|
+
clipboardUnavailable: "The clipboard is not available",
|
|
70
|
+
transcript: "Chat history",
|
|
71
|
+
attachments: "Attachments",
|
|
72
|
+
attachFiles: "Attach files",
|
|
73
|
+
attachFilesHint: "Attach files (up to {count} files, {size} in total)",
|
|
74
|
+
removeAttachment: "Remove {name}",
|
|
75
|
+
attachmentPreparing: "Preparing ...",
|
|
76
|
+
attachmentsPreparing: "Preparing attachments ...",
|
|
77
|
+
attachmentReadFailed: "The file could not be read. Remove it and add it again.",
|
|
78
|
+
attachmentsWhileBusy: "Wait until the input is ready again, then add the files again.",
|
|
79
|
+
tooManyAttachments: "At most {count} attachments per message. Remove an attachment first.",
|
|
80
|
+
attachmentsTooLarge: "Attachments may be at most {size} in total. Choose smaller files.",
|
|
81
|
+
attachmentUnsupported: "This chat does not support {name} as {kind}. Remove the attachment.",
|
|
82
|
+
attachmentUnsupportedByModel: "The model {model} does not support {name} as {kind}. Choose a suitable model or remove the attachment.",
|
|
83
|
+
attachmentKindImage: "image",
|
|
84
|
+
attachmentKindVideo: "video",
|
|
85
|
+
attachmentKindPdf: "PDF",
|
|
86
|
+
notSent: "Not sent:",
|
|
87
|
+
notSentAttachments: "({count} attachments)",
|
|
88
|
+
insertUnsent: "Insert unsent input"
|
|
89
|
+
};
|
|
90
|
+
var germanTexts = {
|
|
91
|
+
working: "Arbeitet ...",
|
|
92
|
+
toolRunning: "l\xE4uft ...",
|
|
93
|
+
toolStillRunning: "Werkzeug l\xE4uft ...",
|
|
94
|
+
toolElapsedSeconds: "{seconds} s",
|
|
95
|
+
currentTool: "Werkzeug l\xE4uft",
|
|
96
|
+
thinkingChip: "Denken",
|
|
97
|
+
toolChip: "Werkzeug",
|
|
98
|
+
stepGroupOne: "1 Schritt",
|
|
99
|
+
stepGroupMany: "{count} Schritte",
|
|
100
|
+
stepGroupErrorOne: "1 Fehler",
|
|
101
|
+
stepGroupErrorMany: "{count} Fehler",
|
|
102
|
+
stepGroupCollapse: "Einklappen",
|
|
103
|
+
thinkingTitle: "Gedankengang",
|
|
104
|
+
toolTitle: "Werkzeugaufruf",
|
|
105
|
+
argumentsLabel: "Argumente",
|
|
106
|
+
resultLabel: "Ergebnis",
|
|
107
|
+
close: "Schlie\xDFen",
|
|
108
|
+
jumpToEnd: "Zum Ende springen",
|
|
109
|
+
send: "Senden",
|
|
110
|
+
sendIntoRun: "In die laufende Antwort einreihen",
|
|
111
|
+
stop: "Bearbeitung stoppen",
|
|
112
|
+
placeholder: "Nachricht schreiben ...",
|
|
113
|
+
steeringPlaceholder: "Nachsteuern ...",
|
|
114
|
+
steered: "In die laufende Antwort eingereiht",
|
|
115
|
+
detailModeTitle: "Detailgrad der Schritte",
|
|
116
|
+
detailModeOff: "nur Antworten",
|
|
117
|
+
detailModeCurrent: "aktueller Schritt",
|
|
118
|
+
detailModeIcons: "Symbole",
|
|
119
|
+
detailModeChips: "kompakt",
|
|
120
|
+
detailModeGrouped: "gruppiert",
|
|
121
|
+
detailModeCompact: "einzeilig",
|
|
122
|
+
detailModeFull: "alles",
|
|
123
|
+
showAllMessages: "Zwischenantworten anzeigen",
|
|
124
|
+
showLatestExchange: "Letzte Antwort zwischen den Eingaben anzeigen",
|
|
125
|
+
showTimestamps: "Zeitstempel anzeigen",
|
|
126
|
+
hideTimestamps: "Zeitstempel ausblenden",
|
|
127
|
+
copyMessage: "Nachricht kopieren",
|
|
128
|
+
editMessage: "Nachricht bearbeiten",
|
|
129
|
+
retryMessage: "Antwort erneut anfordern",
|
|
130
|
+
copyCode: "Code kopieren",
|
|
131
|
+
copied: "Kopiert",
|
|
132
|
+
pendingAction: "wartet auf Eingabe",
|
|
133
|
+
dismissAction: "Verwerfen",
|
|
134
|
+
actionDismissed: "verworfen",
|
|
135
|
+
actionFailed: "Aktion fehlgeschlagen",
|
|
136
|
+
clipboardUnavailable: "Die Zwischenablage ist nicht verf\xFCgbar",
|
|
137
|
+
transcript: "Chatverlauf",
|
|
138
|
+
attachments: "Anh\xE4nge",
|
|
139
|
+
attachFiles: "Dateien anh\xE4ngen",
|
|
140
|
+
attachFilesHint: "Dateien anh\xE4ngen (bis zu {count} Dateien, insgesamt {size})",
|
|
141
|
+
removeAttachment: "{name} entfernen",
|
|
142
|
+
attachmentPreparing: "Wird vorbereitet ...",
|
|
143
|
+
attachmentsPreparing: "Anh\xE4nge werden vorbereitet ...",
|
|
144
|
+
attachmentReadFailed: "Die Datei konnte nicht gelesen werden. Den Anhang entfernen und erneut hinzuf\xFCgen.",
|
|
145
|
+
attachmentsWhileBusy: "Warten, bis die Eingabe wieder bereit ist, dann die Dateien erneut hinzuf\xFCgen.",
|
|
146
|
+
tooManyAttachments: "H\xF6chstens {count} Anh\xE4nge pro Nachricht. Zuerst einen Anhang entfernen.",
|
|
147
|
+
attachmentsTooLarge: "Anh\xE4nge d\xFCrfen zusammen h\xF6chstens {size} gro\xDF sein. Kleinere Dateien ausw\xE4hlen.",
|
|
148
|
+
attachmentUnsupported: "Dieser Chat unterst\xFCtzt {name} nicht als {kind}. Den Anhang entfernen.",
|
|
149
|
+
attachmentUnsupportedByModel: "Das Modell {model} unterst\xFCtzt {name} nicht als {kind}. Ein passendes Modell ausw\xE4hlen oder den Anhang entfernen.",
|
|
150
|
+
attachmentKindImage: "Bild",
|
|
151
|
+
attachmentKindVideo: "Video",
|
|
152
|
+
attachmentKindPdf: "PDF",
|
|
153
|
+
notSent: "Nicht gesendet:",
|
|
154
|
+
notSentAttachments: "({count} Anh\xE4nge)",
|
|
155
|
+
insertUnsent: "Nicht gesendete Eingabe einf\xFCgen"
|
|
156
|
+
};
|
|
157
|
+
var defaultTexts = englishTexts;
|
|
158
|
+
|
|
159
|
+
// packages/quassel/src/ui/cn.ts
|
|
160
|
+
import { createCn } from "cn/config";
|
|
161
|
+
var cn = createCn({ prefix: "qsl", extend: { theme: { text: ["meta", "trace", "timestamp", "label", "chat"] } } });
|
|
162
|
+
|
|
163
|
+
// packages/quassel/src/ChatMessages.tsx
|
|
164
|
+
import { useCallback as useCallback2, useContext as useContext3, useEffect as useEffect3, useLayoutEffect, useMemo as useMemo3, useRef as useRef3, useState as useState4 } from "react";
|
|
165
|
+
import { createPortal } from "react-dom";
|
|
166
|
+
|
|
167
|
+
// packages/quassel/src/chat-scroll.ts
|
|
168
|
+
var atEnd = (viewport) => viewport.scrollHeight - viewport.clientHeight - viewport.scrollTop <= 24;
|
|
169
|
+
function createChatScroll(onFollowChange = () => {
|
|
170
|
+
}) {
|
|
171
|
+
let following = true;
|
|
172
|
+
let lastTop = 0;
|
|
173
|
+
let lastHeight = 0;
|
|
174
|
+
let lastClientHeight = 0;
|
|
175
|
+
const movedUp = (viewport, dragging) => viewport.scrollTop < lastTop && (dragging || viewport.scrollHeight === lastHeight && viewport.clientHeight === lastClientHeight);
|
|
176
|
+
const remember = (viewport) => {
|
|
177
|
+
lastTop = viewport.scrollTop;
|
|
178
|
+
lastHeight = viewport.scrollHeight;
|
|
179
|
+
lastClientHeight = viewport.clientHeight;
|
|
180
|
+
};
|
|
181
|
+
const follow = (value) => {
|
|
182
|
+
if (following === value) return;
|
|
183
|
+
following = value;
|
|
184
|
+
onFollowChange(value);
|
|
185
|
+
};
|
|
186
|
+
const layout = (viewport, dragging = false) => {
|
|
187
|
+
if (viewport.clientHeight <= 0) return;
|
|
188
|
+
if (movedUp(viewport, dragging) && !atEnd(viewport)) follow(false);
|
|
189
|
+
if (following) viewport.scrollTop = Math.max(0, viewport.scrollHeight - viewport.clientHeight);
|
|
190
|
+
else if (viewport.scrollHeight <= viewport.clientHeight) follow(true);
|
|
191
|
+
remember(viewport);
|
|
192
|
+
};
|
|
193
|
+
return {
|
|
194
|
+
layout,
|
|
195
|
+
pause(viewport) {
|
|
196
|
+
if (viewport.scrollHeight > viewport.clientHeight) follow(false);
|
|
197
|
+
},
|
|
198
|
+
scroll(viewport, dragging = false) {
|
|
199
|
+
if (viewport.clientHeight <= 0) return;
|
|
200
|
+
if (atEnd(viewport) && viewport.scrollTop > lastTop) follow(true);
|
|
201
|
+
else if (movedUp(viewport, dragging) && !atEnd(viewport)) follow(false);
|
|
202
|
+
remember(viewport);
|
|
203
|
+
},
|
|
204
|
+
jump(viewport) {
|
|
205
|
+
follow(true);
|
|
206
|
+
layout(viewport);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// packages/quassel/src/Markdown.tsx
|
|
212
|
+
import { createContext as createContext2, useCallback, useContext as useContext2, useRef as useRef2 } from "react";
|
|
213
|
+
import { Streamdown } from "streamdown";
|
|
214
|
+
import { CheckIcon, CopyIcon } from "lucide-react";
|
|
215
|
+
|
|
216
|
+
// packages/quassel/src/QuasselProvider.tsx
|
|
217
|
+
import { createContext, useContext, useMemo } from "react";
|
|
218
|
+
|
|
219
|
+
// packages/quassel/src/ui/button.tsx
|
|
220
|
+
import { Button as ButtonPrimitive } from "@base-ui/react/button";
|
|
221
|
+
import { cva } from "class-variance-authority";
|
|
222
|
+
import { jsx } from "react/jsx-runtime";
|
|
223
|
+
var buttonVariants = cva(
|
|
224
|
+
"qsl:group/button qsl:inline-flex qsl:shrink-0 qsl:items-center qsl:justify-center qsl:rounded-lg qsl:border qsl:border-transparent qsl:bg-clip-padding qsl:text-sm qsl:font-medium qsl:whitespace-nowrap qsl:transition-all qsl:outline-none qsl:select-none qsl:focus-visible:border-ring qsl:focus-visible:ring-3 qsl:focus-visible:ring-ring/50 qsl:active:not-aria-[haspopup]:translate-y-px qsl:disabled:pointer-events-none qsl:disabled:opacity-50 qsl:aria-invalid:border-destructive qsl:aria-invalid:ring-3 qsl:aria-invalid:ring-destructive/20 qsl:dark:aria-invalid:border-destructive/50 qsl:dark:aria-invalid:ring-destructive/40 qsl:[&_svg]:pointer-events-none qsl:[&_svg]:shrink-0 qsl:[&_svg:not([class*='size-'])]:size-4",
|
|
225
|
+
{
|
|
226
|
+
variants: {
|
|
227
|
+
variant: {
|
|
228
|
+
default: "qsl:bg-primary qsl:text-primary-foreground qsl:hover:bg-primary/80",
|
|
229
|
+
outline: "qsl:border-border qsl:bg-background qsl:hover:bg-muted qsl:hover:text-foreground qsl:aria-expanded:bg-muted qsl:aria-expanded:text-foreground qsl:dark:border-input qsl:dark:bg-input/30 qsl:dark:hover:bg-input/50",
|
|
230
|
+
secondary: "qsl:bg-secondary qsl:text-secondary-foreground qsl:hover:bg-[color-mix(in_oklch,var(--qsl-secondary),var(--qsl-foreground)_5%)] qsl:aria-expanded:bg-secondary qsl:aria-expanded:text-secondary-foreground",
|
|
231
|
+
ghost: "qsl:hover:bg-muted qsl:hover:text-foreground qsl:aria-expanded:bg-muted qsl:aria-expanded:text-foreground qsl:dark:hover:bg-muted/50",
|
|
232
|
+
destructive: "qsl:bg-destructive/10 qsl:text-destructive qsl:hover:bg-destructive/20 qsl:focus-visible:border-destructive/40 qsl:focus-visible:ring-destructive/20 qsl:dark:bg-destructive/20 qsl:dark:hover:bg-destructive/30 qsl:dark:focus-visible:ring-destructive/40",
|
|
233
|
+
link: "qsl:text-primary qsl:underline-offset-4 qsl:hover:underline"
|
|
234
|
+
},
|
|
235
|
+
size: {
|
|
236
|
+
default: "qsl:h-8 qsl:gap-1.5 qsl:px-2.5 qsl:has-data-[icon=inline-end]:pr-2 qsl:has-data-[icon=inline-start]:pl-2",
|
|
237
|
+
xs: "qsl:h-6 qsl:gap-1 qsl:rounded-[min(--theme(--radius-md),10px)] qsl:px-2 qsl:text-xs qsl:in-data-[slot=button-group]:rounded-lg qsl:has-data-[icon=inline-end]:pr-1.5 qsl:has-data-[icon=inline-start]:pl-1.5 qsl:[&_svg:not([class*='size-'])]:size-3",
|
|
238
|
+
sm: "qsl:h-7 qsl:gap-1 qsl:rounded-[min(--theme(--radius-md),12px)] qsl:px-2.5 qsl:in-data-[slot=button-group]:rounded-lg qsl:has-data-[icon=inline-end]:pr-1.5 qsl:has-data-[icon=inline-start]:pl-1.5 qsl:[&_svg:not([class*='size-'])]:size-3.5",
|
|
239
|
+
lg: "qsl:h-9 qsl:gap-1.5 qsl:px-2.5 qsl:has-data-[icon=inline-end]:pr-2 qsl:has-data-[icon=inline-start]:pl-2",
|
|
240
|
+
icon: "qsl:size-8",
|
|
241
|
+
"icon-xs": "qsl:size-6 qsl:rounded-[min(--theme(--radius-md),10px)] qsl:in-data-[slot=button-group]:rounded-lg qsl:[&_svg:not([class*='size-'])]:size-3",
|
|
242
|
+
"icon-sm": "qsl:size-7 qsl:rounded-[min(--theme(--radius-md),12px)] qsl:in-data-[slot=button-group]:rounded-lg",
|
|
243
|
+
"icon-lg": "qsl:size-9"
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
defaultVariants: {
|
|
247
|
+
variant: "default",
|
|
248
|
+
size: "default"
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
);
|
|
252
|
+
function Button({
|
|
253
|
+
className,
|
|
254
|
+
variant = "default",
|
|
255
|
+
size = "default",
|
|
256
|
+
...props
|
|
257
|
+
}) {
|
|
258
|
+
return /* @__PURE__ */ jsx(
|
|
259
|
+
ButtonPrimitive,
|
|
260
|
+
{
|
|
261
|
+
"data-quassel": "",
|
|
262
|
+
"data-slot": "button",
|
|
263
|
+
className: cn(buttonVariants({ variant, size, className })),
|
|
264
|
+
...props
|
|
265
|
+
}
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// packages/quassel/src/ui/card.tsx
|
|
270
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
271
|
+
function Card({
|
|
272
|
+
className,
|
|
273
|
+
size = "default",
|
|
274
|
+
...props
|
|
275
|
+
}) {
|
|
276
|
+
return /* @__PURE__ */ jsx2(
|
|
277
|
+
"div",
|
|
278
|
+
{
|
|
279
|
+
"data-quassel": "",
|
|
280
|
+
"data-slot": "card",
|
|
281
|
+
"data-size": size,
|
|
282
|
+
className: cn(
|
|
283
|
+
"qsl:group/card qsl:flex qsl:flex-col qsl:gap-(--qsl-card-spacing) qsl:overflow-hidden qsl:rounded-panel qsl:border qsl:border-border qsl:bg-card qsl:py-(--qsl-card-spacing) qsl:text-sm qsl:text-card-foreground qsl:shadow-bar qsl:[--qsl-card-spacing:--spacing(4)] qsl:has-data-[slot=card-footer]:pb-0 qsl:has-[>img:first-child]:pt-0 qsl:data-[size=sm]:[--qsl-card-spacing:--spacing(3)] qsl:data-[size=sm]:has-data-[slot=card-footer]:pb-0 qsl:*:[img:first-child]:rounded-t-xl qsl:*:[img:last-child]:rounded-b-xl",
|
|
284
|
+
className
|
|
285
|
+
),
|
|
286
|
+
...props
|
|
287
|
+
}
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// packages/quassel/src/ui/popover.tsx
|
|
292
|
+
import { Popover as PopoverPrimitive } from "@base-ui/react/popover";
|
|
293
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
294
|
+
function Popover({ ...props }) {
|
|
295
|
+
return /* @__PURE__ */ jsx3(PopoverPrimitive.Root, { "data-slot": "popover", ...props });
|
|
296
|
+
}
|
|
297
|
+
function PopoverContent({
|
|
298
|
+
className,
|
|
299
|
+
align = "center",
|
|
300
|
+
alignOffset = 0,
|
|
301
|
+
side = "bottom",
|
|
302
|
+
sideOffset = 8,
|
|
303
|
+
anchor,
|
|
304
|
+
collisionPadding,
|
|
305
|
+
keepMounted,
|
|
306
|
+
portalContainer,
|
|
307
|
+
...props
|
|
308
|
+
}) {
|
|
309
|
+
return /* @__PURE__ */ jsx3(PopoverPrimitive.Portal, { container: portalContainer, keepMounted, children: /* @__PURE__ */ jsx3(
|
|
310
|
+
PopoverPrimitive.Positioner,
|
|
311
|
+
{
|
|
312
|
+
align,
|
|
313
|
+
alignOffset,
|
|
314
|
+
anchor,
|
|
315
|
+
collisionPadding,
|
|
316
|
+
side,
|
|
317
|
+
sideOffset,
|
|
318
|
+
className: "qsl:isolate qsl:z-[110]",
|
|
319
|
+
children: /* @__PURE__ */ jsx3(
|
|
320
|
+
PopoverPrimitive.Popup,
|
|
321
|
+
{
|
|
322
|
+
"data-quassel": "",
|
|
323
|
+
"data-quassel-popover": "",
|
|
324
|
+
"data-slot": "popover-content",
|
|
325
|
+
className: cn(
|
|
326
|
+
"qsl:z-[110] qsl:flex qsl:w-72 qsl:origin-(--transform-origin) qsl:flex-col qsl:gap-2.5 qsl:rounded-lg qsl:bg-popover qsl:p-2.5 qsl:text-sm qsl:text-popover-foreground qsl:shadow-md qsl:ring-1 qsl:ring-foreground/10 qsl:outline-hidden qsl:duration-100 qsl:data-[side=bottom]:slide-in-from-top-2 qsl:data-[side=inline-end]:slide-in-from-left-2 qsl:data-[side=inline-start]:slide-in-from-right-2 qsl:data-[side=left]:slide-in-from-right-2 qsl:data-[side=right]:slide-in-from-left-2 qsl:data-[side=top]:slide-in-from-bottom-2 qsl:data-open:animate-in qsl:data-open:fade-in-0 qsl:data-open:zoom-in-95 qsl:data-closed:animate-out qsl:data-closed:fade-out-0 qsl:data-closed:zoom-out-95",
|
|
327
|
+
className
|
|
328
|
+
),
|
|
329
|
+
...props
|
|
330
|
+
}
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
) });
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// packages/quassel/src/ui/stop-button.tsx
|
|
337
|
+
import { SquareIcon } from "lucide-react";
|
|
338
|
+
|
|
339
|
+
// packages/quassel/src/ui/spinner.tsx
|
|
340
|
+
import { Loader2Icon } from "lucide-react";
|
|
341
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
342
|
+
function Spinner({ className, ...props }) {
|
|
343
|
+
return /* @__PURE__ */ jsx4(Loader2Icon, { "data-slot": "spinner", role: "status", "aria-label": "Loading", className: cn("qsl:size-4 qsl:animate-spin", className), ...props });
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// packages/quassel/src/ui/stop-button.tsx
|
|
347
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
348
|
+
function StopGlyph({ className, ...props }) {
|
|
349
|
+
return /* @__PURE__ */ jsx5(SquareIcon, { "aria-hidden": true, className: cn("qsl:text-destructive", className), fill: "currentColor", ...props });
|
|
350
|
+
}
|
|
351
|
+
var stopButtonClass = "qsl:text-destructive qsl:hover:bg-destructive/10 qsl:hover:text-destructive qsl:dark:hover:bg-destructive/20";
|
|
352
|
+
function StopButton({ label, busy = false, className, title, ...props }) {
|
|
353
|
+
return /* @__PURE__ */ jsx5(Button, { "aria-busy": busy || void 0, "aria-label": label, className: cn(stopButtonClass, className), title: title ?? label, variant: "ghost", ...props, children: busy ? /* @__PURE__ */ jsx5(Spinner, {}) : /* @__PURE__ */ jsx5(StopGlyph, {}) });
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// packages/quassel/src/ui/toggle.tsx
|
|
357
|
+
import { Toggle as TogglePrimitive } from "@base-ui/react/toggle";
|
|
358
|
+
import { cva as cva2 } from "class-variance-authority";
|
|
359
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
360
|
+
var toggleVariants = cva2(
|
|
361
|
+
"qsl:group/toggle qsl:inline-flex qsl:items-center qsl:justify-center qsl:gap-1 qsl:rounded-lg qsl:text-sm qsl:font-medium qsl:whitespace-nowrap qsl:transition-all qsl:outline-none qsl:hover:bg-muted qsl:hover:text-foreground qsl:focus-visible:border-ring qsl:focus-visible:ring-[3px] qsl:focus-visible:ring-ring/50 qsl:disabled:pointer-events-none qsl:disabled:opacity-50 qsl:aria-invalid:border-destructive qsl:aria-invalid:ring-destructive/20 qsl:aria-pressed:bg-muted qsl:data-[state=on]:bg-muted qsl:dark:aria-invalid:ring-destructive/40 qsl:[&_svg]:pointer-events-none qsl:[&_svg]:shrink-0 qsl:[&_svg:not([class*='size-'])]:size-4",
|
|
362
|
+
{
|
|
363
|
+
variants: {
|
|
364
|
+
variant: {
|
|
365
|
+
default: "qsl:bg-transparent",
|
|
366
|
+
outline: "qsl:border qsl:border-input qsl:bg-transparent qsl:hover:bg-muted"
|
|
367
|
+
},
|
|
368
|
+
size: {
|
|
369
|
+
default: "qsl:h-8 qsl:min-w-8 qsl:px-2.5 qsl:has-data-[icon=inline-end]:pr-2 qsl:has-data-[icon=inline-start]:pl-2",
|
|
370
|
+
sm: "qsl:h-7 qsl:min-w-7 qsl:rounded-[min(--theme(--radius-md),12px)] qsl:px-2.5 qsl:has-data-[icon=inline-end]:pr-1.5 qsl:has-data-[icon=inline-start]:pl-1.5 qsl:[&_svg:not([class*='size-'])]:size-3.5",
|
|
371
|
+
lg: "qsl:h-9 qsl:min-w-9 qsl:px-2.5 qsl:has-data-[icon=inline-end]:pr-2 qsl:has-data-[icon=inline-start]:pl-2"
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
defaultVariants: {
|
|
375
|
+
variant: "default",
|
|
376
|
+
size: "default"
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
);
|
|
380
|
+
function Toggle({
|
|
381
|
+
className,
|
|
382
|
+
variant = "default",
|
|
383
|
+
size = "default",
|
|
384
|
+
...props
|
|
385
|
+
}) {
|
|
386
|
+
return /* @__PURE__ */ jsx6(
|
|
387
|
+
TogglePrimitive,
|
|
388
|
+
{
|
|
389
|
+
"data-quassel": "",
|
|
390
|
+
"data-slot": "toggle",
|
|
391
|
+
className: cn(toggleVariants({ variant, size, className })),
|
|
392
|
+
...props
|
|
393
|
+
}
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// packages/quassel/src/QuasselProvider.tsx
|
|
398
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
399
|
+
var defaultComponents = { Button, Toggle, Card, StopButton, Popover, PopoverContent };
|
|
400
|
+
var QuasselContext = createContext({ components: defaultComponents });
|
|
401
|
+
function QuasselProvider({ components: components2, allowUrl, resolveUrl, portalContainer, children }) {
|
|
402
|
+
const outer = useContext(QuasselContext);
|
|
403
|
+
const value = useMemo(() => ({
|
|
404
|
+
components: { ...outer.components, ...components2 },
|
|
405
|
+
allowUrl: allowUrl ?? outer.allowUrl,
|
|
406
|
+
resolveUrl: resolveUrl ?? outer.resolveUrl,
|
|
407
|
+
portalContainer: portalContainer ?? outer.portalContainer
|
|
408
|
+
}), [outer, components2, allowUrl, resolveUrl, portalContainer]);
|
|
409
|
+
return /* @__PURE__ */ jsx7(QuasselContext.Provider, { value, children });
|
|
410
|
+
}
|
|
411
|
+
var useQuasselComponents = () => useContext(QuasselContext).components;
|
|
412
|
+
var useAllowUrl = () => useContext(QuasselContext).allowUrl;
|
|
413
|
+
var useResolveUrl = () => useContext(QuasselContext).resolveUrl;
|
|
414
|
+
function useQuasselPortalContainer() {
|
|
415
|
+
const container = useContext(QuasselContext).portalContainer;
|
|
416
|
+
const target = typeof container === "function" ? container() : container && "current" in container ? container.current : container;
|
|
417
|
+
return target ?? void 0;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// packages/quassel/src/useChatAction.ts
|
|
421
|
+
import { useEffect, useRef, useState } from "react";
|
|
422
|
+
function useChatAction() {
|
|
423
|
+
const pending = useRef(false);
|
|
424
|
+
const [status, setStatus] = useState("idle");
|
|
425
|
+
const [error, setError] = useState();
|
|
426
|
+
useEffect(() => {
|
|
427
|
+
if (status !== "done") return;
|
|
428
|
+
const timer = window.setTimeout(() => setStatus("idle"), 1800);
|
|
429
|
+
return () => window.clearTimeout(timer);
|
|
430
|
+
}, [status]);
|
|
431
|
+
const invoke = async (action, failureLabel) => {
|
|
432
|
+
if (pending.current) return;
|
|
433
|
+
pending.current = true;
|
|
434
|
+
setError(void 0);
|
|
435
|
+
setStatus("pending");
|
|
436
|
+
try {
|
|
437
|
+
await action();
|
|
438
|
+
setStatus("done");
|
|
439
|
+
} catch (failure) {
|
|
440
|
+
const detail = failure instanceof Error ? failure.message : String(failure);
|
|
441
|
+
setError(`${failureLabel}: ${detail}`);
|
|
442
|
+
setStatus("idle");
|
|
443
|
+
} finally {
|
|
444
|
+
pending.current = false;
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
return { status, error, invoke };
|
|
448
|
+
}
|
|
449
|
+
async function copyChatText(text, unavailable) {
|
|
450
|
+
if (!navigator.clipboard) throw new Error(unavailable);
|
|
451
|
+
await navigator.clipboard.writeText(text);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// packages/quassel/src/Markdown.tsx
|
|
455
|
+
import { jsx as jsx8, jsxs } from "react/jsx-runtime";
|
|
456
|
+
var LinkClickContext = createContext2(void 0);
|
|
457
|
+
var CodeBlockContext = createContext2({ texts: defaultTexts });
|
|
458
|
+
function MarkdownCodeBlocks({ children, options, texts }) {
|
|
459
|
+
return /* @__PURE__ */ jsx8(CodeBlockContext.Provider, { value: { options, texts }, children });
|
|
460
|
+
}
|
|
461
|
+
function CodeBlock({ children }) {
|
|
462
|
+
const { options, texts } = useContext2(CodeBlockContext);
|
|
463
|
+
const { Button: Button2 } = useQuasselComponents();
|
|
464
|
+
const code = useRef2(null);
|
|
465
|
+
const action = useChatAction();
|
|
466
|
+
const label = action.status === "done" ? texts.copied : texts.copyCode;
|
|
467
|
+
const block = /* @__PURE__ */ jsx8(
|
|
468
|
+
"pre",
|
|
469
|
+
{
|
|
470
|
+
ref: code,
|
|
471
|
+
"data-chat": "code-block",
|
|
472
|
+
className: cn(scrollbar, "qsl:my-2 qsl:overflow-auto qsl:overscroll-contain qsl:rounded-lg qsl:bg-secondary qsl:px-3 qsl:py-2 qsl:font-mono qsl:text-[length:1em] qsl:leading-relaxed qsl:in-data-[tone=on-color]:bg-black/30 qsl:in-data-[tone=on-color]:text-inherit"),
|
|
473
|
+
style: { maxHeight: options?.maxHeight, whiteSpace: options?.wrap ? "pre-wrap" : void 0, overflowWrap: options?.wrap ? "anywhere" : void 0 },
|
|
474
|
+
children
|
|
475
|
+
}
|
|
476
|
+
);
|
|
477
|
+
if (!options?.showCopyButton) return block;
|
|
478
|
+
return /* @__PURE__ */ jsxs("div", { className: "qsl:relative", children: [
|
|
479
|
+
block,
|
|
480
|
+
/* @__PURE__ */ jsx8(
|
|
481
|
+
Button2,
|
|
482
|
+
{
|
|
483
|
+
"aria-label": label,
|
|
484
|
+
className: "qsl:absolute qsl:top-1 qsl:right-1 qsl:bg-secondary",
|
|
485
|
+
disabled: action.status === "pending",
|
|
486
|
+
size: "icon-sm",
|
|
487
|
+
title: label,
|
|
488
|
+
variant: "ghost",
|
|
489
|
+
onClick: () => {
|
|
490
|
+
void action.invoke(() => copyChatText(code.current?.textContent ?? "", texts.clipboardUnavailable), texts.actionFailed);
|
|
491
|
+
},
|
|
492
|
+
children: action.status === "done" ? /* @__PURE__ */ jsx8(CheckIcon, {}) : /* @__PURE__ */ jsx8(CopyIcon, {})
|
|
493
|
+
}
|
|
494
|
+
),
|
|
495
|
+
action.error && /* @__PURE__ */ jsx8("p", { className: "qsl:text-sm qsl:text-destructive", role: "alert", children: action.error })
|
|
496
|
+
] });
|
|
497
|
+
}
|
|
498
|
+
function MarkdownLinks({ children, onLinkClick }) {
|
|
499
|
+
return /* @__PURE__ */ jsx8(LinkClickContext.Provider, { value: onLinkClick, children });
|
|
500
|
+
}
|
|
501
|
+
function MarkdownLink({ href, children }) {
|
|
502
|
+
const onLinkClick = useContext2(LinkClickContext);
|
|
503
|
+
if (!href || href === "streamdown:incomplete-link") {
|
|
504
|
+
return /* @__PURE__ */ jsx8("span", { children });
|
|
505
|
+
}
|
|
506
|
+
return /* @__PURE__ */ jsx8(
|
|
507
|
+
"a",
|
|
508
|
+
{
|
|
509
|
+
className: "qsl:text-primary qsl:[text-underline-offset:2px] qsl:in-data-[tone=on-color]:text-inherit qsl:in-data-[tone=on-color]:underline",
|
|
510
|
+
href,
|
|
511
|
+
onClick: (event) => {
|
|
512
|
+
if (href && onLinkClick?.(href, event.currentTarget.textContent ?? "")) {
|
|
513
|
+
event.preventDefault();
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
rel: "noreferrer",
|
|
517
|
+
target: "_blank",
|
|
518
|
+
children
|
|
519
|
+
}
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
var scrollbar = "qsl:[scrollbar-width:thin] qsl:[&::-webkit-scrollbar]:block qsl:[&::-webkit-scrollbar]:h-2 qsl:[&::-webkit-scrollbar-thumb]:rounded-full qsl:[&::-webkit-scrollbar-thumb]:bg-border";
|
|
523
|
+
var scrollCover = "qsl:bg-[linear-gradient(to_right,var(--qsl-scroll-cover,var(--qsl-background))_50%,transparent)_left/20px_100%_no-repeat_local,linear-gradient(to_left,var(--qsl-scroll-cover,var(--qsl-background))_50%,transparent)_right/20px_100%_no-repeat_local,linear-gradient(to_right,#00000047,transparent)_left/14px_100%_no-repeat_scroll,linear-gradient(to_left,#00000047,transparent)_right/14px_100%_no-repeat_scroll]";
|
|
524
|
+
var headingClasses = "qsl:mt-3 qsl:mb-1 qsl:text-[length:1.1em] qsl:font-medium";
|
|
525
|
+
var cellClasses = "qsl:max-w-[44ch] qsl:border qsl:border-border qsl:px-2 qsl:py-1 qsl:text-left qsl:align-top qsl:in-data-[tone=on-color]:border-white/35";
|
|
526
|
+
var components = {
|
|
527
|
+
h1: ({ children }) => /* @__PURE__ */ jsx8("h1", { className: "qsl:mt-3 qsl:mb-1 qsl:text-[length:1.2em] qsl:font-semibold", children }),
|
|
528
|
+
h2: ({ children }) => /* @__PURE__ */ jsx8("h2", { className: headingClasses, children }),
|
|
529
|
+
h3: ({ children }) => /* @__PURE__ */ jsx8("h3", { className: headingClasses, children }),
|
|
530
|
+
h4: ({ children }) => /* @__PURE__ */ jsx8("h4", { className: "qsl:mt-2 qsl:mb-1 qsl:font-medium", children }),
|
|
531
|
+
h5: ({ children }) => /* @__PURE__ */ jsx8("h5", { className: "qsl:mt-2 qsl:mb-1 qsl:font-medium", children }),
|
|
532
|
+
h6: ({ children }) => /* @__PURE__ */ jsx8("h6", { className: "qsl:mt-2 qsl:mb-1 qsl:font-medium", children }),
|
|
533
|
+
p: ({ children }) => /* @__PURE__ */ jsx8("p", { className: "qsl:[p+&]:mt-2", children }),
|
|
534
|
+
ul: ({ children }) => /* @__PURE__ */ jsx8("ul", { className: "qsl:my-2 qsl:list-disc qsl:pl-6 qsl:[li>&]:my-0", children }),
|
|
535
|
+
ol: ({ children }) => /* @__PURE__ */ jsx8("ol", { className: "qsl:my-2 qsl:list-decimal qsl:pl-6 qsl:[li>&]:my-0", children }),
|
|
536
|
+
li: ({ children }) => /* @__PURE__ */ jsx8("li", { className: "qsl:marker:text-muted-foreground qsl:in-data-[tone=on-color]:marker:text-inherit", children }),
|
|
537
|
+
hr: () => /* @__PURE__ */ jsx8("hr", { className: "qsl:my-3 qsl:border-t qsl:border-border" }),
|
|
538
|
+
img: ({ src, alt }) => /* @__PURE__ */ jsx8("img", { alt, className: "qsl:h-auto qsl:max-w-full", src: typeof src === "string" ? src : void 0 }),
|
|
539
|
+
blockquote: ({ children }) => /* @__PURE__ */ jsx8("blockquote", { className: "qsl:my-1 qsl:border-l-2 qsl:border-border qsl:pl-3 qsl:text-muted-foreground qsl:in-data-[tone=on-color]:border-white/50 qsl:in-data-[tone=on-color]:text-inherit", children }),
|
|
540
|
+
strong: ({ children }) => /* @__PURE__ */ jsx8("strong", { className: "qsl:font-medium", children }),
|
|
541
|
+
em: ({ children }) => /* @__PURE__ */ jsx8("em", { children }),
|
|
542
|
+
a: ({ href, children }) => /* @__PURE__ */ jsx8(MarkdownLink, { href, children }),
|
|
543
|
+
pre: ({ children }) => /* @__PURE__ */ jsx8(CodeBlock, { children }),
|
|
544
|
+
code: ({ children, className }) => /* @__PURE__ */ jsx8("code", { className: cn(className, "qsl:rounded-sm qsl:bg-secondary qsl:px-1 qsl:py-0.5 qsl:font-mono qsl:text-[length:1em] qsl:[overflow-wrap:anywhere] qsl:in-data-[tone=on-color]:bg-white/20 qsl:in-data-[tone=on-color]:text-inherit", "qsl:[pre>&]:bg-transparent qsl:[pre>&]:p-0"), children }),
|
|
545
|
+
// Without max-content the table shrinks to the container and stacks letters.
|
|
546
|
+
table: ({ children }) => /* @__PURE__ */ jsx8("div", { className: cn(scrollbar, scrollCover, "qsl:my-2 qsl:overflow-x-auto qsl:overscroll-x-contain"), children: /* @__PURE__ */ jsx8("table", { className: "qsl:w-max qsl:min-w-full qsl:border-collapse qsl:text-sm", children }) }),
|
|
547
|
+
thead: ({ children }) => /* @__PURE__ */ jsx8("thead", { children }),
|
|
548
|
+
tbody: ({ children }) => /* @__PURE__ */ jsx8("tbody", { children }),
|
|
549
|
+
tr: ({ children }) => /* @__PURE__ */ jsx8("tr", { children }),
|
|
550
|
+
th: ({ children }) => /* @__PURE__ */ jsx8("th", { className: cn(cellClasses, "qsl:bg-secondary qsl:font-medium qsl:in-data-[tone=on-color]:bg-white/15 qsl:in-data-[tone=on-color]:text-inherit"), children }),
|
|
551
|
+
td: ({ children }) => /* @__PURE__ */ jsx8("td", { className: cellClasses, children })
|
|
552
|
+
};
|
|
553
|
+
function transformUrl(url, allowUrl, resolveUrl) {
|
|
554
|
+
const resolved = resolveUrl ? resolveUrl(url) : url;
|
|
555
|
+
if (allowUrl?.(resolved)) return resolved;
|
|
556
|
+
const scheme = /^[^/?#]*:/.exec(resolved);
|
|
557
|
+
return !scheme || /^(https?|mailto|tel|ftp|irc|ircs|xmpp):$/i.test(scheme[0]) ? resolved : "";
|
|
558
|
+
}
|
|
559
|
+
function Markdown({ text, streaming = false }) {
|
|
560
|
+
const allowUrl = useAllowUrl();
|
|
561
|
+
const resolveUrl = useResolveUrl();
|
|
562
|
+
const urlTransform = useCallback((url) => transformUrl(url, allowUrl, resolveUrl), [allowUrl, resolveUrl]);
|
|
563
|
+
return /* @__PURE__ */ jsx8(
|
|
564
|
+
Streamdown,
|
|
565
|
+
{
|
|
566
|
+
className: "space-y-0 text-inherit",
|
|
567
|
+
prefix: "qsl",
|
|
568
|
+
mode: streaming ? "streaming" : "static",
|
|
569
|
+
isAnimating: streaming,
|
|
570
|
+
parseIncompleteMarkdown: streaming,
|
|
571
|
+
components,
|
|
572
|
+
controls: false,
|
|
573
|
+
rehypePlugins: [],
|
|
574
|
+
urlTransform,
|
|
575
|
+
skipHtml: true,
|
|
576
|
+
children: text
|
|
577
|
+
}
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
function markdownPlainText(text) {
|
|
581
|
+
return text.split("\n").filter((line) => !/^\s*```\w*\s*$/.test(line) && !isTableDivider(line)).map((line) => (line.trim().startsWith("|") ? splitTableRow(line).join(", ") : line).replace(/^(#{1,6})\s+/, "").replace(/^\s*>\s?/, "").replace(/^\s*([-*+]|\d+[.)])\s+/, "").replace(/!?\[([^\]]*)\]\(([^)]+)\)|\*\*([^*]+)\*\*|__([^_]+)__|\*([^*]+)\*|`([^`]+)`/g, (_, link, _href, bold, boldUnderscore, italic, code) => link ?? bold ?? boldUnderscore ?? italic ?? code).trim()).filter((line) => line !== "").join("\n");
|
|
582
|
+
}
|
|
583
|
+
function splitTableRow(line) {
|
|
584
|
+
let row = line.trim();
|
|
585
|
+
if (row.startsWith("|")) {
|
|
586
|
+
row = row.slice(1);
|
|
587
|
+
}
|
|
588
|
+
if (row.endsWith("|")) {
|
|
589
|
+
row = row.slice(0, -1);
|
|
590
|
+
}
|
|
591
|
+
return row.split("|").map((cell) => cell.trim());
|
|
592
|
+
}
|
|
593
|
+
function isTableDivider(line) {
|
|
594
|
+
return /^\s*\|?\s*:?-{1,}:?\s*(\|\s*:?-{1,}:?\s*)*\|?\s*$/.test(line) && line.includes("-");
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// packages/quassel/src/StepPopover.tsx
|
|
598
|
+
import { useMemo as useMemo2 } from "react";
|
|
599
|
+
import { SparklesIcon, WrenchIcon, XIcon } from "lucide-react";
|
|
600
|
+
import { jsx as jsx9, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
601
|
+
var stepPopoverOrigin = (event) => ({
|
|
602
|
+
x: event.clientX,
|
|
603
|
+
y: event.clientY,
|
|
604
|
+
base: getComputedStyle(event.currentTarget).getPropertyValue("--qsl-base")
|
|
605
|
+
});
|
|
606
|
+
var preClasses = "qsl:rounded-lg qsl:bg-secondary qsl:p-3 qsl:font-mono qsl:text-trace qsl:leading-[1.625] qsl:break-all qsl:whitespace-pre-wrap";
|
|
607
|
+
function StepPopover({
|
|
608
|
+
message,
|
|
609
|
+
position,
|
|
610
|
+
texts,
|
|
611
|
+
toolArgumentsText,
|
|
612
|
+
onClose
|
|
613
|
+
}) {
|
|
614
|
+
const anchor = useMemo2(() => ({
|
|
615
|
+
getBoundingClientRect: () => new DOMRect(
|
|
616
|
+
Math.max(16, Math.min(position.x, window.innerWidth - 16)),
|
|
617
|
+
Math.max(16, Math.min(position.y, window.innerHeight - 16)),
|
|
618
|
+
0,
|
|
619
|
+
0
|
|
620
|
+
),
|
|
621
|
+
contextElement: document.documentElement
|
|
622
|
+
}), [position]);
|
|
623
|
+
const { Button: Button2, Popover: Popover2, PopoverContent: PopoverContent2 } = useQuasselComponents();
|
|
624
|
+
const portalContainer = useQuasselPortalContainer();
|
|
625
|
+
const tool = message.tool;
|
|
626
|
+
const thinking = message.role === "thinking";
|
|
627
|
+
const title = thinking ? texts.thinkingTitle : texts.toolTitle;
|
|
628
|
+
return /* @__PURE__ */ jsx9(Popover2, { open: true, onOpenChange: (open) => {
|
|
629
|
+
if (!open) onClose();
|
|
630
|
+
}, children: /* @__PURE__ */ jsxs2(
|
|
631
|
+
PopoverContent2,
|
|
632
|
+
{
|
|
633
|
+
align: "start",
|
|
634
|
+
anchor,
|
|
635
|
+
"aria-label": title,
|
|
636
|
+
className: cn(
|
|
637
|
+
"qsl:w-[min(680px,calc(100vw-32px))] qsl:gap-0 qsl:overflow-hidden qsl:p-0 qsl:text-base qsl:leading-normal qsl:shadow-pop",
|
|
638
|
+
"qsl:max-h-[min(70dvh,620px,var(--available-height))]"
|
|
639
|
+
),
|
|
640
|
+
collisionPadding: 16,
|
|
641
|
+
portalContainer,
|
|
642
|
+
side: "bottom",
|
|
643
|
+
sideOffset: 10,
|
|
644
|
+
style: { "--qsl-base": position.base || void 0 },
|
|
645
|
+
children: [
|
|
646
|
+
/* @__PURE__ */ jsxs2("div", { className: "qsl:flex qsl:flex-none qsl:items-center qsl:gap-2 qsl:border-b qsl:border-border-soft qsl:px-4 qsl:py-2.5", children: [
|
|
647
|
+
thinking ? /* @__PURE__ */ jsx9(SparklesIcon, { className: "qsl:text-muted-foreground", size: 14 }) : /* @__PURE__ */ jsx9(WrenchIcon, { className: tool?.isError ? "qsl:text-destructive" : "qsl:text-muted-foreground", size: 14 }),
|
|
648
|
+
/* @__PURE__ */ jsx9("span", { className: "qsl:min-w-0 qsl:flex-1 qsl:overflow-hidden qsl:text-ellipsis qsl:whitespace-nowrap qsl:font-medium", children: title }),
|
|
649
|
+
/* @__PURE__ */ jsx9(Button2, { "aria-label": texts.close, className: "qsl:flex-none", onClick: onClose, size: "icon-sm", title: texts.close, variant: "ghost", children: /* @__PURE__ */ jsx9(XIcon, {}) })
|
|
650
|
+
] }),
|
|
651
|
+
/* @__PURE__ */ jsx9("div", { className: "qsl:min-h-0 qsl:flex-1 qsl:overflow-auto qsl:overscroll-contain qsl:p-4", children: thinking ? /* @__PURE__ */ jsx9("div", { className: "qsl:text-sm qsl:leading-[1.625] qsl:whitespace-pre-wrap", children: message.text }) : tool ? /* @__PURE__ */ jsxs2("div", { className: "qsl:flex qsl:flex-col qsl:gap-4", children: [
|
|
652
|
+
/* @__PURE__ */ jsx9("div", { className: "qsl:font-mono qsl:text-sm", children: message.text }),
|
|
653
|
+
/* @__PURE__ */ jsx9(PopoverSection, { label: texts.argumentsLabel, value: toolArgumentsText(tool) }),
|
|
654
|
+
tool.result === void 0 ? /* @__PURE__ */ jsx9("div", { className: "qsl:text-sm qsl:text-muted-foreground", children: texts.toolStillRunning }) : /* @__PURE__ */ jsx9(PopoverSection, { error: tool.isError, label: texts.resultLabel, value: prettyJson(tool.result) })
|
|
655
|
+
] }) : null })
|
|
656
|
+
]
|
|
657
|
+
}
|
|
658
|
+
) });
|
|
659
|
+
}
|
|
660
|
+
function PopoverSection({ label, value, error = false }) {
|
|
661
|
+
return /* @__PURE__ */ jsxs2("section", { children: [
|
|
662
|
+
/* @__PURE__ */ jsx9("h3", { className: "qsl:mb-1.5 qsl:text-trace qsl:font-semibold qsl:tracking-[0.04em] qsl:text-muted-foreground qsl:uppercase", children: label }),
|
|
663
|
+
/* @__PURE__ */ jsx9("pre", { className: cn(preClasses, error && "qsl:text-destructive"), children: value })
|
|
664
|
+
] });
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// packages/quassel/src/PendingActionCard.tsx
|
|
668
|
+
import { CheckIcon as CheckIcon2, XIcon as XIcon2 } from "lucide-react";
|
|
669
|
+
import { jsx as jsx10, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
670
|
+
var cardClasses = "qsl:gap-2.5 qsl:bg-background qsl:px-3.5 qsl:text-sm";
|
|
671
|
+
var resultText = (result) => typeof result === "string" ? result : result === null || result === void 0 ? "" : JSON.stringify(result);
|
|
672
|
+
function PendingActionCard({
|
|
673
|
+
text,
|
|
674
|
+
action,
|
|
675
|
+
waitingLabel,
|
|
676
|
+
dismissLabel,
|
|
677
|
+
dismissedLabel,
|
|
678
|
+
onDismiss
|
|
679
|
+
}) {
|
|
680
|
+
const { Button: Button2, Card: Card2 } = useQuasselComponents();
|
|
681
|
+
if (action.status !== void 0) {
|
|
682
|
+
return /* @__PURE__ */ jsxs3(Card2, { className: cardClasses, size: "sm", children: [
|
|
683
|
+
/* @__PURE__ */ jsx10("div", { className: "qsl:leading-normal", children: text }),
|
|
684
|
+
/* @__PURE__ */ jsxs3("div", { className: "qsl:flex qsl:items-center qsl:gap-1.5 qsl:text-sm qsl:text-muted-foreground", "data-action": "resolved", children: [
|
|
685
|
+
action.status === "approved" ? /* @__PURE__ */ jsx10(CheckIcon2, { className: "qsl:text-success", size: 12 }) : /* @__PURE__ */ jsx10(XIcon2, { size: 12 }),
|
|
686
|
+
action.status === "approved" ? resultText(action.result) : dismissedLabel
|
|
687
|
+
] })
|
|
688
|
+
] });
|
|
689
|
+
}
|
|
690
|
+
return /* @__PURE__ */ jsxs3(Card2, { className: cardClasses, size: "sm", children: [
|
|
691
|
+
/* @__PURE__ */ jsx10("div", { className: "qsl:leading-normal", children: text }),
|
|
692
|
+
/* @__PURE__ */ jsxs3("div", { className: "qsl:flex qsl:items-center qsl:justify-between qsl:gap-2", children: [
|
|
693
|
+
/* @__PURE__ */ jsx10("span", { className: "qsl:text-sm qsl:text-muted-foreground", "data-action": "waiting", children: waitingLabel }),
|
|
694
|
+
onDismiss && /* @__PURE__ */ jsx10(Button2, { onClick: onDismiss, size: "sm", variant: "outline", children: dismissLabel })
|
|
695
|
+
] })
|
|
696
|
+
] });
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// packages/quassel/src/WorkingScenes.tsx
|
|
700
|
+
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
701
|
+
import { jsx as jsx11, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
702
|
+
var TRACK = 13;
|
|
703
|
+
var BASE_TICK_MS = 90;
|
|
704
|
+
var SCENE_CHANGE_MS = 2600;
|
|
705
|
+
function track(figures, width = TRACK) {
|
|
706
|
+
return Array.from({ length: width }, (_, x) => figures[x] ?? " ");
|
|
707
|
+
}
|
|
708
|
+
var SCENES = [
|
|
709
|
+
{
|
|
710
|
+
id: "thinking",
|
|
711
|
+
tick: 2,
|
|
712
|
+
draw: (step) => {
|
|
713
|
+
const tick = step % 10;
|
|
714
|
+
if (tick >= 8) return "\u{1F929}\u{1F4A1}";
|
|
715
|
+
return "\u{1F914}" + "\u{1F4AD}".repeat(Math.min(tick, 3));
|
|
716
|
+
}
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
// Flies to the right, dragging its tail behind.
|
|
720
|
+
id: "rocket",
|
|
721
|
+
tick: 2,
|
|
722
|
+
moving: true,
|
|
723
|
+
draw: (step) => {
|
|
724
|
+
const x = step % TRACK;
|
|
725
|
+
return track({ [x - 2]: "\xB7", [x - 1]: "\u{1F4A8}", [x]: "\u{1F680}" });
|
|
726
|
+
}
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
// The dots stay put and are eaten one by one.
|
|
730
|
+
id: "pacman",
|
|
731
|
+
tick: 2,
|
|
732
|
+
moving: true,
|
|
733
|
+
draw: (step) => {
|
|
734
|
+
const x = step % TRACK;
|
|
735
|
+
const figures = {};
|
|
736
|
+
for (let i = x + 1; i < TRACK; i++) {
|
|
737
|
+
figures[i] = "\xB7";
|
|
738
|
+
}
|
|
739
|
+
figures[x] = step % 2 === 0 ? "\u{1F7E1}" : "\u{1F315}";
|
|
740
|
+
return track(figures);
|
|
741
|
+
}
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
// A seed becomes a tree, then start over.
|
|
745
|
+
id: "growing",
|
|
746
|
+
compact: true,
|
|
747
|
+
tick: 2,
|
|
748
|
+
draw: (step) => {
|
|
749
|
+
const stages = ["\u{1F331}", "\u{1F331}", "\u{1F33F}", "\u{1F33F}", "\u{1F343}", "\u{1F333}", "\u{1F333}", "\u{1F333}"];
|
|
750
|
+
return stages[step % stages.length];
|
|
751
|
+
}
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
// The egg cracks, the chick hatches.
|
|
755
|
+
id: "hatching",
|
|
756
|
+
compact: true,
|
|
757
|
+
tick: 2,
|
|
758
|
+
draw: (step) => {
|
|
759
|
+
const tick = step % 12;
|
|
760
|
+
if (tick < 4) return "\u{1F95A}";
|
|
761
|
+
if (tick < 6) return "\u{1F95A}*";
|
|
762
|
+
if (tick < 9) return "\u{1F423}";
|
|
763
|
+
return "\u{1F425}";
|
|
764
|
+
}
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
// Morse-like signal that resolves into an OK.
|
|
768
|
+
id: "signal",
|
|
769
|
+
tick: 2,
|
|
770
|
+
draw: (step) => {
|
|
771
|
+
const tick = step % 14;
|
|
772
|
+
if (tick >= 11) return "\u{1F4E1} OK";
|
|
773
|
+
const pattern = ["\xB7", "\xB7\xB7", "\xB7-", "-\xB7", "\xB7\xB7-", "-\xB7\xB7"];
|
|
774
|
+
return "\u{1F4E1} " + pattern[tick % pattern.length];
|
|
775
|
+
}
|
|
776
|
+
},
|
|
777
|
+
{
|
|
778
|
+
// Hourglass: the sand runs through, then it is flipped.
|
|
779
|
+
id: "hourglass",
|
|
780
|
+
tick: 2,
|
|
781
|
+
draw: (step) => {
|
|
782
|
+
const tick = step % 12;
|
|
783
|
+
const grains = "\xB7".repeat(Math.max(0, 5 - Math.floor(tick / 2)));
|
|
784
|
+
return (tick < 10 ? "\u23F3" : "\u231B") + " " + grains;
|
|
785
|
+
}
|
|
786
|
+
},
|
|
787
|
+
{
|
|
788
|
+
// Egg, chick, hen - and she lays the next egg.
|
|
789
|
+
id: "chicken-cycle",
|
|
790
|
+
compact: true,
|
|
791
|
+
tick: 3,
|
|
792
|
+
draw: (step) => {
|
|
793
|
+
const tick = step % 16;
|
|
794
|
+
if (tick < 3) return "\u{1F95A}";
|
|
795
|
+
if (tick < 5) return "\u{1F95A}*";
|
|
796
|
+
if (tick < 8) return "\u{1F423}";
|
|
797
|
+
if (tick < 11) return "\u{1F424}";
|
|
798
|
+
if (tick < 14) return "\u{1F414}";
|
|
799
|
+
return "\u{1F414}\u{1F95A}";
|
|
800
|
+
}
|
|
801
|
+
},
|
|
802
|
+
{
|
|
803
|
+
// Hat, sparks - and the rabbit bolts.
|
|
804
|
+
id: "magic-hat",
|
|
805
|
+
compact: true,
|
|
806
|
+
tick: 3,
|
|
807
|
+
draw: (step) => {
|
|
808
|
+
const tick = step % 14;
|
|
809
|
+
if (tick < 4) return "\u{1F3A9}";
|
|
810
|
+
if (tick < 7) return "\u{1F3A9}*";
|
|
811
|
+
if (tick < 11) return "\u{1F3A9}\u{1F407}";
|
|
812
|
+
return "\u{1F407}\u{1F4A8}";
|
|
813
|
+
}
|
|
814
|
+
},
|
|
815
|
+
{
|
|
816
|
+
// Red, yellow, green - and off.
|
|
817
|
+
id: "traffic-light",
|
|
818
|
+
compact: true,
|
|
819
|
+
tick: 3,
|
|
820
|
+
draw: (step) => {
|
|
821
|
+
const tick = step % 12;
|
|
822
|
+
if (tick < 4) return "\u{1F534}";
|
|
823
|
+
if (tick < 6) return "\u{1F7E1}";
|
|
824
|
+
if (tick < 8) return "\u{1F7E2}";
|
|
825
|
+
return "\u{1F697}\u{1F4A8}";
|
|
826
|
+
}
|
|
827
|
+
},
|
|
828
|
+
{
|
|
829
|
+
// Ringing, oversleeping, running off.
|
|
830
|
+
id: "alarm-clock",
|
|
831
|
+
compact: true,
|
|
832
|
+
tick: 2,
|
|
833
|
+
draw: (step) => {
|
|
834
|
+
const tick = step % 14;
|
|
835
|
+
if (tick < 5) return tick % 2 === 0 ? "\u23F0" : "\u{1F514}";
|
|
836
|
+
if (tick < 8) return "\u{1F634}";
|
|
837
|
+
if (tick < 10) return "\u{1F633}";
|
|
838
|
+
return "\u{1F3C3}\u{1F4A8}";
|
|
839
|
+
}
|
|
840
|
+
},
|
|
841
|
+
{
|
|
842
|
+
// A snail crawls to the flag - and celebrates.
|
|
843
|
+
id: "snail",
|
|
844
|
+
tick: 4,
|
|
845
|
+
draw: (step) => {
|
|
846
|
+
const tick = step % 8;
|
|
847
|
+
if (tick < 5) return track({ [tick]: "\u{1F40C}", 4: "\u{1F3C1}" }, 5);
|
|
848
|
+
return track({ 4: "\u{1F389}" }, 5);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
];
|
|
852
|
+
var COMPACT_SCENES = SCENES.filter((scene) => scene.compact);
|
|
853
|
+
function WorkingScenes({ label = "Working ...", compact = false }) {
|
|
854
|
+
const scenes = compact ? COMPACT_SCENES : SCENES;
|
|
855
|
+
const [sceneIndex, setSceneIndex] = useState2(() => Math.floor(Math.random() * scenes.length));
|
|
856
|
+
const [offset, setOffset] = useState2(() => Math.floor(Math.random() * 20));
|
|
857
|
+
const [frameTick, setFrameTick] = useState2(0);
|
|
858
|
+
const [reducedMotion, setReducedMotion] = useState2(() => typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches);
|
|
859
|
+
const scene = scenes[sceneIndex % scenes.length];
|
|
860
|
+
useEffect2(() => {
|
|
861
|
+
const preference = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
862
|
+
const update = () => setReducedMotion(preference.matches);
|
|
863
|
+
preference.addEventListener("change", update);
|
|
864
|
+
update();
|
|
865
|
+
return () => preference.removeEventListener("change", update);
|
|
866
|
+
}, []);
|
|
867
|
+
useEffect2(() => {
|
|
868
|
+
if (reducedMotion) return;
|
|
869
|
+
const timer = window.setInterval(() => {
|
|
870
|
+
setSceneIndex((previous) => (previous + 1 + Math.floor(Math.random() * (scenes.length - 1))) % scenes.length);
|
|
871
|
+
setOffset(Math.floor(Math.random() * 20));
|
|
872
|
+
setFrameTick(0);
|
|
873
|
+
}, SCENE_CHANGE_MS);
|
|
874
|
+
return () => window.clearInterval(timer);
|
|
875
|
+
}, [reducedMotion, scenes]);
|
|
876
|
+
useEffect2(() => {
|
|
877
|
+
if (reducedMotion) return;
|
|
878
|
+
const timer = window.setInterval(() => setFrameTick((value) => value + 1), BASE_TICK_MS);
|
|
879
|
+
return () => window.clearInterval(timer);
|
|
880
|
+
}, [reducedMotion]);
|
|
881
|
+
const frame = scene.draw(Math.floor(frameTick / scene.tick) + (scene.moving ? offset : 0));
|
|
882
|
+
return /* @__PURE__ */ jsxs4("span", { "aria-label": label, className: cn("qsl:flex qsl:items-center qsl:gap-2.5 qsl:text-chat", compact ? "qsl:text-inherit" : "qsl:mt-4 qsl:text-foreground"), "data-chat": "working", role: "status", children: [
|
|
883
|
+
!compact && /* @__PURE__ */ jsx11("span", { "aria-hidden": "true", className: "qsl:text-muted-foreground qsl:opacity-70", children: ">" }),
|
|
884
|
+
/* @__PURE__ */ jsx11("span", { "aria-hidden": "true", className: cn("qsl:inline-block qsl:flex-none qsl:font-mono qsl:[font-variant-ligatures:none]", compact ? "qsl:w-[2.4em] qsl:text-center" : "qsl:w-[15rem]"), children: /* @__PURE__ */ jsx11("span", { className: "qsl:tracking-[0.02em] qsl:whitespace-pre qsl:motion-reduce:opacity-60", children: typeof frame === "string" ? frame : frame.map((cell, index) => /* @__PURE__ */ jsx11("span", { className: "qsl:inline-block qsl:w-[1.35em] qsl:text-center", children: cell }, index)) }) }, scene.id)
|
|
885
|
+
] });
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// packages/quassel/src/attachments.ts
|
|
889
|
+
var maxAttachmentSizeLabel = `${MAX_CHAT_ATTACHMENT_BYTES / (1024 * 1024)} MiB`;
|
|
890
|
+
function attachmentMediaType(file) {
|
|
891
|
+
return chatAttachmentMediaType(file.name, file.type);
|
|
892
|
+
}
|
|
893
|
+
function attachmentCapabilityError(files, capabilities, texts = defaultTexts) {
|
|
894
|
+
for (const file of files) {
|
|
895
|
+
const required = file.mediaType.startsWith("image/") ? "image" : file.mediaType.startsWith("video/") ? "video" : file.mediaType === "application/pdf" ? "file" : void 0;
|
|
896
|
+
if (required && !capabilities.input.includes(required)) {
|
|
897
|
+
const kind = required === "image" ? texts.attachmentKindImage : required === "video" ? texts.attachmentKindVideo : texts.attachmentKindPdf;
|
|
898
|
+
if (!capabilities.model) return fillText(texts.attachmentUnsupported, { name: file.name, kind });
|
|
899
|
+
return fillText(texts.attachmentUnsupportedByModel, { model: capabilities.model, name: file.name, kind });
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
function validateAttachmentSelection(existing, incoming, texts = defaultTexts) {
|
|
904
|
+
if (existing.length + incoming.length > MAX_CHAT_ATTACHMENTS) {
|
|
905
|
+
throw new Error(fillText(texts.tooManyAttachments, { count: MAX_CHAT_ATTACHMENTS }));
|
|
906
|
+
}
|
|
907
|
+
if ([...existing, ...incoming].reduce((total, file) => total + file.size, 0) > MAX_CHAT_ATTACHMENT_BYTES) {
|
|
908
|
+
throw new Error(fillText(texts.attachmentsTooLarge, { size: maxAttachmentSizeLabel }));
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
async function encodeAttachment(file) {
|
|
912
|
+
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
913
|
+
const chunks = [];
|
|
914
|
+
for (let offset = 0; offset < bytes.length; offset += 8192) {
|
|
915
|
+
chunks.push(String.fromCharCode(...bytes.subarray(offset, offset + 8192)));
|
|
916
|
+
}
|
|
917
|
+
return { name: file.name, mediaType: attachmentMediaType(file), data: btoa(chunks.join("")) };
|
|
918
|
+
}
|
|
919
|
+
function formatAttachmentSize(bytes) {
|
|
920
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
921
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
922
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
|
|
923
|
+
}
|
|
924
|
+
function attachmentDownloadUrl(url) {
|
|
925
|
+
if (/^(data|blob):/i.test(url)) return url;
|
|
926
|
+
const [resource, fragment] = url.split(/#(.*)/s);
|
|
927
|
+
return `${resource}${resource.includes("?") ? "&" : "?"}download=1${fragment === void 0 ? "" : `#${fragment}`}`;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// packages/quassel/src/ChatMessages.tsx
|
|
931
|
+
import { CheckIcon as CheckIcon4, ChevronDownIcon, ChevronRightIcon, ChevronUpIcon, LayersIcon, SparklesIcon as SparklesIcon2, WrenchIcon as WrenchIcon2 } from "lucide-react";
|
|
932
|
+
|
|
933
|
+
// packages/quassel/src/options.ts
|
|
934
|
+
var cssLength = (value) => typeof value === "number" ? `${value}px` : value;
|
|
935
|
+
var appearanceStyle = (appearance) => ({
|
|
936
|
+
"--qsl-font-size": cssLength(appearance?.fontSize),
|
|
937
|
+
"--qsl-chat-font-size": cssLength(appearance?.fontSize),
|
|
938
|
+
"--qsl-chat-line-height": appearance?.lineHeight,
|
|
939
|
+
"--qsl-chat-message-gap": cssLength(appearance?.messageGap),
|
|
940
|
+
"--qsl-chat-dense-message-gap": cssLength(appearance?.denseMessageGap),
|
|
941
|
+
"--qsl-tool-font-scale": appearance?.toolFontScale,
|
|
942
|
+
"--qsl-tool-group-gap": cssLength(appearance?.toolGroupGap),
|
|
943
|
+
"--qsl-tool-row-padding": cssLength(appearance?.toolRowPadding)
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
// packages/quassel/src/timestamps.ts
|
|
947
|
+
function messageDate(at) {
|
|
948
|
+
if (!at) return void 0;
|
|
949
|
+
const date = new Date(at);
|
|
950
|
+
return Number.isNaN(date.getTime()) ? void 0 : date;
|
|
951
|
+
}
|
|
952
|
+
function timestampLabel(date, options = {}, now = Date.now()) {
|
|
953
|
+
if ((!options.format || options.format === "time") && !options.locale && !options.timeZone) {
|
|
954
|
+
return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
|
|
955
|
+
}
|
|
956
|
+
if (options.format === "relative") {
|
|
957
|
+
const seconds = (date.getTime() - now) / 1e3;
|
|
958
|
+
const units = [
|
|
959
|
+
["year", 365 * 86400],
|
|
960
|
+
["month", 30 * 86400],
|
|
961
|
+
["day", 86400],
|
|
962
|
+
["hour", 3600],
|
|
963
|
+
["minute", 60]
|
|
964
|
+
];
|
|
965
|
+
const [unit, divisor] = units.find(([, value]) => Math.abs(seconds) >= value) ?? ["second", 1];
|
|
966
|
+
return new Intl.RelativeTimeFormat(options.locale, { numeric: "auto" }).format(Math.round(seconds / divisor), unit);
|
|
967
|
+
}
|
|
968
|
+
return new Intl.DateTimeFormat(options.locale, {
|
|
969
|
+
timeZone: options.timeZone,
|
|
970
|
+
hour: "2-digit",
|
|
971
|
+
minute: "2-digit",
|
|
972
|
+
hourCycle: "h23",
|
|
973
|
+
...options.format === "date-time" ? { year: "numeric", month: "2-digit", day: "2-digit" } : {}
|
|
974
|
+
}).format(date);
|
|
975
|
+
}
|
|
976
|
+
function timestampDay(date, options = {}) {
|
|
977
|
+
const key = new Intl.DateTimeFormat("en-CA", {
|
|
978
|
+
timeZone: options.timeZone,
|
|
979
|
+
year: "numeric",
|
|
980
|
+
month: "2-digit",
|
|
981
|
+
day: "2-digit"
|
|
982
|
+
}).format(date);
|
|
983
|
+
const label = new Intl.DateTimeFormat(options.locale, {
|
|
984
|
+
timeZone: options.timeZone,
|
|
985
|
+
dateStyle: "long"
|
|
986
|
+
}).format(date);
|
|
987
|
+
return { key, label };
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// packages/quassel/src/elapsed.ts
|
|
991
|
+
import { useSyncExternalStore } from "react";
|
|
992
|
+
var tickMs = 500;
|
|
993
|
+
function formatElapsed(ms, texts) {
|
|
994
|
+
const total = Math.max(0, Math.floor(ms / 1e3));
|
|
995
|
+
if (total < 60) return fillText(texts.toolElapsedSeconds, { seconds: total });
|
|
996
|
+
const hours = Math.floor(total / 3600);
|
|
997
|
+
const minutes = Math.floor(total / 60) % 60;
|
|
998
|
+
const seconds = String(total % 60).padStart(2, "0");
|
|
999
|
+
return hours > 0 ? `${hours}:${String(minutes).padStart(2, "0")}:${seconds}` : `${minutes}:${seconds}`;
|
|
1000
|
+
}
|
|
1001
|
+
function createTicker(intervalMs2 = tickMs) {
|
|
1002
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1003
|
+
let now = Date.now();
|
|
1004
|
+
let timer;
|
|
1005
|
+
return {
|
|
1006
|
+
subscribe(listener) {
|
|
1007
|
+
listeners.add(listener);
|
|
1008
|
+
if (timer === void 0) {
|
|
1009
|
+
now = Date.now();
|
|
1010
|
+
timer = setInterval(() => {
|
|
1011
|
+
now = Date.now();
|
|
1012
|
+
listeners.forEach((notify) => notify());
|
|
1013
|
+
}, intervalMs2);
|
|
1014
|
+
}
|
|
1015
|
+
return () => {
|
|
1016
|
+
listeners.delete(listener);
|
|
1017
|
+
if (listeners.size === 0 && timer !== void 0) {
|
|
1018
|
+
clearInterval(timer);
|
|
1019
|
+
timer = void 0;
|
|
1020
|
+
}
|
|
1021
|
+
};
|
|
1022
|
+
},
|
|
1023
|
+
now() {
|
|
1024
|
+
const current = Date.now();
|
|
1025
|
+
if (timer === void 0 && (current - now >= intervalMs2 || current < now)) now = current;
|
|
1026
|
+
return now;
|
|
1027
|
+
},
|
|
1028
|
+
active: () => timer !== void 0
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
var elapsedTicker = createTicker();
|
|
1032
|
+
var nothingOnServer = () => void 0;
|
|
1033
|
+
function useElapsed(start) {
|
|
1034
|
+
return useSyncExternalStore(elapsedTicker.subscribe, () => Math.floor((elapsedTicker.now() - start) / 1e3) * 1e3, nothingOnServer);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// packages/quassel/src/MessageActions.tsx
|
|
1038
|
+
import { CheckIcon as CheckIcon3, CopyIcon as CopyIcon2, PencilIcon, RotateCcwIcon } from "lucide-react";
|
|
1039
|
+
import { useState as useState3 } from "react";
|
|
1040
|
+
import { Fragment, jsx as jsx12, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1041
|
+
var actionClasses = cn(
|
|
1042
|
+
"qsl:absolute qsl:top-1 qsl:right-1 qsl:z-[1] qsl:flex qsl:flex-wrap qsl:rounded-sm qsl:bg-secondary qsl:opacity-0",
|
|
1043
|
+
"qsl:shadow-[0_2px_6px_color-mix(in_srgb,var(--qsl-foreground)_15%,transparent)]",
|
|
1044
|
+
"qsl:pointer-events-none qsl:group-hover:pointer-events-auto qsl:group-hover:opacity-100 qsl:focus-within:pointer-events-auto qsl:focus-within:opacity-100",
|
|
1045
|
+
"qsl:[@media(hover:none)]:pointer-events-auto qsl:[@media(hover:none)]:opacity-100"
|
|
1046
|
+
);
|
|
1047
|
+
function MessageActions({ message, options, texts }) {
|
|
1048
|
+
const action = useChatAction();
|
|
1049
|
+
const { Button: Button2 } = useQuasselComponents();
|
|
1050
|
+
const [lastAction, setLastAction] = useState3();
|
|
1051
|
+
const copy = typeof options?.copy === "function" ? options.copy(message) : options?.copy ?? message.role === "user";
|
|
1052
|
+
const actions = [
|
|
1053
|
+
...copy && message.text.trim() ? [{ id: "copy", label: texts.copyMessage, icon: /* @__PURE__ */ jsx12(CopyIcon2, {}), onClick: () => copyChatText(message.text, texts.clipboardUnavailable) }] : [],
|
|
1054
|
+
...options?.edit && message.role === "user" ? [{ id: "edit", label: texts.editMessage, icon: /* @__PURE__ */ jsx12(PencilIcon, {}), onClick: options.edit }] : [],
|
|
1055
|
+
...options?.retry && message.role === "assistant" ? [{ id: "retry", label: texts.retryMessage, icon: /* @__PURE__ */ jsx12(RotateCcwIcon, {}), onClick: options.retry }] : [],
|
|
1056
|
+
...options?.custom?.(message) ?? []
|
|
1057
|
+
];
|
|
1058
|
+
return /* @__PURE__ */ jsxs5(Fragment, { children: [
|
|
1059
|
+
actions.length > 0 && /* @__PURE__ */ jsx12("div", { className: actionClasses, children: actions.map((item) => {
|
|
1060
|
+
const copied = item.id === "copy" && lastAction === item.id && action.status === "done";
|
|
1061
|
+
const label = copied ? texts.copied : item.label;
|
|
1062
|
+
return /* @__PURE__ */ jsx12(
|
|
1063
|
+
Button2,
|
|
1064
|
+
{
|
|
1065
|
+
"aria-label": label,
|
|
1066
|
+
className: "qsl:text-inherit",
|
|
1067
|
+
disabled: item.disabled || action.status === "pending",
|
|
1068
|
+
size: item.icon ? "icon-sm" : "sm",
|
|
1069
|
+
title: label,
|
|
1070
|
+
variant: "ghost",
|
|
1071
|
+
onClick: () => {
|
|
1072
|
+
setLastAction(item.id);
|
|
1073
|
+
void action.invoke(() => item.onClick(message), texts.actionFailed);
|
|
1074
|
+
},
|
|
1075
|
+
children: copied ? /* @__PURE__ */ jsx12(CheckIcon3, {}) : item.icon ?? item.label
|
|
1076
|
+
},
|
|
1077
|
+
item.id
|
|
1078
|
+
);
|
|
1079
|
+
}) }),
|
|
1080
|
+
action.error && /* @__PURE__ */ jsx12("p", { className: "qsl:mt-2 qsl:text-sm qsl:text-destructive", role: "alert", children: action.error })
|
|
1081
|
+
] });
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// packages/quassel/src/ChatSendContext.ts
|
|
1085
|
+
import { createContext as createContext3 } from "react";
|
|
1086
|
+
var ChatSendContext = createContext3(void 0);
|
|
1087
|
+
function createChatSendScope(enabled) {
|
|
1088
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1089
|
+
return {
|
|
1090
|
+
notify: () => {
|
|
1091
|
+
if (enabled.current) listeners.forEach((listener) => listener());
|
|
1092
|
+
},
|
|
1093
|
+
subscribe: (listener) => {
|
|
1094
|
+
listeners.add(listener);
|
|
1095
|
+
return () => {
|
|
1096
|
+
listeners.delete(listener);
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// packages/quassel/src/ChatActionDock.ts
|
|
1103
|
+
import { createContext as createContext4 } from "react";
|
|
1104
|
+
var ChatActionDock = createContext4(void 0);
|
|
1105
|
+
|
|
1106
|
+
// packages/quassel/src/announce.ts
|
|
1107
|
+
var intervalMs = 400;
|
|
1108
|
+
var maxQueued = 5;
|
|
1109
|
+
var region;
|
|
1110
|
+
var queued = [];
|
|
1111
|
+
var running = false;
|
|
1112
|
+
function liveRegion() {
|
|
1113
|
+
if (region?.isConnected) {
|
|
1114
|
+
return region;
|
|
1115
|
+
}
|
|
1116
|
+
const element = document.createElement("div");
|
|
1117
|
+
element.setAttribute("role", "status");
|
|
1118
|
+
element.setAttribute("aria-live", "polite");
|
|
1119
|
+
element.setAttribute("aria-atomic", "true");
|
|
1120
|
+
Object.assign(element.style, {
|
|
1121
|
+
position: "absolute",
|
|
1122
|
+
width: "1px",
|
|
1123
|
+
height: "1px",
|
|
1124
|
+
margin: "-1px",
|
|
1125
|
+
padding: "0",
|
|
1126
|
+
border: "0",
|
|
1127
|
+
overflow: "hidden",
|
|
1128
|
+
clipPath: "inset(50%)",
|
|
1129
|
+
whiteSpace: "nowrap"
|
|
1130
|
+
});
|
|
1131
|
+
document.body.append(element);
|
|
1132
|
+
region = element;
|
|
1133
|
+
return element;
|
|
1134
|
+
}
|
|
1135
|
+
function next() {
|
|
1136
|
+
if (queued.length === 0) {
|
|
1137
|
+
running = false;
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
if (document.visibilityState === "hidden") {
|
|
1141
|
+
document.addEventListener("visibilitychange", next, { once: true });
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
const [text, ...rest] = queued;
|
|
1145
|
+
queued = rest;
|
|
1146
|
+
const target = liveRegion();
|
|
1147
|
+
target.textContent = target.textContent === text ? `${text}\xA0` : text;
|
|
1148
|
+
window.setTimeout(next, intervalMs);
|
|
1149
|
+
}
|
|
1150
|
+
function announce(text) {
|
|
1151
|
+
const content = text.trim();
|
|
1152
|
+
if (!content || typeof document === "undefined") {
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
queued = [...queued, content].slice(-maxQueued);
|
|
1156
|
+
if (running) {
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
running = true;
|
|
1160
|
+
liveRegion();
|
|
1161
|
+
window.setTimeout(next, intervalMs);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// packages/quassel/src/ChatMessages.tsx
|
|
1165
|
+
import { Fragment as Fragment2, jsx as jsx13, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1166
|
+
var stepClasses = "qsl:mt-[var(--qsl-chat-message-gap,10px)]";
|
|
1167
|
+
var denseStepClasses = "qsl:mt-[var(--qsl-chat-dense-message-gap,3px)]";
|
|
1168
|
+
var traceClasses = "qsl:flex qsl:w-full qsl:gap-2 qsl:py-[var(--qsl-tool-row-padding,1px)] qsl:pl-6 qsl:text-left qsl:text-trace qsl:leading-[1.375] qsl:text-muted-foreground";
|
|
1169
|
+
var traceButtonClasses = "qsl:cursor-pointer qsl:rounded-md qsl:pr-2 qsl:transition-colors qsl:hover:bg-secondary qsl:hover:text-foreground qsl:focus-visible:bg-secondary qsl:focus-visible:text-foreground qsl:focus-visible:outline-none";
|
|
1170
|
+
var traceIconClasses = "qsl:mt-0.5 qsl:flex-none qsl:opacity-60";
|
|
1171
|
+
var traceErrorIconClasses = "qsl:mt-0.5 qsl:flex-none qsl:text-destructive qsl:opacity-100";
|
|
1172
|
+
var traceLineClasses = "qsl:min-w-0 qsl:self-baseline qsl:[overflow-wrap:anywhere]";
|
|
1173
|
+
var traceRunningClasses = "qsl:ml-1.5 qsl:opacity-60";
|
|
1174
|
+
var rowIconClasses = cn(traceIconClasses, "qsl:mt-0");
|
|
1175
|
+
var rowErrorIconClasses = cn(traceErrorIconClasses, "qsl:mt-0");
|
|
1176
|
+
var rowLineClasses = cn(traceLineClasses, "qsl:self-auto");
|
|
1177
|
+
var chipClasses = cn(
|
|
1178
|
+
"qsl:inline-flex qsl:max-w-[220px] qsl:items-center qsl:gap-[5px] qsl:rounded-full qsl:border qsl:border-border-soft qsl:px-2.5 qsl:py-0.5",
|
|
1179
|
+
"qsl:text-meta qsl:leading-[1.6] qsl:text-muted-foreground qsl:transition-colors",
|
|
1180
|
+
"qsl:data-[state=running]:border-[color-mix(in_srgb,var(--qsl-primary)_40%,var(--qsl-border-soft))]",
|
|
1181
|
+
"qsl:data-[state=thinking]:border-[color-mix(in_srgb,var(--qsl-primary)_40%,var(--qsl-border-soft))]",
|
|
1182
|
+
"qsl:data-[state=error]:border-[color-mix(in_srgb,var(--qsl-destructive)_45%,var(--qsl-border-soft))]",
|
|
1183
|
+
"qsl:data-[state=done]:opacity-85"
|
|
1184
|
+
);
|
|
1185
|
+
var chipInteractiveClasses = "qsl:cursor-pointer qsl:hover:border-border qsl:hover:bg-secondary qsl:hover:text-foreground qsl:focus-visible:border-border qsl:focus-visible:bg-secondary qsl:focus-visible:text-foreground qsl:focus-visible:outline-none qsl:data-[state=done]:hover:opacity-100 qsl:data-[state=done]:focus-visible:opacity-100";
|
|
1186
|
+
var collapseAtBottomFrom = 11;
|
|
1187
|
+
var maxAnnouncementsAtOnce = 2;
|
|
1188
|
+
var bubbleBodyClasses = "qsl:relative qsl:max-w-[86%] qsl:px-3 qsl:py-2 qsl:leading-[var(--qsl-chat-line-height,1.5)] qsl:[overflow-wrap:anywhere] qsl:@max-[620px]/chat:max-w-full";
|
|
1189
|
+
function isStep(message) {
|
|
1190
|
+
return !!message && (message.role === "thinking" || message.role === "tool");
|
|
1191
|
+
}
|
|
1192
|
+
function isOpenAction(message) {
|
|
1193
|
+
return message.role === "action" && message.action !== void 0 && message.action.status === void 0;
|
|
1194
|
+
}
|
|
1195
|
+
function defaultArgumentsText(tool) {
|
|
1196
|
+
return prettyJson(tool.arguments);
|
|
1197
|
+
}
|
|
1198
|
+
function ChatMessages({
|
|
1199
|
+
messages,
|
|
1200
|
+
owner,
|
|
1201
|
+
detailMode = "current",
|
|
1202
|
+
transcriptMode = "all",
|
|
1203
|
+
running: running2 = false,
|
|
1204
|
+
working,
|
|
1205
|
+
stepsExpandable = true,
|
|
1206
|
+
groupsExpandable = true,
|
|
1207
|
+
texts,
|
|
1208
|
+
toolArgumentsText = defaultArgumentsText,
|
|
1209
|
+
renderTool,
|
|
1210
|
+
renderAction,
|
|
1211
|
+
onDismissAction,
|
|
1212
|
+
onLinkClick,
|
|
1213
|
+
emptyState,
|
|
1214
|
+
className,
|
|
1215
|
+
scrollerClassName,
|
|
1216
|
+
showTimestamps = false,
|
|
1217
|
+
announce: announce2,
|
|
1218
|
+
scrollerRef,
|
|
1219
|
+
maxWidth,
|
|
1220
|
+
horizontalPadding,
|
|
1221
|
+
jumpToEndThreshold = 120,
|
|
1222
|
+
appearance,
|
|
1223
|
+
timestampOptions,
|
|
1224
|
+
codeBlockOptions,
|
|
1225
|
+
bubbleOptions,
|
|
1226
|
+
messageActions,
|
|
1227
|
+
toolElapsedThreshold = 3e3
|
|
1228
|
+
}) {
|
|
1229
|
+
const selected = useMemo3(() => {
|
|
1230
|
+
if (transcriptMode === "all") return messages;
|
|
1231
|
+
const answers = /* @__PURE__ */ new Set();
|
|
1232
|
+
let hasAnswer = false;
|
|
1233
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
1234
|
+
const message = messages[index];
|
|
1235
|
+
if (message.role === "user") {
|
|
1236
|
+
hasAnswer = false;
|
|
1237
|
+
} else if (message.role === "assistant" && !hasAnswer && (message.text.trim() !== "" || message.attachments?.length)) {
|
|
1238
|
+
answers.add(message);
|
|
1239
|
+
hasAnswer = true;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
return messages.filter((message) => message.role !== "assistant" || answers.has(message));
|
|
1243
|
+
}, [messages, transcriptMode]);
|
|
1244
|
+
const allTexts = { ...defaultTexts, ...texts };
|
|
1245
|
+
const { Button: Button2 } = useQuasselComponents();
|
|
1246
|
+
const scrollArea = useRef3(null);
|
|
1247
|
+
const threadRef = useRef3(null);
|
|
1248
|
+
const scrollbarDragging = useRef3(false);
|
|
1249
|
+
const touchY = useRef3(void 0);
|
|
1250
|
+
const [showJumpToEnd, setShowJumpToEnd] = useState4(false);
|
|
1251
|
+
const [scroll] = useState4(() => createChatScroll());
|
|
1252
|
+
const sendScope = useContext3(ChatSendContext);
|
|
1253
|
+
const dock = useContext3(ChatActionDock);
|
|
1254
|
+
const [now, setNow] = useState4(() => Date.now());
|
|
1255
|
+
useEffect3(() => {
|
|
1256
|
+
if (!showTimestamps || timestampOptions?.format !== "relative") return;
|
|
1257
|
+
setNow(Date.now());
|
|
1258
|
+
const timer = window.setInterval(() => setNow(Date.now()), 15e3);
|
|
1259
|
+
return () => window.clearInterval(timer);
|
|
1260
|
+
}, [showTimestamps, timestampOptions?.format]);
|
|
1261
|
+
const announced = useRef3(new Set(messages.map((message) => message.key)));
|
|
1262
|
+
useEffect3(() => {
|
|
1263
|
+
const due = messages.filter((message, index) => !announced.current.has(message.key) && (message.role === "action" ? isOpenAction(message) : message.role === "assistant" && message.text.trim() !== "" && (message.closed === true || !running2 && index === messages.length - 1)));
|
|
1264
|
+
if (due.length === 0) {
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
announced.current = /* @__PURE__ */ new Set([...announced.current, ...due.map((message) => message.key)]);
|
|
1268
|
+
if (announce2 === false || due.length > maxAnnouncementsAtOnce) {
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
due.filter((message) => selected.includes(message)).map((message) => announce2 ? announce2({ kind: message.role === "action" ? "action" : "reply", message }) : markdownPlainText(message.text)).forEach((text) => text && announce(text));
|
|
1272
|
+
}, [messages, selected, running2, announce2]);
|
|
1273
|
+
const updateJumpVisibility = useCallback2((viewport) => {
|
|
1274
|
+
if (viewport.clientHeight > 0) setShowJumpToEnd(viewport.scrollHeight - viewport.clientHeight - viewport.scrollTop > jumpToEndThreshold);
|
|
1275
|
+
}, [jumpToEndThreshold]);
|
|
1276
|
+
useLayoutEffect(() => sendScope?.subscribe(() => {
|
|
1277
|
+
const viewport = scrollArea.current;
|
|
1278
|
+
if (!viewport) return;
|
|
1279
|
+
scroll.jump(viewport);
|
|
1280
|
+
updateJumpVisibility(viewport);
|
|
1281
|
+
}), [sendScope, scroll, updateJumpVisibility]);
|
|
1282
|
+
const pauseUp = (viewport, target) => {
|
|
1283
|
+
let element = target instanceof Element ? target : null;
|
|
1284
|
+
while (element && element !== viewport) {
|
|
1285
|
+
if (element.scrollTop > 0 && /auto|scroll/.test(getComputedStyle(element).overflowY)) return;
|
|
1286
|
+
element = element.parentElement;
|
|
1287
|
+
}
|
|
1288
|
+
scroll.pause(viewport);
|
|
1289
|
+
};
|
|
1290
|
+
const checkEnd = useCallback2(() => {
|
|
1291
|
+
const area = scrollArea.current;
|
|
1292
|
+
if (area) {
|
|
1293
|
+
scroll.scroll(area, scrollbarDragging.current);
|
|
1294
|
+
updateJumpVisibility(area);
|
|
1295
|
+
}
|
|
1296
|
+
}, [scroll, updateJumpVisibility]);
|
|
1297
|
+
useEffect3(() => {
|
|
1298
|
+
const finishDrag = () => {
|
|
1299
|
+
if (scrollbarDragging.current && scrollArea.current) scroll.scroll(scrollArea.current, true);
|
|
1300
|
+
scrollbarDragging.current = false;
|
|
1301
|
+
};
|
|
1302
|
+
window.addEventListener("pointerup", finishDrag);
|
|
1303
|
+
window.addEventListener("pointercancel", finishDrag);
|
|
1304
|
+
return () => {
|
|
1305
|
+
window.removeEventListener("pointerup", finishDrag);
|
|
1306
|
+
window.removeEventListener("pointercancel", finishDrag);
|
|
1307
|
+
};
|
|
1308
|
+
}, [scroll]);
|
|
1309
|
+
useLayoutEffect(() => {
|
|
1310
|
+
const area = scrollArea.current;
|
|
1311
|
+
if (!area) return;
|
|
1312
|
+
const align = () => {
|
|
1313
|
+
scroll.layout(area, scrollbarDragging.current);
|
|
1314
|
+
updateJumpVisibility(area);
|
|
1315
|
+
};
|
|
1316
|
+
align();
|
|
1317
|
+
const observer = new ResizeObserver(align);
|
|
1318
|
+
observer.observe(area);
|
|
1319
|
+
if (threadRef.current) observer.observe(threadRef.current, { box: "border-box" });
|
|
1320
|
+
return () => observer.disconnect();
|
|
1321
|
+
});
|
|
1322
|
+
const visible = useMemo3(
|
|
1323
|
+
() => {
|
|
1324
|
+
const lastNonUser = selected.reduce((last, message, index) => message.role === "user" ? last : index, -1);
|
|
1325
|
+
return selected.filter((message, index) => {
|
|
1326
|
+
if (dock !== void 0 && isOpenAction(message)) {
|
|
1327
|
+
return false;
|
|
1328
|
+
}
|
|
1329
|
+
if (detailMode === "current" && isStep(message)) {
|
|
1330
|
+
return running2 && index === lastNonUser && (message.role === "thinking" ? !message.closed : stepState(message) === "running");
|
|
1331
|
+
}
|
|
1332
|
+
if ((message.role === "thinking" || message.role === "assistant") && message.text.trim() === "" && !message.attachments?.length) {
|
|
1333
|
+
return false;
|
|
1334
|
+
}
|
|
1335
|
+
if (message.role === "thinking" || message.role === "tool") {
|
|
1336
|
+
return detailMode !== "off";
|
|
1337
|
+
}
|
|
1338
|
+
return true;
|
|
1339
|
+
});
|
|
1340
|
+
},
|
|
1341
|
+
[selected, detailMode, running2, dock]
|
|
1342
|
+
);
|
|
1343
|
+
const openActions = dock ? messages.filter(isOpenAction) : [];
|
|
1344
|
+
const withTime = (content, key, at, dense = false) => showTimestamps ? /* @__PURE__ */ jsxs6(
|
|
1345
|
+
"div",
|
|
1346
|
+
{
|
|
1347
|
+
className: cn("qsl:flex", timestampOptions?.position === "above" ? "qsl:flex-col qsl:items-stretch qsl:gap-1" : "qsl:items-baseline qsl:gap-2", dense ? denseStepClasses : stepClasses),
|
|
1348
|
+
"data-chat": "timestamped",
|
|
1349
|
+
"data-timestamp-position": timestampOptions?.position ?? "aside",
|
|
1350
|
+
children: [
|
|
1351
|
+
/* @__PURE__ */ jsx13("time", { dateTime: messageDate(at)?.toISOString(), className: cn(
|
|
1352
|
+
"qsl:flex-none qsl:text-timestamp qsl:leading-[1.375] qsl:tabular-nums qsl:text-muted-foreground qsl:opacity-85",
|
|
1353
|
+
timestampOptions?.position === "above" ? "qsl:self-start" : timestampOptions?.format && timestampOptions.format !== "time" ? "qsl:w-[clamp(60px,20%,120px)]" : "qsl:min-w-[5ch] qsl:whitespace-nowrap"
|
|
1354
|
+
), children: messageDate(at) ? timestampLabel(messageDate(at), timestampOptions, now) : "" }),
|
|
1355
|
+
/* @__PURE__ */ jsx13("div", { className: "qsl:min-w-0 qsl:flex-1 qsl:[&>*:first-child]:mt-0", children: content })
|
|
1356
|
+
]
|
|
1357
|
+
},
|
|
1358
|
+
`time-${key}`
|
|
1359
|
+
) : content;
|
|
1360
|
+
const chipMode = detailMode === "current" || detailMode === "chips" || detailMode === "icons";
|
|
1361
|
+
const elapsedAfter = running2 && toolElapsedThreshold !== false ? toolElapsedThreshold : void 0;
|
|
1362
|
+
const groupMode = detailMode === "grouped";
|
|
1363
|
+
const blocks = [];
|
|
1364
|
+
let lastDay;
|
|
1365
|
+
let group = [];
|
|
1366
|
+
const closeGroup = () => {
|
|
1367
|
+
if (group.length > 0) {
|
|
1368
|
+
const runningStep = running2 ? [...group].reverse().find((message) => ["running", "thinking"].includes(stepState(message))) : void 0;
|
|
1369
|
+
blocks.push(withTime(
|
|
1370
|
+
groupMode ? /* @__PURE__ */ jsx13(
|
|
1371
|
+
StepGroup,
|
|
1372
|
+
{
|
|
1373
|
+
elapsedAfter,
|
|
1374
|
+
expandable: stepsExpandable,
|
|
1375
|
+
groupExpandable: groupsExpandable,
|
|
1376
|
+
runningStep,
|
|
1377
|
+
messages: group,
|
|
1378
|
+
texts: allTexts,
|
|
1379
|
+
toolArgumentsText
|
|
1380
|
+
},
|
|
1381
|
+
group[0].key
|
|
1382
|
+
) : /* @__PURE__ */ jsx13(
|
|
1383
|
+
StepRow,
|
|
1384
|
+
{
|
|
1385
|
+
elapsedAfter,
|
|
1386
|
+
expandable: stepsExpandable,
|
|
1387
|
+
messages: group,
|
|
1388
|
+
withText: detailMode !== "icons",
|
|
1389
|
+
texts: allTexts,
|
|
1390
|
+
toolArgumentsText,
|
|
1391
|
+
toolLabel: detailMode === "current" && !stepsExpandable ? allTexts.currentTool : void 0
|
|
1392
|
+
},
|
|
1393
|
+
group[0].key
|
|
1394
|
+
),
|
|
1395
|
+
group[0].key,
|
|
1396
|
+
group[0].at
|
|
1397
|
+
));
|
|
1398
|
+
group = [];
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1401
|
+
visible.forEach((message, index) => {
|
|
1402
|
+
const date = timestampOptions?.showDaySeparators ? messageDate(message.at) : void 0;
|
|
1403
|
+
if (date) {
|
|
1404
|
+
const day = timestampDay(date, timestampOptions);
|
|
1405
|
+
if (day.key !== lastDay) {
|
|
1406
|
+
closeGroup();
|
|
1407
|
+
blocks.push(/* @__PURE__ */ jsx13("div", { className: cn(stepClasses, "qsl:text-center qsl:text-xs qsl:text-muted-foreground"), "data-chat": "day-separator", "data-day": day.key, children: day.label }, `day-${message.key}`));
|
|
1408
|
+
lastDay = day.key;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
const customRendering = message.role === "tool" && message.tool && detailMode !== "current" ? renderTool?.(message.tool) : void 0;
|
|
1412
|
+
if (customRendering !== void 0) {
|
|
1413
|
+
closeGroup();
|
|
1414
|
+
blocks.push(withTime(
|
|
1415
|
+
/* @__PURE__ */ jsx13("div", { className: stepClasses, children: customRendering }, message.key),
|
|
1416
|
+
message.key,
|
|
1417
|
+
message.at
|
|
1418
|
+
));
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
if ((chipMode || groupMode) && isStep(message)) {
|
|
1422
|
+
group.push(message);
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
closeGroup();
|
|
1426
|
+
const dense = isStep(message) && isStep(visible[index - 1]);
|
|
1427
|
+
blocks.push(withTime(
|
|
1428
|
+
/* @__PURE__ */ jsx13(
|
|
1429
|
+
Bubble,
|
|
1430
|
+
{
|
|
1431
|
+
detailMode,
|
|
1432
|
+
dense,
|
|
1433
|
+
elapsedAfter,
|
|
1434
|
+
expandable: stepsExpandable,
|
|
1435
|
+
message,
|
|
1436
|
+
plain: owner != null && message.sender === owner,
|
|
1437
|
+
texts: allTexts,
|
|
1438
|
+
toolArgumentsText,
|
|
1439
|
+
renderAction,
|
|
1440
|
+
onDismissAction,
|
|
1441
|
+
bubbleOptions,
|
|
1442
|
+
messageActions
|
|
1443
|
+
},
|
|
1444
|
+
message.key
|
|
1445
|
+
),
|
|
1446
|
+
message.key,
|
|
1447
|
+
message.at,
|
|
1448
|
+
dense
|
|
1449
|
+
));
|
|
1450
|
+
});
|
|
1451
|
+
closeGroup();
|
|
1452
|
+
const transcript = /* @__PURE__ */ jsxs6("div", { className: cn("qsl:@container/chat qsl:relative qsl:flex qsl:min-h-0 qsl:flex-1 qsl:flex-col", className), "data-quassel": "", style: {
|
|
1453
|
+
...appearanceStyle(appearance),
|
|
1454
|
+
"--qsl-chat-content-max-width": typeof maxWidth === "number" ? `${maxWidth}px` : maxWidth,
|
|
1455
|
+
"--qsl-chat-horizontal-padding": typeof horizontalPadding === "number" ? `${horizontalPadding}px` : horizontalPadding,
|
|
1456
|
+
"--qsl-timestamp-font-size": timestampOptions?.fontScale === void 0 ? void 0 : `calc(var(--qsl-base) * ${timestampOptions.fontScale})`
|
|
1457
|
+
}, children: [
|
|
1458
|
+
/* @__PURE__ */ jsx13(
|
|
1459
|
+
"div",
|
|
1460
|
+
{
|
|
1461
|
+
"aria-label": allTexts.transcript,
|
|
1462
|
+
className: cn(
|
|
1463
|
+
"qsl:min-h-0 qsl:flex-1 qsl:cursor-default qsl:overflow-y-auto qsl:overscroll-contain qsl:text-chat qsl:text-foreground",
|
|
1464
|
+
"qsl:[--qsl-fade-edge:calc(100%_-_var(--qsl-composer-height,0px))]",
|
|
1465
|
+
"qsl:[-webkit-mask-image:linear-gradient(to_bottom,#000_calc(var(--qsl-fade-edge)_-_40px),transparent_calc(var(--qsl-fade-edge)_+_16px))]",
|
|
1466
|
+
"qsl:[mask-image:linear-gradient(to_bottom,#000_calc(var(--qsl-fade-edge)_-_40px),transparent_calc(var(--qsl-fade-edge)_+_16px))]",
|
|
1467
|
+
"qsl:in-data-[tone=material]:p-0",
|
|
1468
|
+
scrollerClassName
|
|
1469
|
+
),
|
|
1470
|
+
"data-quassel-transcript": "",
|
|
1471
|
+
onScroll: checkEnd,
|
|
1472
|
+
onWheel: (event) => {
|
|
1473
|
+
if (!event.ctrlKey && event.deltaY < 0) pauseUp(event.currentTarget, event.target);
|
|
1474
|
+
},
|
|
1475
|
+
onKeyDown: (event) => {
|
|
1476
|
+
const editable = event.target instanceof Element && event.target.closest("input, textarea, select, [contenteditable=true]");
|
|
1477
|
+
if (!editable && (["ArrowUp", "PageUp", "Home"].includes(event.key) || event.target === event.currentTarget && event.key === " " && event.shiftKey)) pauseUp(event.currentTarget, event.target);
|
|
1478
|
+
},
|
|
1479
|
+
onPointerDown: (event) => {
|
|
1480
|
+
const viewport = event.currentTarget;
|
|
1481
|
+
scrollbarDragging.current = event.clientX >= viewport.getBoundingClientRect().right - Math.max(16, viewport.offsetWidth - viewport.clientWidth);
|
|
1482
|
+
},
|
|
1483
|
+
onTouchStart: (event) => {
|
|
1484
|
+
touchY.current = event.touches[0]?.clientY;
|
|
1485
|
+
},
|
|
1486
|
+
onTouchMove: (event) => {
|
|
1487
|
+
const next2 = event.touches[0]?.clientY;
|
|
1488
|
+
if (next2 !== void 0 && touchY.current !== void 0 && next2 > touchY.current) pauseUp(event.currentTarget, event.target);
|
|
1489
|
+
touchY.current = next2;
|
|
1490
|
+
},
|
|
1491
|
+
role: "region",
|
|
1492
|
+
tabIndex: 0,
|
|
1493
|
+
ref: (element) => {
|
|
1494
|
+
scrollArea.current = element;
|
|
1495
|
+
scrollerRef?.(element);
|
|
1496
|
+
},
|
|
1497
|
+
children: visible.length === 0 && !running2 ? emptyState ?? null : /* @__PURE__ */ jsxs6(
|
|
1498
|
+
"div",
|
|
1499
|
+
{
|
|
1500
|
+
"aria-busy": running2 || void 0,
|
|
1501
|
+
className: cn(
|
|
1502
|
+
"qsl:mx-auto qsl:w-[calc(100%_-_2_*_var(--qsl-chat-horizontal-padding,24px))] qsl:max-w-[var(--qsl-chat-content-max-width,none)] qsl:pt-2",
|
|
1503
|
+
"qsl:pb-[calc(var(--qsl-composer-height,0px)_+_max(3.25em,40px))]",
|
|
1504
|
+
"qsl:in-data-[tone=material]:[&>*:first-child]:mt-0"
|
|
1505
|
+
),
|
|
1506
|
+
ref: threadRef,
|
|
1507
|
+
children: [
|
|
1508
|
+
blocks,
|
|
1509
|
+
running2 && (working ?? /* @__PURE__ */ jsx13(WorkingScenes, { label: allTexts.working }))
|
|
1510
|
+
]
|
|
1511
|
+
}
|
|
1512
|
+
)
|
|
1513
|
+
}
|
|
1514
|
+
),
|
|
1515
|
+
showJumpToEnd && /* @__PURE__ */ jsx13("div", { className: "qsl:pointer-events-none qsl:absolute qsl:inset-x-0 qsl:bottom-[calc(var(--qsl-composer-height,0px)_+_12px)] qsl:z-10 qsl:flex qsl:justify-center", children: /* @__PURE__ */ jsx13(Button2, { "aria-label": allTexts.jumpToEnd, className: "qsl:pointer-events-auto qsl:rounded-full qsl:border qsl:border-primary-foreground/20 qsl:shadow-md", size: "icon", title: allTexts.jumpToEnd, variant: "default", onClick: () => {
|
|
1516
|
+
if (scrollArea.current) {
|
|
1517
|
+
scroll.jump(scrollArea.current);
|
|
1518
|
+
updateJumpVisibility(scrollArea.current);
|
|
1519
|
+
}
|
|
1520
|
+
}, children: /* @__PURE__ */ jsx13(ChevronDownIcon, {}) }) }),
|
|
1521
|
+
dock && openActions.length > 0 && createPortal(
|
|
1522
|
+
/* @__PURE__ */ jsx13("div", { className: "qsl:flex qsl:flex-col qsl:gap-[var(--qsl-chat-dense-message-gap,3px)] qsl:text-chat qsl:text-foreground", style: appearanceStyle(appearance), children: openActions.map((message) => /* @__PURE__ */ jsx13("div", { children: /* @__PURE__ */ jsx13(ActionContent, { action: message.action, onDismissAction, renderAction, text: message.text, texts: allTexts }) }, message.key)) }),
|
|
1523
|
+
dock
|
|
1524
|
+
)
|
|
1525
|
+
] });
|
|
1526
|
+
return /* @__PURE__ */ jsx13(MarkdownLinks, { onLinkClick, children: /* @__PURE__ */ jsx13(MarkdownCodeBlocks, { options: codeBlockOptions, texts: allTexts, children: transcript }) });
|
|
1527
|
+
}
|
|
1528
|
+
function StepRow({
|
|
1529
|
+
messages,
|
|
1530
|
+
withText,
|
|
1531
|
+
expandable,
|
|
1532
|
+
elapsedAfter,
|
|
1533
|
+
texts,
|
|
1534
|
+
toolArgumentsText,
|
|
1535
|
+
toolLabel
|
|
1536
|
+
}) {
|
|
1537
|
+
const [detail, setDetail] = useState4();
|
|
1538
|
+
const open = expandable && detail && messages.find((message) => message.key === detail.key);
|
|
1539
|
+
return /* @__PURE__ */ jsxs6("div", { className: cn(stepClasses, "qsl:flex qsl:flex-wrap qsl:items-center qsl:gap-x-1.5 qsl:gap-y-1 qsl:pl-6"), "data-step": "row", children: [
|
|
1540
|
+
messages.map((message) => {
|
|
1541
|
+
const thinking = message.role === "thinking";
|
|
1542
|
+
const tool = message.tool;
|
|
1543
|
+
const state = stepState(message);
|
|
1544
|
+
const running2 = state === "running" || state === "thinking";
|
|
1545
|
+
const label = thinking ? texts.thinkingChip : toolLabel ?? (tool?.name || message.text || texts.toolChip);
|
|
1546
|
+
const content = /* @__PURE__ */ jsxs6(Fragment2, { children: [
|
|
1547
|
+
thinking ? /* @__PURE__ */ jsx13(SparklesIcon2, { size: 11 }) : /* @__PURE__ */ jsx13(WrenchIcon2, { className: tool?.isError ? "qsl:text-destructive" : void 0, size: 11 }),
|
|
1548
|
+
withText && /* @__PURE__ */ jsx13("span", { className: "qsl:min-w-0 qsl:self-baseline qsl:overflow-hidden qsl:text-ellipsis qsl:whitespace-nowrap", children: label }),
|
|
1549
|
+
withText && /* @__PURE__ */ jsx13(ToolElapsed, { after: elapsedAfter, className: "qsl:flex-none qsl:self-baseline", message, texts }),
|
|
1550
|
+
withText && /* @__PURE__ */ jsx13("span", { className: "qsl:inline-flex qsl:w-2.5 qsl:flex-none qsl:items-center qsl:justify-center qsl:text-success", children: state === "done" && /* @__PURE__ */ jsx13(CheckIcon4, { size: 10 }) })
|
|
1551
|
+
] });
|
|
1552
|
+
const classes = cn(chipClasses, !withText && "qsl:p-1", running2 && "qsl:animate-fade-pulse qsl:motion-reduce:animate-none");
|
|
1553
|
+
if (!expandable) {
|
|
1554
|
+
return /* @__PURE__ */ jsx13("span", { className: classes, "data-kind": thinking ? "thinking" : "tool", "data-state": state, "data-step": "chip", title: label, children: content }, message.key);
|
|
1555
|
+
}
|
|
1556
|
+
return /* @__PURE__ */ jsx13(
|
|
1557
|
+
"button",
|
|
1558
|
+
{
|
|
1559
|
+
"aria-haspopup": "dialog",
|
|
1560
|
+
className: cn(classes, chipInteractiveClasses),
|
|
1561
|
+
"data-kind": thinking ? "thinking" : "tool",
|
|
1562
|
+
"data-state": state,
|
|
1563
|
+
"data-step": "chip",
|
|
1564
|
+
onClick: (event) => setDetail({ key: message.key, position: stepPopoverOrigin(event) }),
|
|
1565
|
+
title: label,
|
|
1566
|
+
type: "button",
|
|
1567
|
+
children: content
|
|
1568
|
+
},
|
|
1569
|
+
message.key
|
|
1570
|
+
);
|
|
1571
|
+
}),
|
|
1572
|
+
open && detail && /* @__PURE__ */ jsx13(
|
|
1573
|
+
StepPopover,
|
|
1574
|
+
{
|
|
1575
|
+
message: open,
|
|
1576
|
+
position: detail.position,
|
|
1577
|
+
texts,
|
|
1578
|
+
toolArgumentsText,
|
|
1579
|
+
onClose: () => setDetail(void 0)
|
|
1580
|
+
}
|
|
1581
|
+
)
|
|
1582
|
+
] });
|
|
1583
|
+
}
|
|
1584
|
+
function StepGroup({
|
|
1585
|
+
messages,
|
|
1586
|
+
runningStep,
|
|
1587
|
+
elapsedAfter,
|
|
1588
|
+
expandable,
|
|
1589
|
+
groupExpandable,
|
|
1590
|
+
texts,
|
|
1591
|
+
toolArgumentsText
|
|
1592
|
+
}) {
|
|
1593
|
+
const [open, setOpen] = useState4(false);
|
|
1594
|
+
const header = useRef3(null);
|
|
1595
|
+
const collapsedFromBottom = useRef3(false);
|
|
1596
|
+
useLayoutEffect(() => {
|
|
1597
|
+
if (!open && collapsedFromBottom.current) {
|
|
1598
|
+
collapsedFromBottom.current = false;
|
|
1599
|
+
header.current?.scrollIntoView({ block: "nearest" });
|
|
1600
|
+
}
|
|
1601
|
+
}, [open]);
|
|
1602
|
+
const active = runningStep !== void 0;
|
|
1603
|
+
const errorCount = messages.filter((message) => message.tool?.isError).length;
|
|
1604
|
+
const errorLabel = errorCount === 1 ? texts.stepGroupErrorOne ?? defaultTexts.stepGroupErrorOne : (texts.stepGroupErrorMany ?? defaultTexts.stepGroupErrorMany).replace("{count}", String(errorCount));
|
|
1605
|
+
const count = messages.length === 1 ? texts.stepGroupOne : texts.stepGroupMany.replace("{count}", String(messages.length));
|
|
1606
|
+
const runningLabel = runningStep && (runningStep.role === "thinking" ? texts.thinkingChip : runningStep.tool?.name || runningStep.text || texts.toolChip);
|
|
1607
|
+
const summary = /* @__PURE__ */ jsxs6(Fragment2, { children: [
|
|
1608
|
+
/* @__PURE__ */ jsx13(LayersIcon, { className: errorCount > 0 ? rowErrorIconClasses : rowIconClasses, size: 12 }),
|
|
1609
|
+
/* @__PURE__ */ jsxs6("span", { className: cn(rowLineClasses, "qsl:overflow-hidden qsl:text-ellipsis qsl:whitespace-nowrap"), children: [
|
|
1610
|
+
count,
|
|
1611
|
+
errorCount > 0 && /* @__PURE__ */ jsx13("span", { className: "qsl:text-destructive", "data-step": "errors", children: `, ${errorLabel}` }),
|
|
1612
|
+
runningLabel && /* @__PURE__ */ jsxs6("span", { className: traceRunningClasses, children: [
|
|
1613
|
+
runningLabel,
|
|
1614
|
+
" ",
|
|
1615
|
+
texts.toolRunning
|
|
1616
|
+
] }),
|
|
1617
|
+
runningStep && /* @__PURE__ */ jsx13(ToolElapsed, { after: elapsedAfter, message: runningStep, texts })
|
|
1618
|
+
] })
|
|
1619
|
+
] });
|
|
1620
|
+
if (!groupExpandable) {
|
|
1621
|
+
return /* @__PURE__ */ jsx13("div", { className: stepClasses, "data-step": "group", children: /* @__PURE__ */ jsx13("div", { className: cn(traceClasses, "qsl:items-center qsl:pr-2 qsl:pl-1", active && "qsl:animate-fade-pulse qsl:motion-reduce:animate-none"), children: summary }) });
|
|
1622
|
+
}
|
|
1623
|
+
return /* @__PURE__ */ jsxs6("div", { className: stepClasses, "data-step": "group", children: [
|
|
1624
|
+
/* @__PURE__ */ jsxs6(
|
|
1625
|
+
"button",
|
|
1626
|
+
{
|
|
1627
|
+
"aria-expanded": open,
|
|
1628
|
+
className: cn(traceClasses, traceButtonClasses, "qsl:items-center qsl:pl-1", active && "qsl:animate-fade-pulse qsl:motion-reduce:animate-none"),
|
|
1629
|
+
onClick: () => setOpen((value) => !value),
|
|
1630
|
+
ref: header,
|
|
1631
|
+
type: "button",
|
|
1632
|
+
children: [
|
|
1633
|
+
/* @__PURE__ */ jsx13(ChevronRightIcon, { className: cn("qsl:flex-none qsl:opacity-60 qsl:transition-transform", open && "qsl:rotate-90"), size: 12 }),
|
|
1634
|
+
summary
|
|
1635
|
+
]
|
|
1636
|
+
}
|
|
1637
|
+
),
|
|
1638
|
+
open && /* @__PURE__ */ jsx13("div", { className: "qsl:mt-0.5 qsl:flex qsl:flex-col qsl:gap-[var(--qsl-tool-group-gap,1px)] qsl:pl-3", children: messages.map((message) => /* @__PURE__ */ jsx13(TraceLine, { className: "qsl:pl-3", elapsedAfter, expandable, message, texts, toolArgumentsText }, message.key)) }),
|
|
1639
|
+
open && messages.length >= collapseAtBottomFrom && /* @__PURE__ */ jsxs6(
|
|
1640
|
+
"button",
|
|
1641
|
+
{
|
|
1642
|
+
className: cn(traceClasses, traceButtonClasses, "qsl:mt-0.5 qsl:items-center qsl:pl-1"),
|
|
1643
|
+
"data-step": "group-collapse",
|
|
1644
|
+
onClick: () => {
|
|
1645
|
+
collapsedFromBottom.current = true;
|
|
1646
|
+
setOpen(false);
|
|
1647
|
+
},
|
|
1648
|
+
type: "button",
|
|
1649
|
+
children: [
|
|
1650
|
+
/* @__PURE__ */ jsx13(ChevronUpIcon, { className: "qsl:flex-none qsl:opacity-60", size: 12 }),
|
|
1651
|
+
/* @__PURE__ */ jsx13("span", { className: rowLineClasses, children: texts.stepGroupCollapse })
|
|
1652
|
+
]
|
|
1653
|
+
}
|
|
1654
|
+
)
|
|
1655
|
+
] });
|
|
1656
|
+
}
|
|
1657
|
+
function TraceLine({
|
|
1658
|
+
message,
|
|
1659
|
+
className,
|
|
1660
|
+
elapsedAfter,
|
|
1661
|
+
expandable,
|
|
1662
|
+
texts,
|
|
1663
|
+
toolArgumentsText
|
|
1664
|
+
}) {
|
|
1665
|
+
const [detailPosition, setDetailPosition] = useState4();
|
|
1666
|
+
const thinking = message.role === "thinking";
|
|
1667
|
+
const tool = message.tool;
|
|
1668
|
+
const classes = cn(traceClasses, "qsl:items-center", className);
|
|
1669
|
+
const line = cn(rowLineClasses, thinking && "qsl:italic qsl:whitespace-pre-wrap", "qsl:overflow-hidden qsl:text-ellipsis qsl:whitespace-nowrap");
|
|
1670
|
+
const content = /* @__PURE__ */ jsxs6(Fragment2, { children: [
|
|
1671
|
+
thinking ? /* @__PURE__ */ jsx13(SparklesIcon2, { className: rowIconClasses, size: 12 }) : /* @__PURE__ */ jsx13(WrenchIcon2, { className: tool?.isError ? rowErrorIconClasses : rowIconClasses, size: 12 }),
|
|
1672
|
+
/* @__PURE__ */ jsxs6("span", { className: line, children: [
|
|
1673
|
+
message.text,
|
|
1674
|
+
tool && tool.result === void 0 && /* @__PURE__ */ jsx13("span", { className: traceRunningClasses, children: texts.toolRunning }),
|
|
1675
|
+
/* @__PURE__ */ jsx13(ToolElapsed, { after: elapsedAfter, message, texts })
|
|
1676
|
+
] })
|
|
1677
|
+
] });
|
|
1678
|
+
const marks = { "data-compact": "true", "data-kind": thinking ? "thinking" : "tool", "data-step": "line" };
|
|
1679
|
+
if (!expandable) {
|
|
1680
|
+
return /* @__PURE__ */ jsx13("div", { className: classes, ...marks, children: content });
|
|
1681
|
+
}
|
|
1682
|
+
return /* @__PURE__ */ jsxs6(Fragment2, { children: [
|
|
1683
|
+
/* @__PURE__ */ jsx13(
|
|
1684
|
+
"button",
|
|
1685
|
+
{
|
|
1686
|
+
"aria-expanded": detailPosition !== void 0,
|
|
1687
|
+
"aria-haspopup": "dialog",
|
|
1688
|
+
className: cn(classes, traceButtonClasses),
|
|
1689
|
+
...marks,
|
|
1690
|
+
onClick: (event) => setDetailPosition(stepPopoverOrigin(event)),
|
|
1691
|
+
type: "button",
|
|
1692
|
+
children: content
|
|
1693
|
+
}
|
|
1694
|
+
),
|
|
1695
|
+
detailPosition && /* @__PURE__ */ jsx13(
|
|
1696
|
+
StepPopover,
|
|
1697
|
+
{
|
|
1698
|
+
message,
|
|
1699
|
+
position: detailPosition,
|
|
1700
|
+
texts,
|
|
1701
|
+
toolArgumentsText,
|
|
1702
|
+
onClose: () => setDetailPosition(void 0)
|
|
1703
|
+
}
|
|
1704
|
+
)
|
|
1705
|
+
] });
|
|
1706
|
+
}
|
|
1707
|
+
function ToolElapsed({ message, after, texts, className = traceRunningClasses }) {
|
|
1708
|
+
const start = stepState(message) === "running" ? messageDate(message.at)?.getTime() : void 0;
|
|
1709
|
+
return after === void 0 || start === void 0 ? null : /* @__PURE__ */ jsx13(Elapsed, { after, className, start, texts });
|
|
1710
|
+
}
|
|
1711
|
+
function Elapsed({ start, after, className, texts }) {
|
|
1712
|
+
const elapsed = useElapsed(start);
|
|
1713
|
+
if (elapsed === void 0 || elapsed < after) return null;
|
|
1714
|
+
return /* @__PURE__ */ jsx13("span", { "aria-hidden": "true", className: cn(className, "qsl:tabular-nums"), "data-step": "elapsed", children: formatElapsed(elapsed, texts) });
|
|
1715
|
+
}
|
|
1716
|
+
function Bubble({
|
|
1717
|
+
message,
|
|
1718
|
+
plain,
|
|
1719
|
+
detailMode,
|
|
1720
|
+
dense,
|
|
1721
|
+
elapsedAfter,
|
|
1722
|
+
expandable,
|
|
1723
|
+
texts,
|
|
1724
|
+
toolArgumentsText,
|
|
1725
|
+
renderAction,
|
|
1726
|
+
onDismissAction,
|
|
1727
|
+
bubbleOptions,
|
|
1728
|
+
messageActions
|
|
1729
|
+
}) {
|
|
1730
|
+
const stepSpacing = dense ? denseStepClasses : stepClasses;
|
|
1731
|
+
if (message.role === "thinking" || message.role === "tool" && message.tool) {
|
|
1732
|
+
if (detailMode === "compact") {
|
|
1733
|
+
return /* @__PURE__ */ jsx13(TraceLine, { className: stepSpacing, elapsedAfter, expandable, message, texts, toolArgumentsText });
|
|
1734
|
+
}
|
|
1735
|
+
const thinking = message.role === "thinking";
|
|
1736
|
+
const tool = message.tool;
|
|
1737
|
+
const icon = thinking ? /* @__PURE__ */ jsx13(SparklesIcon2, { className: traceIconClasses, size: 12 }) : /* @__PURE__ */ jsx13(WrenchIcon2, { className: tool?.isError ? traceErrorIconClasses : traceIconClasses, size: 12 });
|
|
1738
|
+
const line = /* @__PURE__ */ jsxs6(Fragment2, { children: [
|
|
1739
|
+
message.text,
|
|
1740
|
+
tool && tool.result === void 0 && /* @__PURE__ */ jsx13("span", { className: traceRunningClasses, children: texts.toolRunning }),
|
|
1741
|
+
/* @__PURE__ */ jsx13(ToolElapsed, { after: elapsedAfter, message, texts })
|
|
1742
|
+
] });
|
|
1743
|
+
return /* @__PURE__ */ jsxs6("div", { className: cn(traceClasses, stepSpacing), "data-kind": thinking ? "thinking" : "tool", "data-step": "detail", children: [
|
|
1744
|
+
icon,
|
|
1745
|
+
/* @__PURE__ */ jsxs6("div", { className: "qsl:min-w-0 qsl:flex-1", children: [
|
|
1746
|
+
/* @__PURE__ */ jsx13("div", { className: cn(traceLineClasses, thinking && "qsl:italic qsl:whitespace-pre-wrap"), children: line }),
|
|
1747
|
+
tool && /* @__PURE__ */ jsxs6("div", { className: "qsl:mt-0.5 qsl:border-l-2 qsl:border-border-soft qsl:pl-2", children: [
|
|
1748
|
+
/* @__PURE__ */ jsx13("pre", { className: "qsl:mb-0.5 qsl:max-h-[208px] qsl:overflow-auto qsl:overscroll-contain qsl:font-mono qsl:text-meta qsl:leading-[1.375] qsl:break-all qsl:whitespace-pre-wrap qsl:opacity-80", children: toolArgumentsText(tool) }),
|
|
1749
|
+
tool.result !== void 0 && /* @__PURE__ */ jsx13("pre", { className: cn(
|
|
1750
|
+
"qsl:mb-0.5 qsl:max-h-[208px] qsl:overflow-auto qsl:overscroll-contain qsl:font-mono qsl:text-meta qsl:leading-[1.375] qsl:break-all qsl:whitespace-pre-wrap qsl:opacity-80",
|
|
1751
|
+
tool.isError && "qsl:text-destructive qsl:opacity-100"
|
|
1752
|
+
), children: prettyJson(tool.result) })
|
|
1753
|
+
] })
|
|
1754
|
+
] })
|
|
1755
|
+
] });
|
|
1756
|
+
}
|
|
1757
|
+
if (message.role === "action" && message.action) {
|
|
1758
|
+
return /* @__PURE__ */ jsx13("div", { className: stepSpacing, children: /* @__PURE__ */ jsx13(ActionContent, { action: message.action, onDismissAction, renderAction, text: message.text, texts }) });
|
|
1759
|
+
}
|
|
1760
|
+
if (message.role === "system") {
|
|
1761
|
+
return /* @__PURE__ */ jsxs6("div", { className: cn(stepSpacing, "qsl:border-l-2 qsl:border-destructive/50 qsl:pl-3 qsl:text-sm qsl:text-muted-foreground"), "data-message": "system", children: [
|
|
1762
|
+
/* @__PURE__ */ jsx13(Markdown, { text: message.text }),
|
|
1763
|
+
/* @__PURE__ */ jsx13(MessageAttachments, { label: texts.attachments, message })
|
|
1764
|
+
] });
|
|
1765
|
+
}
|
|
1766
|
+
if (!message.text.trim() && !message.attachments?.length) return null;
|
|
1767
|
+
const variant = bubbleOptions?.variant ?? "default";
|
|
1768
|
+
const useBubble = !plain && variant !== "plain" && (variant === "bubbles" || message.role === "user" || !!message.bubble);
|
|
1769
|
+
const side = (message.role === "user" ? bubbleOptions?.userSide : bubbleOptions?.assistantSide) ?? message.bubble?.side ?? (message.role === "user" ? "end" : "start");
|
|
1770
|
+
const sender = bubbleOptions?.senderLabel?.(message) ?? message.bubble?.label ?? message.sender;
|
|
1771
|
+
const showSender = bubbleOptions?.showSender ?? (!!message.bubble?.label && !plain);
|
|
1772
|
+
const content = /* @__PURE__ */ jsxs6(Fragment2, { children: [
|
|
1773
|
+
showSender && sender && /* @__PURE__ */ jsx13("span", { className: cn(
|
|
1774
|
+
"qsl:mb-[5px] qsl:table qsl:text-label qsl:leading-[1.45] qsl:font-semibold",
|
|
1775
|
+
useBubble && message.bubble && "qsl:rounded-full qsl:border qsl:border-white/30 qsl:bg-black/15 qsl:px-[7px] qsl:py-px qsl:tracking-[0.02em]"
|
|
1776
|
+
), "data-chat": "sender", children: sender }),
|
|
1777
|
+
/* @__PURE__ */ jsx13(Markdown, { text: message.text, streaming: message.role === "assistant" && !message.closed }),
|
|
1778
|
+
/* @__PURE__ */ jsx13(MessageAttachments, { label: texts.attachments, message }),
|
|
1779
|
+
message.steered && /* @__PURE__ */ jsx13("small", { className: "qsl:mt-1 qsl:block qsl:text-label qsl:leading-[1.45] qsl:opacity-70", "data-chat": "steered", children: texts.steered }),
|
|
1780
|
+
/* @__PURE__ */ jsx13(MessageActions, { message, options: messageActions, texts })
|
|
1781
|
+
] });
|
|
1782
|
+
const cursor = message.role === "assistant" && message.textCursor ? JSON.stringify(message.textCursor) : void 0;
|
|
1783
|
+
if (!useBubble) return /* @__PURE__ */ jsx13("div", { "data-chat-text-cursor": cursor, className: cn(stepSpacing, "qsl:group qsl:relative qsl:w-full qsl:leading-[var(--qsl-chat-line-height,1.5)] qsl:text-foreground"), "data-message": "answer", children: content });
|
|
1784
|
+
const fullBackground = message.role === "user" && bubbleOptions?.userBackgroundFullWidth !== false;
|
|
1785
|
+
const surfaceClasses = message.bubble ? "qsl:rounded-lg" : "qsl:rounded-[var(--qsl-bubble-radius,10px_10px_4px_10px)] qsl:border qsl:border-border qsl:bg-secondary qsl:in-data-[tone=material]:rounded-[var(--qsl-bubble-radius,9px)] qsl:in-data-[tone=material]:border-glass-edge/20 qsl:in-data-[tone=material]:bg-primary/15";
|
|
1786
|
+
return /* @__PURE__ */ jsxs6(
|
|
1787
|
+
"div",
|
|
1788
|
+
{
|
|
1789
|
+
"data-chat-text-cursor": cursor,
|
|
1790
|
+
className: cn(stepSpacing, "qsl:relative qsl:flex", side === "end" ? "qsl:justify-end" : "qsl:justify-start"),
|
|
1791
|
+
"data-message": message.role === "user" && !message.bubble && variant === "default" ? "user" : "bubble",
|
|
1792
|
+
"data-side": side,
|
|
1793
|
+
"data-background-width": fullBackground ? "full" : "content",
|
|
1794
|
+
children: [
|
|
1795
|
+
fullBackground && /* @__PURE__ */ jsx13("div", { "aria-hidden": "true", className: cn("qsl:pointer-events-none qsl:absolute qsl:inset-0", surfaceClasses), "data-chat": "bubble-background", style: { background: message.bubble?.color } }),
|
|
1796
|
+
/* @__PURE__ */ jsx13(
|
|
1797
|
+
"div",
|
|
1798
|
+
{
|
|
1799
|
+
className: cn(bubbleBodyClasses, "qsl:group", !fullBackground && surfaceClasses, fullBackground && !message.bubble && "qsl:border qsl:border-transparent", message.bubble ? "qsl:text-white qsl:[--qsl-scroll-cover:var(--qsl-primary)]" : "qsl:text-foreground qsl:[--qsl-scroll-cover:var(--qsl-secondary)]"),
|
|
1800
|
+
"data-chat": "bubble-content",
|
|
1801
|
+
"data-tone": message.bubble ? "on-color" : void 0,
|
|
1802
|
+
style: { background: fullBackground ? void 0 : message.bubble?.color, maxWidth: bubbleOptions?.maxWidth },
|
|
1803
|
+
children: content
|
|
1804
|
+
}
|
|
1805
|
+
)
|
|
1806
|
+
]
|
|
1807
|
+
}
|
|
1808
|
+
);
|
|
1809
|
+
}
|
|
1810
|
+
function ActionContent({
|
|
1811
|
+
action,
|
|
1812
|
+
text,
|
|
1813
|
+
texts,
|
|
1814
|
+
renderAction,
|
|
1815
|
+
onDismissAction
|
|
1816
|
+
}) {
|
|
1817
|
+
return renderAction?.(action, text) ?? /* @__PURE__ */ jsx13(
|
|
1818
|
+
PendingActionCard,
|
|
1819
|
+
{
|
|
1820
|
+
action,
|
|
1821
|
+
dismissedLabel: texts.actionDismissed,
|
|
1822
|
+
dismissLabel: texts.dismissAction,
|
|
1823
|
+
onDismiss: onDismissAction ? () => onDismissAction(action.actionId) : void 0,
|
|
1824
|
+
text,
|
|
1825
|
+
waitingLabel: texts.pendingAction
|
|
1826
|
+
}
|
|
1827
|
+
);
|
|
1828
|
+
}
|
|
1829
|
+
function MessageAttachments({ message, label }) {
|
|
1830
|
+
if (!message.attachments?.length) return null;
|
|
1831
|
+
return /* @__PURE__ */ jsx13("ul", { className: "qsl:mt-2.5 qsl:flex qsl:flex-wrap qsl:gap-2", "aria-label": label, children: message.attachments.map((attachment, index) => /* @__PURE__ */ jsxs6("li", { className: "qsl:relative qsl:flex qsl:w-[190px] qsl:max-w-full qsl:min-w-0 qsl:flex-col qsl:overflow-hidden qsl:rounded-lg qsl:border qsl:border-border qsl:text-sm", children: [
|
|
1832
|
+
attachment.mediaType.startsWith("image/") && /* @__PURE__ */ jsx13("a", { href: attachmentDownloadUrl(attachment.url), download: attachment.name, children: /* @__PURE__ */ jsx13("img", { className: "qsl:block qsl:h-[112px] qsl:w-full qsl:bg-secondary qsl:object-contain", src: attachment.url, alt: attachment.name, loading: "lazy" }) }),
|
|
1833
|
+
attachment.mediaType.startsWith("video/") && /* @__PURE__ */ jsx13("video", { className: "qsl:block qsl:h-[112px] qsl:w-full qsl:bg-secondary qsl:object-contain", src: attachment.url, controls: true, preload: "metadata", "aria-label": attachment.name }),
|
|
1834
|
+
/* @__PURE__ */ jsxs6("div", { className: "qsl:flex qsl:flex-col qsl:gap-1 qsl:p-2 qsl:[overflow-wrap:anywhere]", children: [
|
|
1835
|
+
/* @__PURE__ */ jsx13("a", { className: "qsl:font-medium", href: attachmentDownloadUrl(attachment.url), download: attachment.name, children: attachment.name }),
|
|
1836
|
+
/* @__PURE__ */ jsx13("small", { children: formatAttachmentSize(attachment.size) })
|
|
1837
|
+
] })
|
|
1838
|
+
] }, `${attachment.url}-${index}`)) });
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
// packages/quassel/src/ChatPanel.tsx
|
|
1842
|
+
import { useEffect as useEffect4, useLayoutEffect as useLayoutEffect2, useRef as useRef4, useState as useState5 } from "react";
|
|
1843
|
+
import { jsx as jsx14, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1844
|
+
function ChatPanel({
|
|
1845
|
+
children,
|
|
1846
|
+
composer,
|
|
1847
|
+
className,
|
|
1848
|
+
nodeRef,
|
|
1849
|
+
maxWidth,
|
|
1850
|
+
horizontalPadding,
|
|
1851
|
+
appearance,
|
|
1852
|
+
scrollOnSend = false
|
|
1853
|
+
}) {
|
|
1854
|
+
const root = useRef4(null);
|
|
1855
|
+
const composerRef = useRef4(null);
|
|
1856
|
+
const [dock, setDock] = useState5(null);
|
|
1857
|
+
const hasComposer = composer !== void 0;
|
|
1858
|
+
const scrollOnSendRef = useRef4(scrollOnSend);
|
|
1859
|
+
const [sendScope] = useState5(() => createChatSendScope(scrollOnSendRef));
|
|
1860
|
+
useLayoutEffect2(() => {
|
|
1861
|
+
scrollOnSendRef.current = scrollOnSend;
|
|
1862
|
+
}, [scrollOnSend]);
|
|
1863
|
+
useEffect4(() => {
|
|
1864
|
+
const element = composerRef.current;
|
|
1865
|
+
const frame = root.current;
|
|
1866
|
+
if (!element || !frame) {
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
const measure = () => {
|
|
1870
|
+
frame.style.setProperty("--qsl-composer-height", `${element.offsetHeight}px`);
|
|
1871
|
+
frame.style.setProperty("--qsl-panel-height", `${frame.offsetHeight}px`);
|
|
1872
|
+
};
|
|
1873
|
+
const observer = new ResizeObserver(measure);
|
|
1874
|
+
observer.observe(element);
|
|
1875
|
+
observer.observe(frame);
|
|
1876
|
+
measure();
|
|
1877
|
+
return () => {
|
|
1878
|
+
observer.disconnect();
|
|
1879
|
+
frame.style.removeProperty("--qsl-composer-height");
|
|
1880
|
+
frame.style.removeProperty("--qsl-panel-height");
|
|
1881
|
+
};
|
|
1882
|
+
}, [hasComposer]);
|
|
1883
|
+
return /* @__PURE__ */ jsx14(ChatSendContext.Provider, { value: sendScope, children: /* @__PURE__ */ jsx14(ChatActionDock.Provider, { value: hasComposer ? dock : void 0, children: /* @__PURE__ */ jsxs7(
|
|
1884
|
+
"div",
|
|
1885
|
+
{
|
|
1886
|
+
className: cn("qsl:relative qsl:flex qsl:min-h-0 qsl:min-w-0 qsl:flex-col qsl:overflow-hidden", className),
|
|
1887
|
+
"data-chat": "panel",
|
|
1888
|
+
"data-quassel": "",
|
|
1889
|
+
style: {
|
|
1890
|
+
...appearanceStyle(appearance),
|
|
1891
|
+
"--qsl-chat-content-max-width": typeof maxWidth === "number" ? `${maxWidth}px` : maxWidth,
|
|
1892
|
+
"--qsl-chat-horizontal-padding": typeof horizontalPadding === "number" ? `${horizontalPadding}px` : horizontalPadding
|
|
1893
|
+
},
|
|
1894
|
+
ref: (element) => {
|
|
1895
|
+
root.current = element;
|
|
1896
|
+
if (typeof nodeRef === "function") {
|
|
1897
|
+
nodeRef(element);
|
|
1898
|
+
} else if (nodeRef) {
|
|
1899
|
+
nodeRef.current = element;
|
|
1900
|
+
}
|
|
1901
|
+
},
|
|
1902
|
+
children: [
|
|
1903
|
+
children,
|
|
1904
|
+
hasComposer && /* @__PURE__ */ jsxs7("div", { className: "qsl:absolute qsl:inset-x-0 qsl:bottom-0 qsl:z-[5] qsl:px-[var(--qsl-chat-horizontal-padding,24px)] qsl:pb-3.5 qsl:*:mx-auto qsl:*:max-w-[var(--qsl-chat-content-max-width,none)]", "data-chat": "composer", ref: composerRef, children: [
|
|
1905
|
+
/* @__PURE__ */ jsx14("div", { children: /* @__PURE__ */ jsx14(
|
|
1906
|
+
"div",
|
|
1907
|
+
{
|
|
1908
|
+
className: "qsl:-mx-[16px] qsl:max-h-[calc(var(--qsl-panel-height,100dvh)_/_2)] qsl:overflow-y-auto qsl:overscroll-contain qsl:px-[16px] qsl:pb-[var(--qsl-chat-message-gap,10px)] qsl:[-webkit-mask-image:linear-gradient(to_bottom,#000_calc(100%_-_var(--qsl-chat-message-gap,10px)),transparent)] qsl:[mask-image:linear-gradient(to_bottom,#000_calc(100%_-_var(--qsl-chat-message-gap,10px)),transparent)] qsl:empty:hidden",
|
|
1909
|
+
"data-chat": "actions",
|
|
1910
|
+
ref: setDock
|
|
1911
|
+
}
|
|
1912
|
+
) }),
|
|
1913
|
+
composer
|
|
1914
|
+
] })
|
|
1915
|
+
]
|
|
1916
|
+
}
|
|
1917
|
+
) }) });
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
// packages/quassel/src/ChatInputToolbar.tsx
|
|
1921
|
+
import { PaperclipIcon, SendIcon, XIcon as XIcon3 } from "lucide-react";
|
|
1922
|
+
|
|
1923
|
+
// packages/quassel/src/ui/useFileInput.ts
|
|
1924
|
+
import { useRef as useRef5, useState as useState6 } from "react";
|
|
1925
|
+
function useFileInput({ disabled, onFiles, onPasteText }) {
|
|
1926
|
+
const inputRef = useRef5(null);
|
|
1927
|
+
const [dragging, setDragging] = useState6(false);
|
|
1928
|
+
const receive = (files) => {
|
|
1929
|
+
if (files.length) void onFiles(files);
|
|
1930
|
+
};
|
|
1931
|
+
return {
|
|
1932
|
+
inputRef,
|
|
1933
|
+
dragging,
|
|
1934
|
+
clearDragging: () => setDragging(false),
|
|
1935
|
+
inputProps: {
|
|
1936
|
+
ref: inputRef,
|
|
1937
|
+
onChange: (event) => {
|
|
1938
|
+
const files = Array.from(event.currentTarget.files ?? []);
|
|
1939
|
+
event.currentTarget.value = "";
|
|
1940
|
+
receive(files);
|
|
1941
|
+
}
|
|
1942
|
+
},
|
|
1943
|
+
dropProps: {
|
|
1944
|
+
onDragOver: (event) => {
|
|
1945
|
+
if (!event.dataTransfer.types.includes("Files")) return;
|
|
1946
|
+
event.preventDefault();
|
|
1947
|
+
event.dataTransfer.dropEffect = disabled ? "none" : "copy";
|
|
1948
|
+
setDragging(!disabled);
|
|
1949
|
+
},
|
|
1950
|
+
onDragLeave: (event) => {
|
|
1951
|
+
if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
|
|
1952
|
+
},
|
|
1953
|
+
onDrop: (event) => {
|
|
1954
|
+
if (!event.dataTransfer.types.includes("Files")) return;
|
|
1955
|
+
event.preventDefault();
|
|
1956
|
+
setDragging(false);
|
|
1957
|
+
receive(Array.from(event.dataTransfer.files));
|
|
1958
|
+
},
|
|
1959
|
+
onPaste: (event) => {
|
|
1960
|
+
const files = Array.from(event.clipboardData.files);
|
|
1961
|
+
if (!files.length) return;
|
|
1962
|
+
event.preventDefault();
|
|
1963
|
+
receive(files);
|
|
1964
|
+
const text = event.clipboardData.getData("text/plain");
|
|
1965
|
+
if (text) onPasteText?.(text);
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
};
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
// packages/quassel/src/ChatInputToolbar.tsx
|
|
1972
|
+
import { useCallback as useCallback3, useContext as useContext4, useEffect as useEffect5, useImperativeHandle, useLayoutEffect as useLayoutEffect3, useRef as useRef6, useState as useState7 } from "react";
|
|
1973
|
+
import { createPortal as createPortal2 } from "react-dom";
|
|
1974
|
+
import { Fragment as Fragment3, jsx as jsx15, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1975
|
+
var COMPACT_WIDTH_PX = 480;
|
|
1976
|
+
var errorClasses = "qsl:px-3 qsl:py-2 qsl:text-sm qsl:text-destructive";
|
|
1977
|
+
var attachmentItemClasses = "qsl:relative qsl:flex qsl:w-[190px] qsl:max-w-full qsl:min-w-0 qsl:flex-col qsl:overflow-hidden qsl:rounded-lg qsl:border qsl:border-border qsl:text-sm";
|
|
1978
|
+
var attachmentMediaClasses = "qsl:block qsl:h-[112px] qsl:w-full qsl:bg-secondary qsl:object-contain";
|
|
1979
|
+
function ChatInputToolbar({
|
|
1980
|
+
onSend,
|
|
1981
|
+
onStop,
|
|
1982
|
+
running: running2 = false,
|
|
1983
|
+
disabled = false,
|
|
1984
|
+
sendDisabled = false,
|
|
1985
|
+
layout = "card",
|
|
1986
|
+
detailsContainer,
|
|
1987
|
+
onErrorChange,
|
|
1988
|
+
inputAriaControls,
|
|
1989
|
+
rows = 3,
|
|
1990
|
+
maxRows,
|
|
1991
|
+
actions,
|
|
1992
|
+
toolbarLeft,
|
|
1993
|
+
toolbarRight,
|
|
1994
|
+
texts,
|
|
1995
|
+
handleRef,
|
|
1996
|
+
attachments: attachmentsEnabled = true,
|
|
1997
|
+
attachmentCapabilities,
|
|
1998
|
+
attachmentCapabilitiesError,
|
|
1999
|
+
onAttachmentsChange,
|
|
2000
|
+
initialValue = "",
|
|
2001
|
+
onDraftChange,
|
|
2002
|
+
sendShortcut = "enter",
|
|
2003
|
+
autoFocus = false,
|
|
2004
|
+
onAutoFocusSettled
|
|
2005
|
+
}) {
|
|
2006
|
+
const sendScope = useContext4(ChatSendContext);
|
|
2007
|
+
const allTexts = { ...defaultTexts, ...texts };
|
|
2008
|
+
const { Button: Button2, StopButton: StopButton2, Toggle: Toggle2 } = useQuasselComponents();
|
|
2009
|
+
const [draft, setDraft] = useState7(initialValue);
|
|
2010
|
+
const [sending, setSending] = useState7(false);
|
|
2011
|
+
const [sendError, setSendError] = useState7();
|
|
2012
|
+
const [failedSubmissions, setFailedSubmissions] = useState7([]);
|
|
2013
|
+
const nextFailedId = useRef6(0);
|
|
2014
|
+
const pendingSend = useRef6(false);
|
|
2015
|
+
const generation = useRef6(0);
|
|
2016
|
+
useLayoutEffect3(() => () => {
|
|
2017
|
+
generation.current += 1;
|
|
2018
|
+
}, []);
|
|
2019
|
+
const draftRevision = useRef6(0);
|
|
2020
|
+
const [attachments, setAttachments] = useState7([]);
|
|
2021
|
+
const attachmentsRef = useRef6([]);
|
|
2022
|
+
const nextAttachmentId = useRef6(0);
|
|
2023
|
+
const [attachmentError, setAttachmentError] = useState7();
|
|
2024
|
+
const updateAttachments = (update) => {
|
|
2025
|
+
attachmentsRef.current = update(attachmentsRef.current);
|
|
2026
|
+
setAttachments(attachmentsRef.current);
|
|
2027
|
+
};
|
|
2028
|
+
const addFiles = (files) => {
|
|
2029
|
+
if (!files.length) return;
|
|
2030
|
+
if (disabled || pendingSend.current) {
|
|
2031
|
+
setAttachmentError(allTexts.attachmentsWhileBusy);
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
2034
|
+
try {
|
|
2035
|
+
validateAttachmentSelection(attachmentsRef.current.map((entry) => entry.file), files, allTexts);
|
|
2036
|
+
} catch (error) {
|
|
2037
|
+
setAttachmentError(error instanceof Error ? error.message : String(error));
|
|
2038
|
+
return;
|
|
2039
|
+
}
|
|
2040
|
+
setAttachmentError(void 0);
|
|
2041
|
+
const entries = files.map((file) => ({ id: ++nextAttachmentId.current, file, status: "reading" }));
|
|
2042
|
+
updateAttachments((current) => [...current, ...entries]);
|
|
2043
|
+
for (const entry of entries) {
|
|
2044
|
+
void encodeAttachment(entry.file).then(
|
|
2045
|
+
(input2) => updateAttachments((current) => current.map((item) => item.id === entry.id ? { ...entry, status: "ready", input: input2 } : item)),
|
|
2046
|
+
() => updateAttachments((current) => current.map((item) => item.id === entry.id ? {
|
|
2047
|
+
...entry,
|
|
2048
|
+
status: "error",
|
|
2049
|
+
error: allTexts.attachmentReadFailed
|
|
2050
|
+
} : item))
|
|
2051
|
+
);
|
|
2052
|
+
}
|
|
2053
|
+
};
|
|
2054
|
+
const rootRef = useRef6(null);
|
|
2055
|
+
const textareaRef = useRef6(null);
|
|
2056
|
+
const autoFocusSettled = useRef6(false);
|
|
2057
|
+
useEffect5(() => {
|
|
2058
|
+
if (!autoFocus || autoFocusSettled.current) return;
|
|
2059
|
+
const input2 = textareaRef.current;
|
|
2060
|
+
if (!input2) return;
|
|
2061
|
+
let frame = 0;
|
|
2062
|
+
const settle = () => {
|
|
2063
|
+
if (autoFocusSettled.current) return;
|
|
2064
|
+
autoFocusSettled.current = true;
|
|
2065
|
+
window.cancelAnimationFrame(frame);
|
|
2066
|
+
onAutoFocusSettled?.();
|
|
2067
|
+
};
|
|
2068
|
+
const onFocus = (event) => {
|
|
2069
|
+
if (event.target !== input2) settle();
|
|
2070
|
+
};
|
|
2071
|
+
const focusWhenVisible = () => {
|
|
2072
|
+
if (autoFocusSettled.current) return;
|
|
2073
|
+
const dialog = [...document.querySelectorAll('[role="dialog"], dialog[open]')].find((element) => element.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true }) && !element.contains(input2));
|
|
2074
|
+
if (dialog) {
|
|
2075
|
+
settle();
|
|
2076
|
+
return;
|
|
2077
|
+
}
|
|
2078
|
+
const rect = input2.getBoundingClientRect();
|
|
2079
|
+
const visible = input2.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true }) && rect.width > 0 && rect.height > 0 && !input2.closest('[inert], [hidden], [aria-hidden="true"]') && document.elementFromPoint(rect.x + rect.width / 2, rect.y + rect.height / 2) === input2;
|
|
2080
|
+
if (visible && !input2.disabled && !input2.readOnly && document.visibilityState === "visible") {
|
|
2081
|
+
input2.focus({ preventScroll: true });
|
|
2082
|
+
if (document.activeElement === input2) {
|
|
2083
|
+
settle();
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
frame = window.requestAnimationFrame(focusWhenVisible);
|
|
2088
|
+
};
|
|
2089
|
+
window.addEventListener("pointerdown", settle, true);
|
|
2090
|
+
window.addEventListener("keydown", settle, true);
|
|
2091
|
+
window.addEventListener("focusin", onFocus);
|
|
2092
|
+
window.addEventListener("blur", settle);
|
|
2093
|
+
if (!disabled) frame = window.requestAnimationFrame(focusWhenVisible);
|
|
2094
|
+
return () => {
|
|
2095
|
+
window.cancelAnimationFrame(frame);
|
|
2096
|
+
window.removeEventListener("pointerdown", settle, true);
|
|
2097
|
+
window.removeEventListener("keydown", settle, true);
|
|
2098
|
+
window.removeEventListener("focusin", onFocus);
|
|
2099
|
+
window.removeEventListener("blur", settle);
|
|
2100
|
+
};
|
|
2101
|
+
}, [autoFocus, disabled, onAutoFocusSettled]);
|
|
2102
|
+
const [compact, setCompact] = useState7(false);
|
|
2103
|
+
const pendingFocus = useRef6(false);
|
|
2104
|
+
const fileInput = useFileInput({
|
|
2105
|
+
disabled: disabled || sending,
|
|
2106
|
+
onFiles: addFiles,
|
|
2107
|
+
onPasteText: (text) => {
|
|
2108
|
+
const input2 = textareaRef.current;
|
|
2109
|
+
if (!input2 || disabled || pendingSend.current) return;
|
|
2110
|
+
const start = input2.selectionStart;
|
|
2111
|
+
const end = input2.selectionEnd;
|
|
2112
|
+
draftRevision.current += 1;
|
|
2113
|
+
setDraft((current) => current.slice(0, start) + text + current.slice(end));
|
|
2114
|
+
pendingFocus.current = true;
|
|
2115
|
+
}
|
|
2116
|
+
});
|
|
2117
|
+
const toolbar = layout === "toolbar";
|
|
2118
|
+
const inline = layout === "inline";
|
|
2119
|
+
const inputRows = inline || toolbar ? 1 : rows;
|
|
2120
|
+
const inputMaxRows = toolbar ? 1 : inline ? maxRows ?? 1 : maxRows;
|
|
2121
|
+
const rootClasses = cn(
|
|
2122
|
+
"qsl:flex qsl:shrink-0 qsl:flex-col qsl:overflow-hidden qsl:rounded-[var(--qsl-input-card-radius,--theme(--radius-xl))] qsl:border qsl:border-border-strong qsl:bg-background qsl:shadow-bar qsl:transition-colors qsl:focus-within:border-primary",
|
|
2123
|
+
"qsl:in-data-[tone=material]:rounded-xl qsl:in-data-[tone=material]:border-glass-edge/50 qsl:in-data-[tone=material]:bg-background",
|
|
2124
|
+
toolbar && "qsl:h-full qsl:min-w-0 qsl:flex-row qsl:items-center qsl:rounded-lg qsl:data-[working=true]:animate-working-pulse qsl:data-[working=true]:border-primary qsl:motion-reduce:data-[working=true]:animate-none",
|
|
2125
|
+
fileInput.dragging && "qsl:outline-2 qsl:outline-offset-[3px] qsl:outline-dashed qsl:outline-primary"
|
|
2126
|
+
);
|
|
2127
|
+
const textareaClasses = cn(
|
|
2128
|
+
"qsl:resize-none qsl:overscroll-contain qsl:border-0 qsl:bg-transparent qsl:text-chat qsl:leading-[var(--qsl-chat-line-height,1.5)] qsl:text-foreground qsl:placeholder:text-muted-foreground qsl:focus:outline-none",
|
|
2129
|
+
inline ? "qsl:min-w-0 qsl:flex-1 qsl:px-2.5 qsl:py-2" : toolbar ? "qsl:w-full qsl:min-w-0 qsl:flex-auto qsl:px-2 qsl:py-[3px] qsl:leading-[1.3]" : "qsl:px-3 qsl:pt-3 qsl:pb-2"
|
|
2130
|
+
);
|
|
2131
|
+
const toolbarRowClasses = cn(
|
|
2132
|
+
"qsl:flex qsl:items-center qsl:gap-2 qsl:border-t qsl:border-border-soft qsl:px-2 qsl:py-1.5 qsl:in-data-[tone=overseer-details]:flex-nowrap",
|
|
2133
|
+
inline && "qsl:flex-none qsl:gap-1 qsl:border-t-0 qsl:p-1",
|
|
2134
|
+
toolbar && "qsl:flex-wrap qsl:border-t-0"
|
|
2135
|
+
);
|
|
2136
|
+
const toolbarSideClasses = "qsl:flex qsl:min-w-0 qsl:items-center qsl:gap-2 qsl:in-data-[tone=overseer-details]:flex-nowrap";
|
|
2137
|
+
const changeDraft = useCallback3((text) => {
|
|
2138
|
+
draftRevision.current += 1;
|
|
2139
|
+
setDraft(text);
|
|
2140
|
+
}, []);
|
|
2141
|
+
useImperativeHandle(handleRef, () => ({
|
|
2142
|
+
insert: (text) => {
|
|
2143
|
+
pendingFocus.current = true;
|
|
2144
|
+
changeDraft(text);
|
|
2145
|
+
const element = textareaRef.current;
|
|
2146
|
+
element?.focus();
|
|
2147
|
+
if (element?.value === text) element.setSelectionRange(text.length, text.length);
|
|
2148
|
+
},
|
|
2149
|
+
focus: () => textareaRef.current?.focus(),
|
|
2150
|
+
submit: (onSubmit, options) => send(onSubmit, options?.allowEmpty),
|
|
2151
|
+
reset: () => {
|
|
2152
|
+
generation.current += 1;
|
|
2153
|
+
pendingSend.current = false;
|
|
2154
|
+
pendingFocus.current = false;
|
|
2155
|
+
changeDraft("");
|
|
2156
|
+
updateAttachments(() => []);
|
|
2157
|
+
setSendError(void 0);
|
|
2158
|
+
setFailedSubmissions([]);
|
|
2159
|
+
setAttachmentError(void 0);
|
|
2160
|
+
setSending(false);
|
|
2161
|
+
fileInput.clearDragging();
|
|
2162
|
+
}
|
|
2163
|
+
}));
|
|
2164
|
+
useEffect5(() => {
|
|
2165
|
+
onDraftChange?.(draft);
|
|
2166
|
+
}, [draft, onDraftChange]);
|
|
2167
|
+
useEffect5(() => {
|
|
2168
|
+
if (!pendingFocus.current) {
|
|
2169
|
+
return;
|
|
2170
|
+
}
|
|
2171
|
+
pendingFocus.current = false;
|
|
2172
|
+
const element = textareaRef.current;
|
|
2173
|
+
if (element) {
|
|
2174
|
+
element.focus();
|
|
2175
|
+
element.setSelectionRange(element.value.length, element.value.length);
|
|
2176
|
+
}
|
|
2177
|
+
}, [draft, sending]);
|
|
2178
|
+
const applyAutoGrow = useCallback3(() => {
|
|
2179
|
+
const element = textareaRef.current;
|
|
2180
|
+
if (!element || inputMaxRows === void 0) {
|
|
2181
|
+
return;
|
|
2182
|
+
}
|
|
2183
|
+
const style = getComputedStyle(element);
|
|
2184
|
+
const lineHeight = Number.parseFloat(style.lineHeight) || Number.parseFloat(style.fontSize) * 1.4;
|
|
2185
|
+
const padding = Number.parseFloat(style.paddingTop) + Number.parseFloat(style.paddingBottom);
|
|
2186
|
+
const maximum = lineHeight * Math.max(inputMaxRows, inputRows) + padding;
|
|
2187
|
+
element.style.height = "auto";
|
|
2188
|
+
const contentHeight = element.scrollHeight;
|
|
2189
|
+
element.style.height = `${Math.min(contentHeight, maximum)}px`;
|
|
2190
|
+
element.style.overflowY = contentHeight > maximum ? "auto" : "hidden";
|
|
2191
|
+
}, [inputMaxRows, inputRows]);
|
|
2192
|
+
useEffect5(() => {
|
|
2193
|
+
applyAutoGrow();
|
|
2194
|
+
}, [draft, applyAutoGrow]);
|
|
2195
|
+
useEffect5(() => {
|
|
2196
|
+
const element = textareaRef.current;
|
|
2197
|
+
if (!element || inputMaxRows === void 0) {
|
|
2198
|
+
return;
|
|
2199
|
+
}
|
|
2200
|
+
let lastWidth = element.clientWidth;
|
|
2201
|
+
const observer = new ResizeObserver(() => {
|
|
2202
|
+
if (element.clientWidth !== lastWidth) {
|
|
2203
|
+
lastWidth = element.clientWidth;
|
|
2204
|
+
applyAutoGrow();
|
|
2205
|
+
}
|
|
2206
|
+
});
|
|
2207
|
+
observer.observe(element);
|
|
2208
|
+
return () => observer.disconnect();
|
|
2209
|
+
}, [inputMaxRows, applyAutoGrow]);
|
|
2210
|
+
useEffect5(() => {
|
|
2211
|
+
const element = rootRef.current;
|
|
2212
|
+
if (!element) {
|
|
2213
|
+
return;
|
|
2214
|
+
}
|
|
2215
|
+
const update = () => setCompact(element.clientWidth < COMPACT_WIDTH_PX);
|
|
2216
|
+
update();
|
|
2217
|
+
const observer = new ResizeObserver(update);
|
|
2218
|
+
observer.observe(element);
|
|
2219
|
+
return () => observer.disconnect();
|
|
2220
|
+
}, []);
|
|
2221
|
+
const hasAttachments = attachments.length > 0;
|
|
2222
|
+
useEffect5(() => {
|
|
2223
|
+
onAttachmentsChange?.(hasAttachments);
|
|
2224
|
+
}, [hasAttachments, onAttachmentsChange]);
|
|
2225
|
+
const capabilityError = hasAttachments && attachmentCapabilitiesError ? attachmentCapabilitiesError : attachmentCapabilities ? attachmentCapabilityError(attachments.map((entry) => ({
|
|
2226
|
+
name: entry.file.name,
|
|
2227
|
+
mediaType: attachmentMediaType(entry.file)
|
|
2228
|
+
})), attachmentCapabilities, allTexts) : void 0;
|
|
2229
|
+
const hasContent = draft.trim().length > 0 || attachments.length > 0;
|
|
2230
|
+
const preparing = attachments.some((entry) => entry.status === "reading");
|
|
2231
|
+
const attachmentBlocked = attachments.some((entry) => entry.status !== "ready");
|
|
2232
|
+
const currentError = sendError ?? attachmentError ?? capabilityError ?? attachments.find((entry) => entry.status === "error")?.error;
|
|
2233
|
+
useEffect5(() => {
|
|
2234
|
+
onErrorChange?.(currentError);
|
|
2235
|
+
}, [currentError, onErrorChange]);
|
|
2236
|
+
const send = async (onSubmit = onSend, allowEmpty = false) => {
|
|
2237
|
+
const text = draft.trim();
|
|
2238
|
+
const snapshot = attachmentsRef.current;
|
|
2239
|
+
if (!allowEmpty && !text && !snapshot.length || disabled || sendDisabled || pendingSend.current || capabilityError || snapshot.some((entry) => entry.status !== "ready")) {
|
|
2240
|
+
return;
|
|
2241
|
+
}
|
|
2242
|
+
pendingSend.current = true;
|
|
2243
|
+
const sentGeneration = generation.current;
|
|
2244
|
+
const sentRevision = draftRevision.current;
|
|
2245
|
+
const sentDraft = draft;
|
|
2246
|
+
const focusOrigin = document.activeElement;
|
|
2247
|
+
const startedFocused = focusOrigin === textareaRef.current || rootRef.current?.contains(focusOrigin);
|
|
2248
|
+
let leftComposer = false;
|
|
2249
|
+
const observeFocus = (event) => {
|
|
2250
|
+
const target = event.target;
|
|
2251
|
+
if (target instanceof Node && !rootRef.current?.contains(target) && !detailsContainer?.contains(target)) leftComposer = true;
|
|
2252
|
+
};
|
|
2253
|
+
document.addEventListener("focusin", observeFocus);
|
|
2254
|
+
document.addEventListener("pointerdown", observeFocus);
|
|
2255
|
+
setSending(true);
|
|
2256
|
+
setSendError(void 0);
|
|
2257
|
+
setDraft("");
|
|
2258
|
+
updateAttachments(() => []);
|
|
2259
|
+
try {
|
|
2260
|
+
const inputs = snapshot.flatMap((entry) => entry.status === "ready" ? [entry.input] : []);
|
|
2261
|
+
await onSubmit(text, inputs.length ? inputs : void 0);
|
|
2262
|
+
if (generation.current === sentGeneration && (text || inputs.length)) sendScope?.notify();
|
|
2263
|
+
} catch (error) {
|
|
2264
|
+
if (generation.current !== sentGeneration) return;
|
|
2265
|
+
if (draftRevision.current === sentRevision) {
|
|
2266
|
+
setDraft(sentDraft);
|
|
2267
|
+
updateAttachments((current) => [...snapshot, ...current]);
|
|
2268
|
+
} else if (sentDraft || snapshot.length) {
|
|
2269
|
+
const failed = { id: ++nextFailedId.current, draft: sentDraft, attachments: snapshot };
|
|
2270
|
+
setFailedSubmissions((current) => [...current, failed]);
|
|
2271
|
+
}
|
|
2272
|
+
setSendError(error instanceof Error ? error.message : String(error));
|
|
2273
|
+
} finally {
|
|
2274
|
+
document.removeEventListener("focusin", observeFocus);
|
|
2275
|
+
document.removeEventListener("pointerdown", observeFocus);
|
|
2276
|
+
if (generation.current === sentGeneration) {
|
|
2277
|
+
pendingSend.current = false;
|
|
2278
|
+
pendingFocus.current = !!startedFocused && !leftComposer && document.hasFocus() && (!toolbar || focusOrigin !== textareaRef.current) && (document.activeElement === focusOrigin || !toolbar && document.activeElement === document.body);
|
|
2279
|
+
setSending(false);
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
};
|
|
2283
|
+
const onKeyDown = (event) => {
|
|
2284
|
+
if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing && event.nativeEvent.keyCode !== 229 && (sendShortcut === "enter" || event.metaKey || event.ctrlKey)) {
|
|
2285
|
+
event.preventDefault();
|
|
2286
|
+
if (!event.repeat) void send();
|
|
2287
|
+
}
|
|
2288
|
+
};
|
|
2289
|
+
const attachmentPreviews = attachments.length > 0 && /* @__PURE__ */ jsx15("ul", { className: "qsl:flex qsl:max-h-[240px] qsl:flex-wrap qsl:gap-2 qsl:overflow-y-auto qsl:overscroll-contain qsl:px-3 qsl:pt-2.5 qsl:in-data-[tone=overseer-details]:max-h-40 qsl:in-data-[tone=overseer-details]:overflow-y-auto", "aria-label": allTexts.attachments, children: attachments.map((entry) => /* @__PURE__ */ jsxs8("li", { className: attachmentItemClasses, children: [
|
|
2290
|
+
entry.status === "ready" && entry.input.mediaType.startsWith("image/") && /* @__PURE__ */ jsx15("img", { className: attachmentMediaClasses, src: `data:${entry.input.mediaType};base64,${entry.input.data}`, alt: entry.file.name }),
|
|
2291
|
+
entry.status === "ready" && entry.input.mediaType.startsWith("video/") && /* @__PURE__ */ jsx15("video", { className: attachmentMediaClasses, controls: true, preload: "metadata", "aria-label": entry.file.name, src: `data:${entry.input.mediaType};base64,${entry.input.data}` }),
|
|
2292
|
+
/* @__PURE__ */ jsxs8("div", { className: "qsl:flex qsl:flex-col qsl:gap-1 qsl:p-2 qsl:pr-8 qsl:[overflow-wrap:anywhere]", children: [
|
|
2293
|
+
/* @__PURE__ */ jsx15("span", { className: "qsl:font-medium", title: entry.file.name, children: entry.file.name }),
|
|
2294
|
+
/* @__PURE__ */ jsx15("small", { children: formatAttachmentSize(entry.file.size) }),
|
|
2295
|
+
entry.status === "reading" && /* @__PURE__ */ jsx15("small", { role: "status", children: allTexts.attachmentPreparing }),
|
|
2296
|
+
entry.status === "error" && /* @__PURE__ */ jsx15("small", { role: "alert", children: entry.error })
|
|
2297
|
+
] }),
|
|
2298
|
+
/* @__PURE__ */ jsx15(
|
|
2299
|
+
Button2,
|
|
2300
|
+
{
|
|
2301
|
+
"aria-label": fillText(allTexts.removeAttachment, { name: entry.file.name }),
|
|
2302
|
+
className: "qsl:absolute qsl:top-1 qsl:right-1",
|
|
2303
|
+
disabled: disabled || sending,
|
|
2304
|
+
onClick: () => {
|
|
2305
|
+
updateAttachments((current) => current.filter((item) => item.id !== entry.id));
|
|
2306
|
+
setAttachmentError(void 0);
|
|
2307
|
+
textareaRef.current?.focus();
|
|
2308
|
+
},
|
|
2309
|
+
size: "icon-sm",
|
|
2310
|
+
title: fillText(allTexts.removeAttachment, { name: entry.file.name }),
|
|
2311
|
+
variant: "outline",
|
|
2312
|
+
children: /* @__PURE__ */ jsx15(XIcon3, {})
|
|
2313
|
+
}
|
|
2314
|
+
)
|
|
2315
|
+
] }, entry.id)) });
|
|
2316
|
+
const errors = /* @__PURE__ */ jsxs8(Fragment3, { children: [
|
|
2317
|
+
capabilityError && /* @__PURE__ */ jsx15("p", { className: errorClasses, role: "alert", children: capabilityError }),
|
|
2318
|
+
attachmentError && /* @__PURE__ */ jsx15("p", { className: errorClasses, role: "alert", children: attachmentError }),
|
|
2319
|
+
sendError && /* @__PURE__ */ jsx15("p", { className: errorClasses, role: "alert", children: sendError }),
|
|
2320
|
+
failedSubmissions.map((failed) => /* @__PURE__ */ jsxs8("div", { className: cn(errorClasses, "qsl:flex qsl:flex-wrap qsl:items-center qsl:gap-2"), role: "status", children: [
|
|
2321
|
+
/* @__PURE__ */ jsxs8("span", { children: [
|
|
2322
|
+
allTexts.notSent,
|
|
2323
|
+
" ",
|
|
2324
|
+
failed.draft.slice(0, 100),
|
|
2325
|
+
failed.draft.length > 100 ? " ..." : "",
|
|
2326
|
+
failed.attachments.length > 0 ? ` ${fillText(allTexts.notSentAttachments, { count: failed.attachments.length })}` : "",
|
|
2327
|
+
" "
|
|
2328
|
+
] }),
|
|
2329
|
+
/* @__PURE__ */ jsx15(Button2, { disabled: disabled || sending, size: "sm", variant: "outline", onClick: () => {
|
|
2330
|
+
try {
|
|
2331
|
+
validateAttachmentSelection(attachmentsRef.current.map((entry) => entry.file), failed.attachments.map((entry) => entry.file), allTexts);
|
|
2332
|
+
} catch (error) {
|
|
2333
|
+
setAttachmentError(error instanceof Error ? error.message : String(error));
|
|
2334
|
+
return;
|
|
2335
|
+
}
|
|
2336
|
+
draftRevision.current += 1;
|
|
2337
|
+
setDraft((current) => [current, failed.draft].filter(Boolean).join("\n\n"));
|
|
2338
|
+
updateAttachments((current) => [...current, ...failed.attachments]);
|
|
2339
|
+
setFailedSubmissions((current) => current.filter((entry) => entry.id !== failed.id));
|
|
2340
|
+
setAttachmentError(void 0);
|
|
2341
|
+
pendingFocus.current = true;
|
|
2342
|
+
}, children: allTexts.insertUnsent })
|
|
2343
|
+
] }, failed.id))
|
|
2344
|
+
] });
|
|
2345
|
+
const sendButtons = /* @__PURE__ */ jsxs8(Fragment3, { children: [
|
|
2346
|
+
running2 && onStop && !hasContent && /* @__PURE__ */ jsx15(StopButton2, { className: "qsl:flex-none", label: allTexts.stop, onClick: onStop, size: "icon-sm" }),
|
|
2347
|
+
(!running2 || !onStop || hasContent) && /* @__PURE__ */ jsx15(
|
|
2348
|
+
Button2,
|
|
2349
|
+
{
|
|
2350
|
+
"aria-label": allTexts.send,
|
|
2351
|
+
className: "qsl:flex-none",
|
|
2352
|
+
disabled: disabled || sendDisabled || sending || attachmentBlocked || !!capabilityError || !hasContent,
|
|
2353
|
+
onClick: () => {
|
|
2354
|
+
void send();
|
|
2355
|
+
},
|
|
2356
|
+
size: "icon-sm",
|
|
2357
|
+
title: running2 ? allTexts.sendIntoRun : allTexts.send,
|
|
2358
|
+
children: /* @__PURE__ */ jsx15(SendIcon, { size: 14 })
|
|
2359
|
+
}
|
|
2360
|
+
)
|
|
2361
|
+
] });
|
|
2362
|
+
const detailToolbar = /* @__PURE__ */ jsxs8("div", { className: toolbarRowClasses, "data-input": "controls", children: [
|
|
2363
|
+
/* @__PURE__ */ jsxs8("div", { className: cn(toolbarSideClasses, "qsl:in-data-[tone=overseer-details]:flex-none", inline || toolbar ? "qsl:flex-none" : "qsl:flex-auto"), children: [
|
|
2364
|
+
attachmentsEnabled && /* @__PURE__ */ jsx15(
|
|
2365
|
+
Button2,
|
|
2366
|
+
{
|
|
2367
|
+
"aria-label": allTexts.attachFiles,
|
|
2368
|
+
disabled: disabled || sending,
|
|
2369
|
+
onClick: () => fileInput.inputRef.current?.click(),
|
|
2370
|
+
size: "icon-sm",
|
|
2371
|
+
title: fillText(allTexts.attachFilesHint, { count: MAX_CHAT_ATTACHMENTS, size: maxAttachmentSizeLabel }),
|
|
2372
|
+
variant: "ghost",
|
|
2373
|
+
children: /* @__PURE__ */ jsx15(PaperclipIcon, {})
|
|
2374
|
+
}
|
|
2375
|
+
),
|
|
2376
|
+
preparing && /* @__PURE__ */ jsx15("span", { className: cn("qsl:text-xs qsl:text-muted-foreground", inline && "qsl:sr-only"), role: "status", children: allTexts.attachmentsPreparing }),
|
|
2377
|
+
actions?.map((action, index) => action.active === void 0 ? /* @__PURE__ */ jsxs8(Button2, { disabled: action.disabled, onClick: action.onClick, size: "sm", title: action.title ?? action.label, variant: "ghost", children: [
|
|
2378
|
+
action.icon,
|
|
2379
|
+
action.label && /* @__PURE__ */ jsx15("span", { className: action.icon ? "qsl:in-data-[compact=true]:hidden" : void 0, children: action.label })
|
|
2380
|
+
] }, index) : /* @__PURE__ */ jsxs8(Toggle2, { disabled: action.disabled, onPressedChange: () => action.onClick(), pressed: action.active, size: "sm", title: action.title ?? action.label, children: [
|
|
2381
|
+
action.icon,
|
|
2382
|
+
action.label && /* @__PURE__ */ jsx15("span", { className: action.icon ? "qsl:in-data-[compact=true]:hidden" : void 0, children: action.label })
|
|
2383
|
+
] }, index)),
|
|
2384
|
+
toolbarLeft
|
|
2385
|
+
] }),
|
|
2386
|
+
/* @__PURE__ */ jsxs8("div", { className: cn(toolbarSideClasses, "qsl:in-data-[tone=overseer-details]:flex-1", inline || toolbar ? "qsl:flex-none" : "qsl:flex-[0_1_auto]"), children: [
|
|
2387
|
+
toolbarRight,
|
|
2388
|
+
!toolbar && sendButtons
|
|
2389
|
+
] })
|
|
2390
|
+
] });
|
|
2391
|
+
const details = /* @__PURE__ */ jsxs8("div", { className: "qsl:min-w-0", "data-compact": compact, "data-quassel": "", children: [
|
|
2392
|
+
attachmentPreviews,
|
|
2393
|
+
errors,
|
|
2394
|
+
detailToolbar
|
|
2395
|
+
] });
|
|
2396
|
+
const input = /* @__PURE__ */ jsx15(
|
|
2397
|
+
"textarea",
|
|
2398
|
+
{
|
|
2399
|
+
"aria-controls": inputAriaControls,
|
|
2400
|
+
className: textareaClasses,
|
|
2401
|
+
"aria-label": allTexts.placeholder,
|
|
2402
|
+
disabled: !toolbar && disabled,
|
|
2403
|
+
readOnly: toolbar && disabled,
|
|
2404
|
+
onChange: (event) => changeDraft(event.target.value),
|
|
2405
|
+
onKeyDown,
|
|
2406
|
+
placeholder: running2 ? allTexts.steeringPlaceholder : allTexts.placeholder,
|
|
2407
|
+
ref: textareaRef,
|
|
2408
|
+
rows: inputRows,
|
|
2409
|
+
value: draft
|
|
2410
|
+
}
|
|
2411
|
+
);
|
|
2412
|
+
return /* @__PURE__ */ jsxs8(
|
|
2413
|
+
"div",
|
|
2414
|
+
{
|
|
2415
|
+
className: rootClasses,
|
|
2416
|
+
"data-compact": compact,
|
|
2417
|
+
"data-layout": layout,
|
|
2418
|
+
"data-quassel": "",
|
|
2419
|
+
"data-working": running2 || sending,
|
|
2420
|
+
ref: rootRef,
|
|
2421
|
+
...attachmentsEnabled ? fileInput.dropProps : {},
|
|
2422
|
+
children: [
|
|
2423
|
+
attachmentsEnabled && /* @__PURE__ */ jsx15(
|
|
2424
|
+
"input",
|
|
2425
|
+
{
|
|
2426
|
+
"aria-label": allTexts.attachFiles,
|
|
2427
|
+
hidden: true,
|
|
2428
|
+
multiple: true,
|
|
2429
|
+
type: "file",
|
|
2430
|
+
...fileInput.inputProps
|
|
2431
|
+
}
|
|
2432
|
+
),
|
|
2433
|
+
!toolbar && attachmentPreviews,
|
|
2434
|
+
inline ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
|
|
2435
|
+
errors,
|
|
2436
|
+
/* @__PURE__ */ jsxs8("div", { className: "qsl:flex qsl:min-w-0 qsl:items-end", children: [
|
|
2437
|
+
input,
|
|
2438
|
+
detailToolbar
|
|
2439
|
+
] })
|
|
2440
|
+
] }) : input,
|
|
2441
|
+
toolbar ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
|
|
2442
|
+
/* @__PURE__ */ jsx15("div", { className: "qsl:flex qsl:flex-none qsl:items-center qsl:gap-1 qsl:p-1", children: sendButtons }),
|
|
2443
|
+
detailsContainer && createPortal2(details, detailsContainer)
|
|
2444
|
+
] }) : !inline && /* @__PURE__ */ jsxs8(Fragment3, { children: [
|
|
2445
|
+
errors,
|
|
2446
|
+
detailToolbar
|
|
2447
|
+
] })
|
|
2448
|
+
]
|
|
2449
|
+
}
|
|
2450
|
+
);
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
// packages/quassel/src/DetailModeSwitch.tsx
|
|
2454
|
+
import { CircleDotIcon, EyeOffIcon, ListCollapseIcon, ListIcon, ShapesIcon, TagsIcon, TextAlignJustifyIcon } from "lucide-react";
|
|
2455
|
+
import { jsx as jsx16 } from "react/jsx-runtime";
|
|
2456
|
+
var DETAIL_MODES = ["off", "current", "icons", "chips", "grouped", "compact", "full"];
|
|
2457
|
+
var TEXT_KEYS = {
|
|
2458
|
+
off: "detailModeOff",
|
|
2459
|
+
current: "detailModeCurrent",
|
|
2460
|
+
icons: "detailModeIcons",
|
|
2461
|
+
chips: "detailModeChips",
|
|
2462
|
+
grouped: "detailModeGrouped",
|
|
2463
|
+
compact: "detailModeCompact",
|
|
2464
|
+
full: "detailModeFull"
|
|
2465
|
+
};
|
|
2466
|
+
var ICONS = {
|
|
2467
|
+
off: EyeOffIcon,
|
|
2468
|
+
current: CircleDotIcon,
|
|
2469
|
+
icons: ShapesIcon,
|
|
2470
|
+
chips: TagsIcon,
|
|
2471
|
+
grouped: ListCollapseIcon,
|
|
2472
|
+
compact: ListIcon,
|
|
2473
|
+
full: TextAlignJustifyIcon
|
|
2474
|
+
};
|
|
2475
|
+
var detailModeLabel = (mode, texts) => ({ ...defaultTexts, ...texts })[TEXT_KEYS[mode]];
|
|
2476
|
+
function DetailModeSwitch({
|
|
2477
|
+
mode,
|
|
2478
|
+
onChange,
|
|
2479
|
+
modes = DETAIL_MODES,
|
|
2480
|
+
texts,
|
|
2481
|
+
className
|
|
2482
|
+
}) {
|
|
2483
|
+
const allTexts = { ...defaultTexts, ...texts };
|
|
2484
|
+
const { Button: Button2 } = useQuasselComponents();
|
|
2485
|
+
const current = modes.indexOf(mode);
|
|
2486
|
+
const next2 = modes[(current < 0 ? 0 : current + 1) % modes.length];
|
|
2487
|
+
const Icon = ICONS[mode];
|
|
2488
|
+
const title = `${allTexts.detailModeTitle}: ${detailModeLabel(mode, texts)}`;
|
|
2489
|
+
return /* @__PURE__ */ jsx16(Button2, { "aria-label": title, className, onClick: () => onChange(next2), size: "icon-sm", title, variant: "ghost", children: /* @__PURE__ */ jsx16(Icon, {}) });
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
// packages/quassel/src/TranscriptModeSwitch.tsx
|
|
2493
|
+
import { MessagesSquareIcon, MessageSquareIcon } from "lucide-react";
|
|
2494
|
+
import { jsx as jsx17 } from "react/jsx-runtime";
|
|
2495
|
+
function TranscriptModeSwitch({ mode, onChange, texts, className }) {
|
|
2496
|
+
const labels = { ...defaultTexts, ...texts };
|
|
2497
|
+
const { Button: Button2 } = useQuasselComponents();
|
|
2498
|
+
const latest = mode === "latest";
|
|
2499
|
+
const title = latest ? labels.showAllMessages : labels.showLatestExchange;
|
|
2500
|
+
return /* @__PURE__ */ jsx17(
|
|
2501
|
+
Button2,
|
|
2502
|
+
{
|
|
2503
|
+
"aria-label": title,
|
|
2504
|
+
"aria-pressed": latest,
|
|
2505
|
+
className,
|
|
2506
|
+
onClick: () => onChange(latest ? "all" : "latest"),
|
|
2507
|
+
size: "icon-sm",
|
|
2508
|
+
title,
|
|
2509
|
+
variant: "ghost",
|
|
2510
|
+
children: latest ? /* @__PURE__ */ jsx17(MessageSquareIcon, {}) : /* @__PURE__ */ jsx17(MessagesSquareIcon, {})
|
|
2511
|
+
}
|
|
2512
|
+
);
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
// packages/quassel/src/TimestampSwitch.tsx
|
|
2516
|
+
import { ClockFadingIcon, ClockIcon } from "lucide-react";
|
|
2517
|
+
import { jsx as jsx18 } from "react/jsx-runtime";
|
|
2518
|
+
function TimestampSwitch({ showTimestamps, onChange, texts, className }) {
|
|
2519
|
+
const labels = { ...defaultTexts, ...texts };
|
|
2520
|
+
const { Button: Button2 } = useQuasselComponents();
|
|
2521
|
+
const title = showTimestamps ? labels.hideTimestamps : labels.showTimestamps;
|
|
2522
|
+
return /* @__PURE__ */ jsx18(
|
|
2523
|
+
Button2,
|
|
2524
|
+
{
|
|
2525
|
+
"aria-label": title,
|
|
2526
|
+
"aria-pressed": showTimestamps,
|
|
2527
|
+
className,
|
|
2528
|
+
onClick: () => onChange(!showTimestamps),
|
|
2529
|
+
size: "icon-sm",
|
|
2530
|
+
title,
|
|
2531
|
+
variant: "ghost",
|
|
2532
|
+
children: showTimestamps ? /* @__PURE__ */ jsx18(ClockIcon, {}) : /* @__PURE__ */ jsx18(ClockFadingIcon, {})
|
|
2533
|
+
}
|
|
2534
|
+
);
|
|
2535
|
+
}
|
|
2536
|
+
export {
|
|
2537
|
+
ChatInputToolbar,
|
|
2538
|
+
ChatMessages,
|
|
2539
|
+
ChatPanel,
|
|
2540
|
+
ChatSendContext,
|
|
2541
|
+
DETAIL_MODES,
|
|
2542
|
+
DetailModeSwitch,
|
|
2543
|
+
MAX_CHAT_ATTACHMENTS,
|
|
2544
|
+
MAX_CHAT_ATTACHMENT_BYTES,
|
|
2545
|
+
Markdown,
|
|
2546
|
+
MarkdownCodeBlocks,
|
|
2547
|
+
MarkdownLinks,
|
|
2548
|
+
MessageActions,
|
|
2549
|
+
PendingActionCard,
|
|
2550
|
+
QuasselProvider,
|
|
2551
|
+
StepPopover,
|
|
2552
|
+
TimestampSwitch,
|
|
2553
|
+
TranscriptModeSwitch,
|
|
2554
|
+
WorkingScenes,
|
|
2555
|
+
announce,
|
|
2556
|
+
appearanceStyle,
|
|
2557
|
+
applyEvent,
|
|
2558
|
+
attachmentCapabilityError,
|
|
2559
|
+
attachmentDownloadUrl,
|
|
2560
|
+
attachmentMediaType,
|
|
2561
|
+
compactToolLine,
|
|
2562
|
+
copyChatText,
|
|
2563
|
+
createChatScroll,
|
|
2564
|
+
createChatSendScope,
|
|
2565
|
+
defaultComponents,
|
|
2566
|
+
defaultTexts,
|
|
2567
|
+
detailModeLabel,
|
|
2568
|
+
encodeAttachment,
|
|
2569
|
+
englishTexts,
|
|
2570
|
+
fillText,
|
|
2571
|
+
formatAttachmentSize,
|
|
2572
|
+
formatElapsed,
|
|
2573
|
+
germanTexts,
|
|
2574
|
+
markdownPlainText,
|
|
2575
|
+
messageDate,
|
|
2576
|
+
prettyJson,
|
|
2577
|
+
stepState,
|
|
2578
|
+
timestampDay,
|
|
2579
|
+
timestampLabel,
|
|
2580
|
+
useChatAction,
|
|
2581
|
+
useElapsed,
|
|
2582
|
+
useQuasselComponents,
|
|
2583
|
+
useQuasselPortalContainer,
|
|
2584
|
+
validateAttachmentSelection
|
|
2585
|
+
};
|
|
2586
|
+
//# sourceMappingURL=index.js.map
|