quassel 0.1.1 → 0.2.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.
Files changed (59) hide show
  1. package/ChatInputToolbar.d.ts +35 -6
  2. package/ChatMessages.d.ts +19 -15
  3. package/ChatPanel.d.ts +4 -2
  4. package/ChatSendContext.d.ts +7 -12
  5. package/DetailModeSwitch.d.ts +1 -1
  6. package/Markdown.d.ts +4 -11
  7. package/MessageActions.d.ts +1 -10
  8. package/PendingActionCard.d.ts +13 -0
  9. package/QuasselProvider.d.ts +76 -0
  10. package/README.md +39 -38
  11. package/StepPopover.d.ts +1 -1
  12. package/TimestampSwitch.d.ts +2 -2
  13. package/WorkingScenes.d.ts +3 -1
  14. package/attachments.d.ts +24 -0
  15. package/chat-attachments.d.ts +6 -0
  16. package/chat-scroll.d.ts +12 -0
  17. package/chat.css +2051 -0
  18. package/chunk-IGCFQJZB.js +183 -0
  19. package/chunk-IGCFQJZB.js.map +7 -0
  20. package/events.d.ts +74 -17
  21. package/events.js +11 -1
  22. package/index.d.ts +17 -10
  23. package/index.js +1615 -988
  24. package/index.js.map +4 -4
  25. package/options.d.ts +1 -1
  26. package/package.json +12 -10
  27. package/texts.d.ts +38 -11
  28. package/timestamps.d.ts +6 -4
  29. package/types.d.ts +6 -6
  30. package/ui/button.d.ts +8 -0
  31. package/ui/card.d.ts +5 -0
  32. package/ui/cn.d.ts +2 -0
  33. package/ui/popover.d.ts +4 -0
  34. package/ui/spinner.d.ts +2 -0
  35. package/ui/stop-button.d.ts +10 -0
  36. package/ui/toggle.d.ts +8 -0
  37. package/ui/useFileInput.d.ts +20 -0
  38. package/useChatAction.d.ts +6 -0
  39. package/QuestionCard.d.ts +0 -12
  40. package/chunk-DOLTANTM.js +0 -84
  41. package/chunk-DOLTANTM.js.map +0 -7
  42. package/css/chat.css +0 -927
  43. package/css/foundation/base.css +0 -24
  44. package/css/foundation/index.css +0 -3
  45. package/css/foundation/panel.css +0 -111
  46. package/css/foundation/tokens.css +0 -68
  47. package/css/foundation/wash.css +0 -26
  48. package/css/question.css +0 -149
  49. package/icons.d.ts +0 -15
  50. package/server/http.d.ts +0 -17
  51. package/server/index.d.ts +0 -6
  52. package/server/index.js +0 -478
  53. package/server/index.js.map +0 -7
  54. package/server/manager.d.ts +0 -12
  55. package/server/openai.d.ts +0 -16
  56. package/server/session.d.ts +0 -27
  57. package/server/store.d.ts +0 -18
  58. package/server/types.d.ts +0 -62
  59. package/useChat.d.ts +0 -14
package/index.js CHANGED
@@ -1,8 +1,11 @@
1
1
  import {
2
+ MAX_CHAT_ATTACHMENTS,
3
+ MAX_CHAT_ATTACHMENT_BYTES,
2
4
  applyEvent,
5
+ chatAttachmentMediaType,
3
6
  compactToolLine,
4
7
  prettyJson
5
- } from "./chunk-DOLTANTM.js";
8
+ } from "./chunk-IGCFQJZB.js";
6
9
 
7
10
  // packages/quassel/src/types.ts
8
11
  function stepState(message) {
@@ -16,11 +19,14 @@ function stepState(message) {
16
19
  }
17
20
 
18
21
  // packages/quassel/src/texts.ts
22
+ var fillText = (text, values) => text.replace(/\{(\w+)\}/g, (match, name) => name in values ? String(values[name]) : match);
19
23
  var defaultTexts = {
20
24
  working: "Arbeitet ...",
21
25
  toolRunning: "l\xE4uft ...",
22
26
  toolStillRunning: "Werkzeug l\xE4uft ...",
27
+ currentTool: "Werkzeug l\xE4uft",
23
28
  thinkingChip: "Denken",
29
+ toolChip: "Werkzeug",
24
30
  stepGroupOne: "1 Schritt",
25
31
  stepGroupMany: "{count} Schritte",
26
32
  stepGroupCollapse: "Einklappen",
@@ -31,421 +37,456 @@ var defaultTexts = {
31
37
  close: "Schlie\xDFen",
32
38
  jumpToEnd: "Zum Ende springen",
33
39
  send: "Senden",
34
- sendIntoRun: "In den laufenden Lauf schicken",
35
- stop: "Lauf abbrechen",
40
+ sendIntoRun: "In den laufenden Turn einspeisen",
41
+ stop: "Arbeit stoppen",
36
42
  placeholder: "Nachricht schreiben ...",
37
43
  steeringPlaceholder: "Dazwischenfunken ...",
38
- timestamps: "Zeitstempel",
39
- messageActions: "Nachrichtenaktionen",
40
- questionSubmit: "Auswahl \xFCbernehmen",
41
- questionPlaceholder: "... oder frei antworten",
42
- questionAnswer: "Antworten",
43
- copyMessage: "Nachricht kopieren",
44
- copyCode: "Code kopieren",
45
- copied: "Kopiert",
46
- copyFailed: "Kopieren ist nicht verf\xFCgbar.",
47
- editMessage: "Nachricht bearbeiten",
48
- retryMessage: "Erneut senden",
44
+ steered: "In den laufenden Turn eingespeist",
45
+ inputHint: "Enter zum Senden - Shift + Enter f\xFCr eine neue Zeile",
49
46
  detailModeTitle: "Detailgrad der Schritte",
50
47
  detailModeOff: "nur Antworten",
48
+ detailModeCurrent: "aktuell",
51
49
  detailModeIcons: "Symbole",
52
50
  detailModeChips: "kompakt",
53
51
  detailModeGrouped: "gruppiert",
54
52
  detailModeCompact: "einzeilig",
55
- detailModeFull: "alles"
53
+ detailModeFull: "alles",
54
+ timestamps: "Zeitstempel",
55
+ showTimestamps: "Zeitstempel anzeigen",
56
+ hideTimestamps: "Zeitstempel ausblenden",
57
+ copyMessage: "Nachricht kopieren",
58
+ editMessage: "Nachricht bearbeiten",
59
+ retryMessage: "Antwort erneut anfordern",
60
+ copyCode: "Code kopieren",
61
+ copied: "Kopiert",
62
+ pendingAction: "wartet auf Eingabe",
63
+ dismissAction: "Verwerfen",
64
+ actionDismissed: "verworfen",
65
+ actionFailed: "Aktion fehlgeschlagen",
66
+ clipboardUnavailable: "Die Zwischenablage ist nicht verf\xFCgbar",
67
+ transcript: "Chatverlauf",
68
+ attachments: "Anh\xE4nge",
69
+ attachFiles: "Dateien anh\xE4ngen",
70
+ attachFilesHint: "Dateien anh\xE4ngen (bis zu {count} Dateien, insgesamt {size})",
71
+ removeAttachment: "{name} entfernen",
72
+ attachmentPreparing: "Wird vorbereitet ...",
73
+ attachmentsPreparing: "Anh\xE4nge werden vorbereitet ...",
74
+ attachmentReadFailed: "Datei konnte nicht gelesen werden. Entferne sie und f\xFCge sie erneut hinzu.",
75
+ attachmentsWhileBusy: "Warte, bis die Eingabe wieder bereit ist, und f\xFCge die Dateien dann erneut hinzu.",
76
+ tooManyAttachments: "H\xF6chstens {count} Anh\xE4nge pro Nachricht. Entferne zuerst einen Anhang.",
77
+ attachmentsTooLarge: "Anh\xE4nge d\xFCrfen zusammen h\xF6chstens {size} gro\xDF sein. W\xE4hle kleinere Dateien.",
78
+ attachmentUnsupported: "Dieser Chat unterst\xFCtzt {name} nicht als {kind}. Entferne den Anhang.",
79
+ attachmentUnsupportedByModel: "Das Modell {model} unterst\xFCtzt {name} nicht als {kind}. W\xE4hle ein passendes Modell oder entferne den Anhang.",
80
+ attachmentKindImage: "Bild",
81
+ attachmentKindVideo: "Video",
82
+ attachmentKindPdf: "PDF",
83
+ notSent: "Nicht gesendet:",
84
+ notSentAttachments: "({count} Anh\xE4nge)",
85
+ insertUnsent: "Nicht gesendete Eingabe einf\xFCgen"
56
86
  };
57
87
 
58
- // packages/quassel/src/useChat.ts
59
- import { useCallback, useEffect, useReducer, useRef, useState } from "react";
60
- function useChat(baseUrl, headers = {}, onEvent) {
61
- const [messages, dispatch] = useReducer(applyEvent, []);
62
- const [running, setRunning] = useState(false);
63
- const [connected, setConnected] = useState(false);
64
- const url = useRef(baseUrl);
65
- url.current = baseUrl;
66
- const zusatz = useRef(headers);
67
- zusatz.current = headers;
68
- const observer = useRef(onEvent);
69
- observer.current = onEvent;
70
- useEffect(() => {
71
- const controller = new AbortController();
72
- let active = true;
73
- async function verbinden() {
74
- while (active) {
75
- try {
76
- const response = await fetch(`${url.current}/stream`, {
77
- headers: { Accept: "text/event-stream", ...zusatz.current },
78
- signal: controller.signal
79
- });
80
- if (!response.ok || !response.body) {
81
- throw new Error(`Stream fehlgeschlagen: ${response.status}`);
82
- }
83
- setConnected(true);
84
- const reader = response.body.getReader();
85
- const decoder = new TextDecoder();
86
- let puffer = "";
87
- for (; ; ) {
88
- const { done, value } = await reader.read();
89
- if (done) {
90
- break;
91
- }
92
- puffer += decoder.decode(value, { stream: true });
93
- const teile = puffer.split("\n\n");
94
- puffer = teile.pop() ?? "";
95
- for (const teil of teile) {
96
- const daten = teil.split("\n").filter((zeile) => zeile.startsWith("data:")).map((zeile) => zeile.slice(5).trim()).join("");
97
- if (!daten) {
98
- continue;
99
- }
100
- const event = JSON.parse(daten);
101
- if (event.kind === "status") {
102
- setRunning(event.running);
103
- }
104
- observer.current?.(event);
105
- dispatch(event);
106
- }
107
- }
108
- } catch {
109
- if (!active) {
110
- return;
111
- }
112
- }
113
- setConnected(false);
114
- await new Promise((resolve) => setTimeout(resolve, 3e3));
115
- }
116
- }
117
- void verbinden();
118
- return () => {
119
- active = false;
120
- controller.abort();
121
- };
122
- }, []);
123
- const send = useCallback(async (text) => {
124
- await fetch(`${url.current}/send`, {
125
- method: "POST",
126
- headers: { "Content-Type": "application/json", ...zusatz.current },
127
- body: JSON.stringify({ text })
128
- });
129
- }, []);
130
- const stop = useCallback(async () => {
131
- await fetch(`${url.current}/stop`, { method: "POST", headers: { ...zusatz.current } });
132
- }, []);
133
- return { messages, running, connected, send, stop };
134
- }
88
+ // packages/quassel/src/ui/cn.ts
89
+ import { createCn } from "cn/config";
90
+ var cn = createCn({ prefix: "qsl" });
135
91
 
136
92
  // packages/quassel/src/ChatMessages.tsx
137
- import { useCallback as useCallback2, useContext as useContext3, useEffect as useEffect5, useLayoutEffect as useLayoutEffect3, useMemo, useRef as useRef5, useState as useState7 } from "react";
93
+ import { useCallback as useCallback2, useContext as useContext3, useEffect as useEffect3, useLayoutEffect, useMemo as useMemo3, useRef as useRef3, useState as useState4 } from "react";
138
94
 
139
- // packages/quassel/src/options.ts
140
- var length = (value) => typeof value === "number" ? `${value}px` : value;
141
- function appearanceStyle(appearance) {
95
+ // packages/quassel/src/chat-scroll.ts
96
+ var atEnd = (viewport) => viewport.scrollHeight - viewport.clientHeight - viewport.scrollTop <= 24;
97
+ function createChatScroll(onFollowChange = () => {
98
+ }) {
99
+ let following = true;
100
+ let lastTop = 0;
101
+ const follow = (value) => {
102
+ if (following === value) return;
103
+ following = value;
104
+ onFollowChange(value);
105
+ };
106
+ const layout = (viewport, dragging = false) => {
107
+ if (viewport.clientHeight <= 0) return;
108
+ if (dragging && viewport.scrollTop < lastTop && !atEnd(viewport)) follow(false);
109
+ if (following) viewport.scrollTop = Math.max(0, viewport.scrollHeight - viewport.clientHeight);
110
+ else if (viewport.scrollHeight <= viewport.clientHeight) follow(true);
111
+ lastTop = viewport.scrollTop;
112
+ };
142
113
  return {
143
- "--qsl-chat-font-size": length(appearance?.fontSize),
144
- "--qsl-chat-line-height": appearance?.lineHeight,
145
- "--qsl-message-gap": length(appearance?.messageGap),
146
- "--qsl-dense-message-gap": length(appearance?.denseMessageGap)
114
+ layout,
115
+ pause(viewport) {
116
+ if (viewport.scrollTop > 0) follow(false);
117
+ },
118
+ scroll(viewport, dragging = false) {
119
+ if (viewport.clientHeight <= 0) return;
120
+ if (atEnd(viewport)) follow(true);
121
+ else if (dragging && viewport.scrollTop < lastTop) follow(false);
122
+ lastTop = viewport.scrollTop;
123
+ },
124
+ jump(viewport) {
125
+ follow(true);
126
+ layout(viewport);
127
+ }
147
128
  };
148
129
  }
149
130
 
150
- // packages/quassel/src/timestamps.ts
151
- function timestampDate(at) {
152
- if (!at) return void 0;
153
- const date = new Date(at);
154
- return Number.isNaN(date.getTime()) ? void 0 : date;
155
- }
156
- function formatTimestamp(date, options = {}, now = Date.now()) {
157
- if (options.format === "relative") {
158
- const seconds = (date.getTime() - now) / 1e3;
159
- const [unit, divisor] = Math.abs(seconds) < 60 ? ["second", 1] : Math.abs(seconds) < 3600 ? ["minute", 60] : Math.abs(seconds) < 86400 ? ["hour", 3600] : ["day", 86400];
160
- return new Intl.RelativeTimeFormat(options.locale ?? "de", { numeric: "auto" }).format(Math.round(seconds / divisor), unit);
161
- }
162
- if (options.format === "date-time") {
163
- return new Intl.DateTimeFormat(options.locale ?? "de", {
164
- dateStyle: "short",
165
- timeStyle: "short",
166
- timeZone: options.timeZone
167
- }).format(date);
168
- }
169
- if (!options.locale && !options.timeZone) {
170
- return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
131
+ // packages/quassel/src/Markdown.tsx
132
+ import { createContext as createContext2, useCallback, useContext as useContext2, useRef as useRef2 } from "react";
133
+ import { Streamdown } from "streamdown";
134
+ import { CheckIcon, CopyIcon } from "lucide-react";
135
+
136
+ // packages/quassel/src/QuasselProvider.tsx
137
+ import { createContext, useContext, useMemo } from "react";
138
+
139
+ // packages/quassel/src/ui/button.tsx
140
+ import { Button as ButtonPrimitive } from "@base-ui/react/button";
141
+ import { cva } from "class-variance-authority";
142
+ import { jsx } from "react/jsx-runtime";
143
+ var buttonVariants = cva(
144
+ "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",
145
+ {
146
+ variants: {
147
+ variant: {
148
+ default: "qsl:bg-primary qsl:text-primary-foreground qsl:hover:bg-primary/80",
149
+ 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",
150
+ 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",
151
+ 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",
152
+ 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",
153
+ link: "qsl:text-primary qsl:underline-offset-4 qsl:hover:underline"
154
+ },
155
+ size: {
156
+ 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",
157
+ xs: "qsl:h-6 qsl:gap-1 qsl:rounded-[min(var(--qsl-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",
158
+ sm: "qsl:h-7 qsl:gap-1 qsl:rounded-[min(var(--qsl-radius-md),12px)] qsl:px-2.5 qsl:text-[0.8rem] 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",
159
+ 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",
160
+ icon: "qsl:size-8",
161
+ "icon-xs": "qsl:size-6 qsl:rounded-[min(var(--qsl-radius-md),10px)] qsl:in-data-[slot=button-group]:rounded-lg qsl:[&_svg:not([class*='size-'])]:size-3",
162
+ "icon-sm": "qsl:size-7 qsl:rounded-[min(var(--qsl-radius-md),12px)] qsl:in-data-[slot=button-group]:rounded-lg",
163
+ "icon-lg": "qsl:size-9"
164
+ }
165
+ },
166
+ defaultVariants: {
167
+ variant: "default",
168
+ size: "default"
169
+ }
171
170
  }
172
- return new Intl.DateTimeFormat(options.locale ?? "de", {
173
- hour: "2-digit",
174
- minute: "2-digit",
175
- hourCycle: "h23",
176
- timeZone: options.timeZone
177
- }).format(date);
171
+ );
172
+ function Button({
173
+ className,
174
+ variant = "default",
175
+ size = "default",
176
+ ...props
177
+ }) {
178
+ return /* @__PURE__ */ jsx(
179
+ ButtonPrimitive,
180
+ {
181
+ "data-quassel": "",
182
+ "data-slot": "button",
183
+ className: cn(buttonVariants({ variant, size, className })),
184
+ ...props
185
+ }
186
+ );
178
187
  }
179
- function timestampDay(date, timeZone) {
180
- return new Intl.DateTimeFormat("en-CA", {
181
- year: "numeric",
182
- month: "2-digit",
183
- day: "2-digit",
184
- calendar: "gregory",
185
- numberingSystem: "latn",
186
- timeZone
187
- }).format(date);
188
+
189
+ // packages/quassel/src/ui/card.tsx
190
+ import { jsx as jsx2 } from "react/jsx-runtime";
191
+ function Card({
192
+ className,
193
+ size = "default",
194
+ ...props
195
+ }) {
196
+ return /* @__PURE__ */ jsx2(
197
+ "div",
198
+ {
199
+ "data-quassel": "",
200
+ "data-slot": "card",
201
+ "data-size": size,
202
+ className: cn(
203
+ "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",
204
+ className
205
+ ),
206
+ ...props
207
+ }
208
+ );
188
209
  }
189
- function formatDay(date, options) {
190
- return new Intl.DateTimeFormat(options?.locale ?? "de", {
191
- dateStyle: "long",
192
- timeZone: options?.timeZone
193
- }).format(date);
210
+
211
+ // packages/quassel/src/ui/popover.tsx
212
+ import { Popover as PopoverPrimitive } from "@base-ui/react/popover";
213
+ import { jsx as jsx3 } from "react/jsx-runtime";
214
+ function Popover({ ...props }) {
215
+ return /* @__PURE__ */ jsx3(PopoverPrimitive.Root, { "data-slot": "popover", ...props });
216
+ }
217
+ function PopoverContent({
218
+ className,
219
+ align = "center",
220
+ alignOffset = 0,
221
+ side = "bottom",
222
+ sideOffset = 8,
223
+ anchor,
224
+ collisionPadding,
225
+ keepMounted,
226
+ ...props
227
+ }) {
228
+ return /* @__PURE__ */ jsx3(PopoverPrimitive.Portal, { keepMounted, children: /* @__PURE__ */ jsx3(
229
+ PopoverPrimitive.Positioner,
230
+ {
231
+ align,
232
+ alignOffset,
233
+ anchor,
234
+ collisionPadding,
235
+ side,
236
+ sideOffset,
237
+ className: "qsl:isolate qsl:z-[110]",
238
+ children: /* @__PURE__ */ jsx3(
239
+ PopoverPrimitive.Popup,
240
+ {
241
+ "data-quassel": "",
242
+ "data-slot": "popover-content",
243
+ className: cn(
244
+ "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",
245
+ className
246
+ ),
247
+ ...props
248
+ }
249
+ )
250
+ }
251
+ ) });
194
252
  }
195
253
 
196
- // packages/quassel/src/MessageActions.tsx
197
- import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
198
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
199
- function ActionButton({ label, icon, disabled, onClick, successLabel }) {
200
- const pending = useRef2(false);
201
- const [status, setStatus] = useState2("idle");
202
- const [error, setError] = useState2();
203
- useEffect2(() => {
254
+ // packages/quassel/src/ui/stop-button.tsx
255
+ import { SquareIcon } from "lucide-react";
256
+
257
+ // packages/quassel/src/ui/spinner.tsx
258
+ import { Loader2Icon } from "lucide-react";
259
+ import { jsx as jsx4 } from "react/jsx-runtime";
260
+ function Spinner({ className, ...props }) {
261
+ return /* @__PURE__ */ jsx4(Loader2Icon, { "data-slot": "spinner", role: "status", "aria-label": "Loading", className: cn("qsl:size-4 qsl:animate-spin", className), ...props });
262
+ }
263
+
264
+ // packages/quassel/src/ui/stop-button.tsx
265
+ import { jsx as jsx5 } from "react/jsx-runtime";
266
+ function StopGlyph({ className, ...props }) {
267
+ return /* @__PURE__ */ jsx5(SquareIcon, { "aria-hidden": true, className: cn("qsl:text-destructive", className), fill: "currentColor", ...props });
268
+ }
269
+ var stopButtonClass = "qsl:text-destructive qsl:hover:bg-destructive/10 qsl:hover:text-destructive qsl:dark:hover:bg-destructive/20";
270
+ function StopButton({ label, busy = false, className, title, ...props }) {
271
+ 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, {}) });
272
+ }
273
+
274
+ // packages/quassel/src/ui/toggle.tsx
275
+ import { Toggle as TogglePrimitive } from "@base-ui/react/toggle";
276
+ import { cva as cva2 } from "class-variance-authority";
277
+ import { jsx as jsx6 } from "react/jsx-runtime";
278
+ var toggleVariants = cva2(
279
+ "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",
280
+ {
281
+ variants: {
282
+ variant: {
283
+ default: "qsl:bg-transparent",
284
+ outline: "qsl:border qsl:border-input qsl:bg-transparent qsl:hover:bg-muted"
285
+ },
286
+ size: {
287
+ 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",
288
+ sm: "qsl:h-7 qsl:min-w-7 qsl:rounded-[min(var(--qsl-radius-md),12px)] qsl:px-2.5 qsl:text-[0.8rem] 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",
289
+ 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"
290
+ }
291
+ },
292
+ defaultVariants: {
293
+ variant: "default",
294
+ size: "default"
295
+ }
296
+ }
297
+ );
298
+ function Toggle({
299
+ className,
300
+ variant = "default",
301
+ size = "default",
302
+ ...props
303
+ }) {
304
+ return /* @__PURE__ */ jsx6(
305
+ TogglePrimitive,
306
+ {
307
+ "data-quassel": "",
308
+ "data-slot": "toggle",
309
+ className: cn(toggleVariants({ variant, size, className })),
310
+ ...props
311
+ }
312
+ );
313
+ }
314
+
315
+ // packages/quassel/src/QuasselProvider.tsx
316
+ import { jsx as jsx7 } from "react/jsx-runtime";
317
+ var defaultComponents = { Button, Toggle, Card, StopButton, Popover, PopoverContent };
318
+ var QuasselContext = createContext({ components: defaultComponents });
319
+ function QuasselProvider({ components: components2, allowUrl, children }) {
320
+ const outer = useContext(QuasselContext);
321
+ const value = useMemo(() => ({
322
+ components: { ...outer.components, ...components2 },
323
+ allowUrl: allowUrl ?? outer.allowUrl
324
+ }), [outer, components2, allowUrl]);
325
+ return /* @__PURE__ */ jsx7(QuasselContext.Provider, { value, children });
326
+ }
327
+ var useQuasselComponents = () => useContext(QuasselContext).components;
328
+ var useAllowUrl = () => useContext(QuasselContext).allowUrl;
329
+
330
+ // packages/quassel/src/useChatAction.ts
331
+ import { useEffect, useRef, useState } from "react";
332
+ function useChatAction() {
333
+ const pending = useRef(false);
334
+ const [status, setStatus] = useState("idle");
335
+ const [error, setError] = useState();
336
+ useEffect(() => {
204
337
  if (status !== "done") return;
205
338
  const timer = window.setTimeout(() => setStatus("idle"), 1800);
206
339
  return () => window.clearTimeout(timer);
207
340
  }, [status]);
208
- const run = async () => {
341
+ const invoke = async (action, failureLabel) => {
209
342
  if (pending.current) return;
210
343
  pending.current = true;
211
- setStatus("pending");
212
344
  setError(void 0);
345
+ setStatus("pending");
213
346
  try {
214
- await onClick();
347
+ await action();
215
348
  setStatus("done");
216
349
  } catch (failure) {
350
+ const detail = failure instanceof Error ? failure.message : String(failure);
351
+ setError(`${failureLabel}: ${detail}`);
217
352
  setStatus("idle");
218
- setError(failure instanceof Error ? failure.message : String(failure));
219
353
  } finally {
220
354
  pending.current = false;
221
355
  }
222
356
  };
223
- return /* @__PURE__ */ jsxs(Fragment, { children: [
224
- /* @__PURE__ */ jsxs("button", { className: "qsl-icon-button", type: "button", disabled: disabled || status === "pending", onClick: () => void run(), children: [
225
- icon,
226
- status === "done" && successLabel ? successLabel : label
227
- ] }),
228
- error && /* @__PURE__ */ jsx("span", { className: "qsl-action-error", role: "alert", children: error })
229
- ] });
357
+ return { status, error, invoke };
230
358
  }
231
- async function copyText(text, error) {
232
- if (!navigator.clipboard) throw new Error(error);
359
+ async function copyChatText(text, unavailable) {
360
+ if (!navigator.clipboard) throw new Error(unavailable);
233
361
  await navigator.clipboard.writeText(text);
234
362
  }
235
- function MessageActions({ message, options, texts }) {
236
- if (!options) return null;
237
- const copy = typeof options.copy === "function" ? options.copy(message) : options.copy;
238
- const edit = message.role === "user" ? options.edit : void 0;
239
- const retry = message.role === "assistant" ? options.retry : void 0;
240
- const custom = options.custom?.(message) ?? [];
241
- if (!copy && !edit && !retry && custom.length === 0) return null;
242
- return /* @__PURE__ */ jsxs("div", { className: "qsl-message-actions", role: "group", "aria-label": texts.messageActions, children: [
243
- copy && /* @__PURE__ */ jsx(ActionButton, { label: texts.copyMessage, successLabel: texts.copied, onClick: () => copyText(message.text, texts.copyFailed) }),
244
- edit && /* @__PURE__ */ jsx(ActionButton, { label: texts.editMessage, onClick: () => edit(message) }),
245
- retry && /* @__PURE__ */ jsx(ActionButton, { label: texts.retryMessage, onClick: () => retry(message) }),
246
- custom.map((action) => /* @__PURE__ */ jsx(ActionButton, { label: action.label, icon: action.icon, disabled: action.disabled, onClick: () => action.onClick(message) }, action.id))
247
- ] });
248
- }
249
-
250
- // packages/quassel/src/ChatSendContext.ts
251
- import { createContext, useContext, useLayoutEffect, useRef as useRef3, useState as useState3 } from "react";
252
- var ChatSendContext = createContext(void 0);
253
- function isSendKey(event, shortcut) {
254
- return event.key === "Enter" && !event.shiftKey && !event.altKey && !event.nativeEvent.isComposing && event.nativeEvent.keyCode !== 229 && (shortcut === "enter" || event.ctrlKey || event.metaKey);
255
- }
256
- function useChatSubmit(onSend) {
257
- const panel = useContext(ChatSendContext);
258
- const pending = useRef3(false);
259
- const mounted = useRef3(true);
260
- useLayoutEffect(() => {
261
- mounted.current = true;
262
- return () => {
263
- mounted.current = false;
264
- };
265
- }, []);
266
- const [sending, setSending] = useState3(false);
267
- const [error, setError] = useState3();
268
- const submit = async (text) => {
269
- if (pending.current) return false;
270
- pending.current = true;
271
- setSending(true);
272
- setError(void 0);
273
- try {
274
- await onSend(text);
275
- if (!mounted.current) return false;
276
- panel?.onSent();
277
- return true;
278
- } catch (failure) {
279
- if (mounted.current) setError(failure instanceof Error ? failure.message : String(failure));
280
- return false;
281
- } finally {
282
- pending.current = false;
283
- if (mounted.current) setSending(false);
284
- }
285
- };
286
- return { submit, sending, error };
287
- }
288
363
 
289
364
  // packages/quassel/src/Markdown.tsx
290
- import { createContext as createContext2, useContext as useContext2 } from "react";
291
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
292
- var CodeBlockContext = createContext2({});
365
+ import { jsx as jsx8, jsxs } from "react/jsx-runtime";
366
+ var LinkClickContext = createContext2(void 0);
367
+ var CodeBlockContext = createContext2({ texts: defaultTexts });
293
368
  function MarkdownCodeBlocks({ children, options, texts }) {
294
- return /* @__PURE__ */ jsx2(CodeBlockContext.Provider, { value: { options, texts }, children });
369
+ return /* @__PURE__ */ jsx8(CodeBlockContext.Provider, { value: { options, texts }, children });
295
370
  }
296
- function CodeBlock({ text }) {
371
+ function CodeBlock({ children }) {
297
372
  const { options, texts } = useContext2(CodeBlockContext);
298
- const labels = { ...defaultTexts, ...texts };
299
- const content = /* @__PURE__ */ jsx2("pre", { style: {
300
- whiteSpace: options?.wrap ? "pre-wrap" : void 0,
301
- overflowWrap: options?.wrap ? "anywhere" : void 0,
302
- maxHeight: options?.maxHeight,
303
- overflowY: options?.maxHeight === void 0 ? void 0 : "auto"
304
- }, children: /* @__PURE__ */ jsx2("code", { children: text }) });
305
- return options?.showCopyButton ? /* @__PURE__ */ jsxs2("div", { className: "qsl-code-block", children: [
306
- /* @__PURE__ */ jsx2(ActionButton, { label: labels.copyCode, successLabel: labels.copied, onClick: () => copyText(text, labels.copyFailed) }),
307
- content
308
- ] }) : content;
373
+ const { Button: Button2 } = useQuasselComponents();
374
+ const code = useRef2(null);
375
+ const action = useChatAction();
376
+ const label = action.status === "done" ? texts.copied : texts.copyCode;
377
+ const block = /* @__PURE__ */ jsx8(
378
+ "pre",
379
+ {
380
+ ref: code,
381
+ "data-chat": "code-block",
382
+ className: cn(scrollbar, "qsl:my-2 qsl:overflow-auto qsl:rounded-lg qsl:bg-secondary qsl:px-3 qsl:py-2 qsl:font-mono qsl:text-sm qsl:leading-relaxed qsl:in-data-[tone=on-color]:bg-black/30 qsl:in-data-[tone=on-color]:text-inherit"),
383
+ style: { maxHeight: options?.maxHeight, whiteSpace: options?.wrap ? "pre-wrap" : void 0, overflowWrap: options?.wrap ? "anywhere" : void 0 },
384
+ children
385
+ }
386
+ );
387
+ if (!options?.showCopyButton) return block;
388
+ return /* @__PURE__ */ jsxs("div", { className: "qsl:relative", children: [
389
+ block,
390
+ /* @__PURE__ */ jsx8(
391
+ Button2,
392
+ {
393
+ "aria-label": label,
394
+ className: "qsl:absolute qsl:top-1 qsl:right-1 qsl:bg-secondary",
395
+ disabled: action.status === "pending",
396
+ size: "icon-sm",
397
+ title: label,
398
+ variant: "ghost",
399
+ onClick: () => {
400
+ void action.invoke(() => copyChatText(code.current?.textContent ?? "", texts.clipboardUnavailable), texts.actionFailed);
401
+ },
402
+ children: action.status === "done" ? /* @__PURE__ */ jsx8(CheckIcon, {}) : /* @__PURE__ */ jsx8(CopyIcon, {})
403
+ }
404
+ ),
405
+ action.error && /* @__PURE__ */ jsx8("p", { className: "qsl:text-sm qsl:text-destructive", role: "alert", children: action.error })
406
+ ] });
309
407
  }
310
- var LinkClickContext = createContext2(void 0);
311
408
  function MarkdownLinks({ children, onLinkClick }) {
312
- return /* @__PURE__ */ jsx2(LinkClickContext.Provider, { value: onLinkClick, children });
409
+ return /* @__PURE__ */ jsx8(LinkClickContext.Provider, { value: onLinkClick, children });
313
410
  }
314
- function MarkdownLink({ href, label }) {
411
+ function MarkdownLink({ href, children }) {
315
412
  const onLinkClick = useContext2(LinkClickContext);
316
- return /* @__PURE__ */ jsx2(
413
+ if (!href || href === "streamdown:incomplete-link") {
414
+ return /* @__PURE__ */ jsx8("span", { children });
415
+ }
416
+ return /* @__PURE__ */ jsx8(
317
417
  "a",
318
418
  {
419
+ className: "qsl:text-primary qsl:[text-underline-offset:2px] qsl:in-data-[tone=on-color]:text-inherit qsl:in-data-[tone=on-color]:underline",
319
420
  href,
320
421
  onClick: (event) => {
321
- if (onLinkClick?.(href, label)) {
422
+ if (href && onLinkClick?.(href, event.currentTarget.textContent ?? "")) {
322
423
  event.preventDefault();
323
424
  }
324
425
  },
325
426
  rel: "noreferrer",
326
427
  target: "_blank",
327
- children: label
428
+ children
328
429
  }
329
430
  );
330
431
  }
331
- function Markdown({ text }) {
332
- const lines = text.split("\n");
333
- const blocks = [];
334
- let i = 0;
335
- let key = 0;
336
- while (i < lines.length) {
337
- const line = lines[i];
338
- const fence = line.match(/^\s*```(\w*)\s*$/);
339
- if (fence) {
340
- const body = [];
341
- i++;
342
- while (i < lines.length && !/^\s*```\s*$/.test(lines[i])) {
343
- body.push(lines[i]);
344
- i++;
345
- }
346
- if (i < lines.length) {
347
- i++;
348
- }
349
- blocks.push(
350
- /* @__PURE__ */ jsx2(CodeBlock, { text: body.join("\n") }, key++)
351
- );
352
- continue;
353
- }
354
- if (line.includes("|") && i + 1 < lines.length && isTableDivider(lines[i + 1])) {
355
- const header = splitTableRow(line);
356
- i += 2;
357
- const rows = [];
358
- while (i < lines.length && lines[i].includes("|") && lines[i].trim() !== "") {
359
- rows.push(splitTableRow(lines[i]));
360
- i++;
361
- }
362
- blocks.push(
363
- /* @__PURE__ */ jsx2("div", { className: "qsl-md-scroll", children: /* @__PURE__ */ jsxs2("table", { children: [
364
- /* @__PURE__ */ jsx2("thead", { children: /* @__PURE__ */ jsx2("tr", { children: header.map((cell, ci) => /* @__PURE__ */ jsx2("th", { children: renderInline(cell, `th${key}_${ci}`) }, ci)) }) }),
365
- /* @__PURE__ */ jsx2("tbody", { children: rows.map((row, ri) => /* @__PURE__ */ jsx2("tr", { children: header.map((_, ci) => /* @__PURE__ */ jsx2("td", { children: renderInline(row[ci] ?? "", `td${key}_${ri}_${ci}`) }, ci)) }, ri)) })
366
- ] }) }, key++)
367
- );
368
- continue;
369
- }
370
- const heading = line.match(/^(#{1,4})\s+(.*)$/);
371
- if (heading) {
372
- const level = heading[1].length;
373
- blocks.push(
374
- level === 1 ? /* @__PURE__ */ jsx2("h3", { children: renderInline(heading[2], `h${key}`) }, key++) : /* @__PURE__ */ jsx2("h4", { children: renderInline(heading[2], `h${key}`) }, key++)
375
- );
376
- i++;
377
- continue;
378
- }
379
- const quote = line.match(/^\s*>\s?(.*)$/);
380
- if (quote) {
381
- blocks.push(/* @__PURE__ */ jsx2("blockquote", { children: renderInline(quote[1], `q${key}`) }, key++));
382
- i++;
383
- continue;
384
- }
385
- const bullet = line.match(/^\s*[-*]\s+(.*)$/);
386
- if (bullet) {
387
- blocks.push(
388
- /* @__PURE__ */ jsxs2("div", { className: "qsl-md-bullet", children: [
389
- /* @__PURE__ */ jsx2("span", { children: "\u2022" }),
390
- /* @__PURE__ */ jsx2("span", { children: renderInline(bullet[1], `l${key}`) })
391
- ] }, key++)
392
- );
393
- i++;
394
- continue;
395
- }
396
- const numbered = line.match(/^\s*(\d+)\.\s+(.*)$/);
397
- if (numbered) {
398
- blocks.push(
399
- /* @__PURE__ */ jsxs2("div", { className: "qsl-md-bullet", children: [
400
- /* @__PURE__ */ jsxs2("span", { children: [
401
- numbered[1],
402
- "."
403
- ] }),
404
- /* @__PURE__ */ jsx2("span", { children: renderInline(numbered[2], `l${key}`) })
405
- ] }, key++)
406
- );
407
- i++;
408
- continue;
409
- }
410
- if (line.trim() === "") {
411
- blocks.push(/* @__PURE__ */ jsx2("div", { className: "qsl-md-gap" }, key++));
412
- i++;
413
- continue;
414
- }
415
- blocks.push(/* @__PURE__ */ jsx2("p", { children: renderInline(line, `l${key}`) }, key++));
416
- i++;
417
- }
418
- return /* @__PURE__ */ jsx2("div", { className: "qsl-md", children: blocks });
432
+ 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";
433
+ 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]";
434
+ var headingClasses = "qsl:mt-3 qsl:mb-1 qsl:text-[15px] qsl:font-medium";
435
+ 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";
436
+ var components = {
437
+ h1: ({ children }) => /* @__PURE__ */ jsx8("h1", { className: "qsl:mt-3 qsl:mb-1 qsl:text-lg qsl:font-semibold", children }),
438
+ h2: ({ children }) => /* @__PURE__ */ jsx8("h2", { className: headingClasses, children }),
439
+ h3: ({ children }) => /* @__PURE__ */ jsx8("h3", { className: headingClasses, children }),
440
+ h4: ({ children }) => /* @__PURE__ */ jsx8("h4", { className: "qsl:mt-2 qsl:mb-1 qsl:text-base qsl:font-medium", children }),
441
+ h5: ({ children }) => /* @__PURE__ */ jsx8("h5", { className: "qsl:mt-2 qsl:mb-1 qsl:text-base qsl:font-medium", children }),
442
+ h6: ({ children }) => /* @__PURE__ */ jsx8("h6", { className: "qsl:mt-2 qsl:mb-1 qsl:text-base qsl:font-medium", children }),
443
+ p: ({ children }) => /* @__PURE__ */ jsx8("p", { className: "qsl:[p+&]:mt-2", children }),
444
+ ul: ({ children }) => /* @__PURE__ */ jsx8("ul", { className: "qsl:my-2 qsl:list-disc qsl:pl-6 qsl:[li>&]:my-0", children }),
445
+ ol: ({ children }) => /* @__PURE__ */ jsx8("ol", { className: "qsl:my-2 qsl:list-decimal qsl:pl-6 qsl:[li>&]:my-0", children }),
446
+ li: ({ children }) => /* @__PURE__ */ jsx8("li", { className: "qsl:marker:text-muted-foreground qsl:in-data-[tone=on-color]:marker:text-inherit", children }),
447
+ hr: () => /* @__PURE__ */ jsx8("hr", { className: "qsl:my-3 qsl:border-t qsl:border-border" }),
448
+ img: ({ src, alt }) => /* @__PURE__ */ jsx8("img", { alt, className: "qsl:h-auto qsl:max-w-full", src: typeof src === "string" ? src : void 0 }),
449
+ 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 }),
450
+ strong: ({ children }) => /* @__PURE__ */ jsx8("strong", { className: "qsl:font-medium", children }),
451
+ em: ({ children }) => /* @__PURE__ */ jsx8("em", { children }),
452
+ a: ({ href, children }) => /* @__PURE__ */ jsx8(MarkdownLink, { href, children }),
453
+ pre: ({ children }) => /* @__PURE__ */ jsx8(CodeBlock, { children }),
454
+ 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-sm 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 }),
455
+ // Ohne max-content schrumpft die Tabelle auf den Container und stapelt Buchstaben.
456
+ table: ({ children }) => /* @__PURE__ */ jsx8("div", { className: cn(scrollbar, scrollCover, "qsl:my-2 qsl:overflow-x-auto"), children: /* @__PURE__ */ jsx8("table", { className: "qsl:w-max qsl:min-w-full qsl:border-collapse qsl:text-sm", children }) }),
457
+ thead: ({ children }) => /* @__PURE__ */ jsx8("thead", { children }),
458
+ tbody: ({ children }) => /* @__PURE__ */ jsx8("tbody", { children }),
459
+ tr: ({ children }) => /* @__PURE__ */ jsx8("tr", { children }),
460
+ 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 }),
461
+ td: ({ children }) => /* @__PURE__ */ jsx8("td", { className: cellClasses, children })
462
+ };
463
+ function transformUrl(url, allowUrl) {
464
+ if (allowUrl?.(url)) return url;
465
+ const scheme = /^[^/?#]*:/.exec(url);
466
+ return !scheme || /^(https?|mailto|tel|ftp|irc|ircs|xmpp):$/i.test(scheme[0]) ? url : "";
419
467
  }
420
- function renderInline(text, keyPrefix) {
421
- const out = [];
422
- const pattern = /\[([^\]]+)\]\(([^)]+)\)|\*\*([^*]+)\*\*|\*([^*]+)\*|`([^`]+)`/g;
423
- let last = 0;
424
- let match;
425
- let i = 0;
426
- while ((match = pattern.exec(text)) !== null) {
427
- if (match.index > last) {
428
- out.push(text.slice(last, match.index));
429
- }
430
- if (match[1] !== void 0) {
431
- out.push(/* @__PURE__ */ jsx2(MarkdownLink, { href: match[2], label: match[1] }, `${keyPrefix}a${i}`));
432
- } else if (match[3] !== void 0) {
433
- out.push(/* @__PURE__ */ jsx2("strong", { children: match[3] }, `${keyPrefix}b${i}`));
434
- } else if (match[4] !== void 0) {
435
- out.push(/* @__PURE__ */ jsx2("em", { children: match[4] }, `${keyPrefix}i${i}`));
436
- } else if (match[5] !== void 0) {
437
- out.push(/* @__PURE__ */ jsx2("code", { children: match[5] }, `${keyPrefix}c${i}`));
468
+ function Markdown({ text, streaming = false }) {
469
+ const allowUrl = useAllowUrl();
470
+ const urlTransform = useCallback((url) => transformUrl(url, allowUrl), [allowUrl]);
471
+ return /* @__PURE__ */ jsx8(
472
+ Streamdown,
473
+ {
474
+ className: "space-y-0 text-inherit",
475
+ prefix: "qsl",
476
+ mode: streaming ? "streaming" : "static",
477
+ isAnimating: streaming,
478
+ parseIncompleteMarkdown: streaming,
479
+ components,
480
+ controls: false,
481
+ rehypePlugins: [],
482
+ urlTransform,
483
+ skipHtml: true,
484
+ children: text
438
485
  }
439
- last = match.index + match[0].length;
440
- i++;
441
- }
442
- if (last < text.length) {
443
- out.push(text.slice(last));
444
- }
445
- return out;
486
+ );
446
487
  }
447
488
  function markdownPlainText(text) {
448
- return text.split("\n").filter((line) => !/^\s*```\w*\s*$/.test(line) && !isTableDivider(line)).map((line) => (line.trim().startsWith("|") ? splitTableRow(line).join(", ") : line).replace(/^(#{1,4})\s+/, "").replace(/^\s*>\s?/, "").replace(/^\s*[-*]\s+/, "").replace(/\[([^\]]+)\]\(([^)]+)\)|\*\*([^*]+)\*\*|\*([^*]+)\*|`([^`]+)`/g, (_, link, _href, fett, kursiv, code) => link ?? fett ?? kursiv ?? code).trim()).filter((line) => line !== "").join("\n");
489
+ 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, fett, fettUnterstrich, kursiv, code) => link ?? fett ?? fettUnterstrich ?? kursiv ?? code).trim()).filter((line) => line !== "").join("\n");
449
490
  }
450
491
  function splitTableRow(line) {
451
492
  let row = line.trim();
@@ -461,134 +502,11 @@ function isTableDivider(line) {
461
502
  return /^\s*\|?\s*:?-{1,}:?\s*(\|\s*:?-{1,}:?\s*)*\|?\s*$/.test(line) && line.includes("-");
462
503
  }
463
504
 
464
- // packages/quassel/src/announce.ts
465
- var abstandMs = 400;
466
- var hoechstensWartend = 5;
467
- var region;
468
- var wartend = [];
469
- var laeuft = false;
470
- function ansageRegion() {
471
- if (region?.isConnected) {
472
- return region;
473
- }
474
- const element = document.createElement("div");
475
- element.setAttribute("role", "status");
476
- element.setAttribute("aria-live", "polite");
477
- element.setAttribute("aria-atomic", "true");
478
- Object.assign(element.style, {
479
- position: "absolute",
480
- width: "1px",
481
- height: "1px",
482
- margin: "-1px",
483
- padding: "0",
484
- border: "0",
485
- overflow: "hidden",
486
- clipPath: "inset(50%)",
487
- whiteSpace: "nowrap"
488
- });
489
- document.body.append(element);
490
- region = element;
491
- return element;
492
- }
493
- function naechste() {
494
- if (wartend.length === 0) {
495
- laeuft = false;
496
- return;
497
- }
498
- if (document.visibilityState === "hidden") {
499
- document.addEventListener("visibilitychange", naechste, { once: true });
500
- return;
501
- }
502
- const [text, ...rest] = wartend;
503
- wartend = rest;
504
- const ziel = ansageRegion();
505
- ziel.textContent = ziel.textContent === text ? `${text}\xA0` : text;
506
- window.setTimeout(naechste, abstandMs);
507
- }
508
- function announce(text) {
509
- const inhalt = text.trim();
510
- if (!inhalt || typeof document === "undefined") {
511
- return;
512
- }
513
- wartend = [...wartend, inhalt].slice(-hoechstensWartend);
514
- if (laeuft) {
515
- return;
516
- }
517
- laeuft = true;
518
- ansageRegion();
519
- window.setTimeout(naechste, abstandMs);
520
- }
521
-
522
- // packages/quassel/src/StepPopover.tsx
523
- import { useEffect as useEffect3, useLayoutEffect as useLayoutEffect2, useRef as useRef4, useState as useState4 } from "react";
524
- import { createPortal } from "react-dom";
525
-
526
- // packages/quassel/src/icons.tsx
527
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
528
- function Svg({ size = 14, className, children }) {
529
- return /* @__PURE__ */ jsx3(
530
- "svg",
531
- {
532
- "aria-hidden": "true",
533
- className,
534
- fill: "none",
535
- height: size,
536
- stroke: "currentColor",
537
- strokeLinecap: "round",
538
- strokeLinejoin: "round",
539
- strokeWidth: 2,
540
- viewBox: "0 0 24 24",
541
- width: size,
542
- children
543
- }
544
- );
545
- }
546
- function IconSend(props) {
547
- return /* @__PURE__ */ jsxs3(Svg, { ...props, children: [
548
- /* @__PURE__ */ jsx3("path", { d: "M22 2 11 13" }),
549
- /* @__PURE__ */ jsx3("path", { d: "M22 2 15 22 11 13 2 9Z" })
550
- ] });
551
- }
552
- function IconStop(props) {
553
- return /* @__PURE__ */ jsx3(Svg, { ...props, children: /* @__PURE__ */ jsx3("rect", { fill: "currentColor", height: 12, rx: 2, stroke: "none", width: 12, x: 6, y: 6 }) });
554
- }
555
- function IconX(props) {
556
- return /* @__PURE__ */ jsxs3(Svg, { ...props, children: [
557
- /* @__PURE__ */ jsx3("path", { d: "M18 6 6 18" }),
558
- /* @__PURE__ */ jsx3("path", { d: "m6 6 12 12" })
559
- ] });
560
- }
561
- function IconChevronDown(props) {
562
- return /* @__PURE__ */ jsx3(Svg, { ...props, children: /* @__PURE__ */ jsx3("path", { d: "m6 9 6 6 6-6" }) });
563
- }
564
- function IconChevronRight(props) {
565
- return /* @__PURE__ */ jsx3(Svg, { ...props, children: /* @__PURE__ */ jsx3("path", { d: "m9 18 6-6-6-6" }) });
566
- }
567
- function IconLayers(props) {
568
- return /* @__PURE__ */ jsxs3(Svg, { ...props, children: [
569
- /* @__PURE__ */ jsx3("path", { d: "m12 2 10 5-10 5L2 7Z" }),
570
- /* @__PURE__ */ jsx3("path", { d: "m2 17 10 5 10-5" }),
571
- /* @__PURE__ */ jsx3("path", { d: "m2 12 10 5 10-5" })
572
- ] });
573
- }
574
- function IconCheck(props) {
575
- return /* @__PURE__ */ jsx3(Svg, { ...props, children: /* @__PURE__ */ jsx3("path", { d: "M20 6 9 17l-5-5" }) });
576
- }
577
- function IconSpark(props) {
578
- return /* @__PURE__ */ jsx3(Svg, { ...props, children: /* @__PURE__ */ jsx3("path", { d: "M12 3l1.9 6.1L20 11l-6.1 1.9L12 19l-1.9-6.1L4 11l6.1-1.9Z" }) });
579
- }
580
- function IconClock(props) {
581
- return /* @__PURE__ */ jsxs3(Svg, { ...props, children: [
582
- /* @__PURE__ */ jsx3("circle", { cx: 12, cy: 12, r: 9 }),
583
- /* @__PURE__ */ jsx3("path", { d: "M12 7v5l3 2" })
584
- ] });
585
- }
586
- function IconTool(props) {
587
- return /* @__PURE__ */ jsx3(Svg, { ...props, children: /* @__PURE__ */ jsx3("path", { d: "M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" }) });
588
- }
589
-
590
505
  // packages/quassel/src/StepPopover.tsx
591
- import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
506
+ import { useMemo as useMemo2 } from "react";
507
+ import { SparklesIcon, WrenchIcon, XIcon } from "lucide-react";
508
+ import { jsx as jsx9, jsxs as jsxs2 } from "react/jsx-runtime";
509
+ var preClasses = "qsl:rounded-lg qsl:bg-secondary qsl:p-3 qsl:font-mono qsl:text-[11px] qsl:leading-[1.625] qsl:break-all qsl:whitespace-pre-wrap";
592
510
  function StepPopover({
593
511
  message,
594
512
  position,
@@ -596,168 +514,86 @@ function StepPopover({
596
514
  toolArgumentsText,
597
515
  onClose
598
516
  }) {
599
- const popover = useRef4(null);
600
- const [placement, setPlacement] = useState4();
601
- useLayoutEffect2(() => {
602
- const element = popover.current;
603
- if (!element) {
604
- return;
605
- }
606
- const margin = 16;
607
- const gap = 10;
608
- const bounds = element.getBoundingClientRect();
609
- const preferredLeft = position.x + gap;
610
- const preferredTop = position.y + gap;
611
- const left = preferredLeft + bounds.width <= window.innerWidth - margin ? preferredLeft : position.x - bounds.width - gap;
612
- const top = preferredTop + bounds.height <= window.innerHeight - margin ? preferredTop : position.y - bounds.height - gap;
613
- setPlacement({
614
- left: Math.max(margin, Math.min(left, window.innerWidth - bounds.width - margin)),
615
- top: Math.max(margin, Math.min(top, window.innerHeight - bounds.height - margin))
616
- });
617
- }, [message.text, message.tool?.result, position]);
618
- useEffect3(() => {
619
- const handleKeyDown = (event) => {
620
- if (event.key === "Escape") {
621
- onClose();
622
- }
623
- };
624
- window.addEventListener("keydown", handleKeyDown);
625
- return () => window.removeEventListener("keydown", handleKeyDown);
626
- }, [onClose]);
627
- useEffect3(() => {
628
- const handlePointerDown = (event) => {
629
- if (!popover.current?.contains(event.target)) {
630
- onClose();
631
- }
632
- };
633
- document.addEventListener("pointerdown", handlePointerDown, true);
634
- return () => document.removeEventListener("pointerdown", handlePointerDown, true);
635
- }, [onClose]);
517
+ const anchor = useMemo2(() => ({
518
+ getBoundingClientRect: () => new DOMRect(position.x, position.y, 0, 0),
519
+ contextElement: document.documentElement
520
+ }), [position]);
521
+ const { Button: Button2, Popover: Popover2, PopoverContent: PopoverContent2 } = useQuasselComponents();
636
522
  const tool = message.tool;
637
- const title = message.role === "thinking" ? texts.thinkingTitle : texts.toolTitle;
638
- return createPortal(
639
- /* @__PURE__ */ jsxs4(Fragment2, { children: [
640
- /* @__PURE__ */ jsx4("div", { className: "qsl-popover-backdrop", onClick: onClose }),
641
- /* @__PURE__ */ jsxs4(
642
- "div",
643
- {
644
- "aria-label": title,
645
- className: "qsl-popover",
646
- ref: popover,
647
- role: "dialog",
648
- style: {
649
- left: placement?.left ?? position.x,
650
- top: placement?.top ?? position.y,
651
- visibility: placement ? "visible" : "hidden"
652
- },
653
- children: [
654
- /* @__PURE__ */ jsxs4("div", { className: "qsl-popover__head", children: [
655
- message.role === "thinking" ? /* @__PURE__ */ jsx4(IconSpark, { className: "qsl-popover__icon" }) : /* @__PURE__ */ jsx4(IconTool, { className: tool?.isError ? "qsl-popover__icon qsl-popover__icon--error" : "qsl-popover__icon" }),
656
- /* @__PURE__ */ jsx4("span", { className: "qsl-popover__title", children: title }),
657
- /* @__PURE__ */ jsx4("button", { "aria-label": texts.close, className: "qsl-popover__close", onClick: onClose, title: texts.close, type: "button", children: /* @__PURE__ */ jsx4(IconX, {}) })
658
- ] }),
659
- /* @__PURE__ */ jsx4("div", { className: "qsl-popover__body", children: message.role === "thinking" ? /* @__PURE__ */ jsx4("div", { className: "qsl-popover__thinking", children: message.text }) : tool ? /* @__PURE__ */ jsxs4("div", { className: "qsl-popover__sections", children: [
660
- /* @__PURE__ */ jsx4("div", { className: "qsl-popover__toolname", children: message.text }),
661
- /* @__PURE__ */ jsx4(PopoverSection, { label: texts.argumentsLabel, value: toolArgumentsText(tool) }),
662
- tool.result === void 0 ? /* @__PURE__ */ jsx4("div", { className: "qsl-popover__running", children: texts.toolStillRunning }) : /* @__PURE__ */ jsx4(PopoverSection, { error: tool.isError, label: texts.resultLabel, value: prettyJson(tool.result) })
663
- ] }) : null })
664
- ]
665
- }
666
- )
667
- ] }),
668
- document.body
669
- );
523
+ const thinking = message.role === "thinking";
524
+ const title = thinking ? texts.thinkingTitle : texts.toolTitle;
525
+ return /* @__PURE__ */ jsx9(Popover2, { open: true, onOpenChange: (open) => {
526
+ if (!open) onClose();
527
+ }, children: /* @__PURE__ */ jsxs2(
528
+ PopoverContent2,
529
+ {
530
+ align: "start",
531
+ anchor,
532
+ "aria-label": title,
533
+ className: cn(
534
+ "qsl:w-[min(680px,calc(100vw-32px))] qsl:gap-0 qsl:overflow-hidden qsl:p-0 qsl:text-[13px] qsl:leading-normal qsl:shadow-pop",
535
+ thinking ? "qsl:max-h-none" : "qsl:max-h-[min(70vh,620px)]"
536
+ ),
537
+ collisionPadding: 16,
538
+ side: "bottom",
539
+ sideOffset: 10,
540
+ children: [
541
+ /* @__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: [
542
+ 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 }),
543
+ /* @__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 }),
544
+ /* @__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, {}) })
545
+ ] }),
546
+ /* @__PURE__ */ jsx9("div", { className: cn("qsl:min-h-0 qsl:p-4", thinking ? "qsl:overflow-visible" : "qsl:overflow-auto"), 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: [
547
+ /* @__PURE__ */ jsx9("div", { className: "qsl:font-mono qsl:text-sm", children: message.text }),
548
+ /* @__PURE__ */ jsx9(PopoverSection, { label: texts.argumentsLabel, value: toolArgumentsText(tool) }),
549
+ 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) })
550
+ ] }) : null })
551
+ ]
552
+ }
553
+ ) });
670
554
  }
671
555
  function PopoverSection({ label, value, error = false }) {
672
- return /* @__PURE__ */ jsxs4("section", { children: [
673
- /* @__PURE__ */ jsx4("h3", { children: label }),
674
- /* @__PURE__ */ jsx4("pre", { className: error ? "qsl-popover__pre qsl-popover__pre--error" : "qsl-popover__pre", children: value })
556
+ return /* @__PURE__ */ jsxs2("section", { children: [
557
+ /* @__PURE__ */ jsx9("h3", { className: "qsl:mb-1.5 qsl:text-[11px] qsl:font-semibold qsl:tracking-[0.04em] qsl:text-muted-foreground qsl:uppercase", children: label }),
558
+ /* @__PURE__ */ jsx9("pre", { className: cn(preClasses, error && "qsl:text-destructive"), children: value })
675
559
  ] });
676
560
  }
677
561
 
678
- // packages/quassel/src/QuestionCard.tsx
679
- import { useState as useState5 } from "react";
680
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
681
- function QuestionCard({
562
+ // packages/quassel/src/PendingActionCard.tsx
563
+ import { CheckIcon as CheckIcon2 } from "lucide-react";
564
+ import { jsx as jsx10, jsxs as jsxs3 } from "react/jsx-runtime";
565
+ var cardClasses = "qsl:gap-2.5 qsl:bg-background qsl:px-3.5 qsl:text-sm";
566
+ var resultText = (result) => typeof result === "string" ? result : result === null || result === void 0 ? "" : JSON.stringify(result);
567
+ function PendingActionCard({
682
568
  text,
683
- question,
684
- texts,
685
- onAnswer
569
+ action,
570
+ waitingLabel,
571
+ dismissLabel,
572
+ dismissedLabel,
573
+ onDismiss
686
574
  }) {
687
- const [gewaehlt, setGewaehlt] = useState5([]);
688
- const [frei, setFrei] = useState5("");
689
- if (question.answer !== void 0) {
690
- return /* @__PURE__ */ jsxs5("div", { className: "qsl-question", children: [
691
- /* @__PURE__ */ jsx5("div", { className: "qsl-question__text", children: text }),
692
- /* @__PURE__ */ jsxs5("div", { className: "qsl-question__answered", children: [
693
- /* @__PURE__ */ jsx5(IconCheck, { className: "qsl-question__check", size: 12 }),
694
- question.answer
575
+ const { Button: Button2, Card: Card2 } = useQuasselComponents();
576
+ if (action.status !== void 0) {
577
+ return /* @__PURE__ */ jsxs3(Card2, { className: cardClasses, size: "sm", children: [
578
+ /* @__PURE__ */ jsx10("div", { className: "qsl:leading-normal", children: text }),
579
+ /* @__PURE__ */ jsxs3("div", { className: "qsl:flex qsl:items-center qsl:gap-1.5 qsl:text-sm qsl:text-muted-foreground", "data-action": "resolved", children: [
580
+ /* @__PURE__ */ jsx10(CheckIcon2, { className: "qsl:text-success", size: 12 }),
581
+ action.status === "approved" ? resultText(action.result) : dismissedLabel
695
582
  ] })
696
583
  ] });
697
584
  }
698
- const umschalten = (option) => setGewaehlt((bisher) => bisher.includes(option) ? bisher.filter((o) => o !== option) : [...bisher, option]);
699
- const freiSenden = () => {
700
- const wert = frei.trim();
701
- if (wert) onAnswer(wert);
702
- };
703
- return /* @__PURE__ */ jsxs5("div", { className: "qsl-question", children: [
704
- /* @__PURE__ */ jsx5("div", { className: "qsl-question__text", children: text }),
705
- /* @__PURE__ */ jsx5("div", { className: "qsl-question__options", children: question.options.map(
706
- (option) => question.multi ? /* @__PURE__ */ jsxs5(
707
- "button",
708
- {
709
- "aria-pressed": gewaehlt.includes(option),
710
- className: gewaehlt.includes(option) ? "qsl-question__option qsl-question__option--gewaehlt" : "qsl-question__option",
711
- onClick: () => umschalten(option),
712
- type: "button",
713
- children: [
714
- /* @__PURE__ */ jsx5("span", { className: "qsl-question__kasten", children: gewaehlt.includes(option) && /* @__PURE__ */ jsx5(IconCheck, { size: 10 }) }),
715
- option
716
- ]
717
- },
718
- option
719
- ) : /* @__PURE__ */ jsx5("button", { className: "qsl-question__option", onClick: () => onAnswer(option), type: "button", children: option }, option)
720
- ) }),
721
- question.multi && /* @__PURE__ */ jsx5(
722
- "button",
723
- {
724
- className: "qsl-question__uebernehmen",
725
- disabled: gewaehlt.length === 0,
726
- onClick: () => onAnswer(gewaehlt.join("; ")),
727
- type: "button",
728
- children: texts.questionSubmit
729
- }
730
- ),
731
- /* @__PURE__ */ jsxs5("div", { className: "qsl-question__frei", children: [
732
- /* @__PURE__ */ jsx5(
733
- "input",
734
- {
735
- className: "qsl-field qsl-question__frei-feld",
736
- onChange: (event) => setFrei(event.target.value),
737
- onKeyDown: (event) => {
738
- if (event.key === "Enter" && !event.nativeEvent.isComposing) freiSenden();
739
- },
740
- placeholder: texts.questionPlaceholder,
741
- value: frei
742
- }
743
- ),
744
- /* @__PURE__ */ jsx5(
745
- "button",
746
- {
747
- className: "qsl-question__frei-senden",
748
- disabled: frei.trim() === "",
749
- onClick: freiSenden,
750
- type: "button",
751
- children: texts.questionAnswer
752
- }
753
- )
585
+ return /* @__PURE__ */ jsxs3(Card2, { className: cardClasses, size: "sm", children: [
586
+ /* @__PURE__ */ jsx10("div", { className: "qsl:leading-normal", children: text }),
587
+ /* @__PURE__ */ jsxs3("div", { className: "qsl:flex qsl:items-center qsl:justify-between qsl:gap-2", children: [
588
+ /* @__PURE__ */ jsx10("span", { className: "qsl:text-sm qsl:text-muted-foreground", "data-action": "waiting", children: waitingLabel }),
589
+ onDismiss && /* @__PURE__ */ jsx10(Button2, { onClick: onDismiss, size: "sm", variant: "outline", children: dismissLabel })
754
590
  ] })
755
591
  ] });
756
592
  }
757
593
 
758
594
  // packages/quassel/src/WorkingScenes.tsx
759
- import { useEffect as useEffect4, useState as useState6 } from "react";
760
- import { jsx as jsx6 } from "react/jsx-runtime";
595
+ import { useEffect as useEffect2, useState as useState2 } from "react";
596
+ import { jsx as jsx11, jsxs as jsxs4 } from "react/jsx-runtime";
761
597
  var SPUR = 13;
762
598
  var GRUNDTAKT_MS = 90;
763
599
  var SZENENWECHSEL_MS = 2600;
@@ -802,6 +638,7 @@ var SZENEN = [
802
638
  {
803
639
  // Aus einem Samen wird ein Baum, dann von vorn.
804
640
  id: "wachsen",
641
+ compact: true,
805
642
  takt: 2,
806
643
  zeichne: (schritt) => {
807
644
  const stufen = ["\u{1F331}", "\u{1F331}", "\u{1F33F}", "\u{1F33F}", "\u{1F343}", "\u{1F333}", "\u{1F333}", "\u{1F333}"];
@@ -811,11 +648,12 @@ var SZENEN = [
811
648
  {
812
649
  // Das Ei bekommt Risse, das Kueken schluepft.
813
650
  id: "schluepfen",
651
+ compact: true,
814
652
  takt: 2,
815
653
  zeichne: (schritt) => {
816
654
  const takt = schritt % 12;
817
655
  if (takt < 4) return "\u{1F95A}";
818
- if (takt < 6) return "\u{1F95A}\u2728";
656
+ if (takt < 6) return "\u{1F95A}*";
819
657
  if (takt < 9) return "\u{1F423}";
820
658
  return "\u{1F425}";
821
659
  }
@@ -844,11 +682,12 @@ var SZENEN = [
844
682
  {
845
683
  // Ei, Kueken, Henne - und die legt das naechste Ei.
846
684
  id: "huhn-kreislauf",
685
+ compact: true,
847
686
  takt: 3,
848
687
  zeichne: (schritt) => {
849
688
  const takt = schritt % 16;
850
689
  if (takt < 3) return "\u{1F95A}";
851
- if (takt < 5) return "\u{1F95A}\u2728";
690
+ if (takt < 5) return "\u{1F95A}*";
852
691
  if (takt < 8) return "\u{1F423}";
853
692
  if (takt < 11) return "\u{1F424}";
854
693
  if (takt < 14) return "\u{1F414}";
@@ -858,11 +697,12 @@ var SZENEN = [
858
697
  {
859
698
  // Hut, Funken - und das Kaninchen tuermt.
860
699
  id: "zauberhut",
700
+ compact: true,
861
701
  takt: 3,
862
702
  zeichne: (schritt) => {
863
703
  const takt = schritt % 14;
864
704
  if (takt < 4) return "\u{1F3A9}";
865
- if (takt < 7) return "\u{1F3A9}\u2728";
705
+ if (takt < 7) return "\u{1F3A9}*";
866
706
  if (takt < 11) return "\u{1F3A9}\u{1F407}";
867
707
  return "\u{1F407}\u{1F4A8}";
868
708
  }
@@ -870,6 +710,7 @@ var SZENEN = [
870
710
  {
871
711
  // Rot, Gelb, Gruen - und ab.
872
712
  id: "ampel",
713
+ compact: true,
873
714
  takt: 3,
874
715
  zeichne: (schritt) => {
875
716
  const takt = schritt % 12;
@@ -882,6 +723,7 @@ var SZENEN = [
882
723
  {
883
724
  // Klingeln, verschlafen, losrennen.
884
725
  id: "wecker",
726
+ compact: true,
885
727
  takt: 2,
886
728
  zeichne: (schritt) => {
887
729
  const takt = schritt % 14;
@@ -901,34 +743,286 @@ var SZENEN = [
901
743
  return bahn({ 4: "\u{1F389}" }, 5);
902
744
  }
903
745
  }
904
- ];
905
- function WorkingScenes({ label = "Arbeitet ..." }) {
906
- const [szene, setSzene] = useState6(() => SZENEN[Math.floor(Math.random() * SZENEN.length)]);
907
- const [versatz, setVersatz] = useState6(() => Math.floor(Math.random() * 20));
908
- const [frameTick, setFrameTick] = useState6(0);
909
- useEffect4(() => {
910
- const timer = window.setInterval(() => {
911
- setSzene((bisher) => {
912
- const andere = SZENEN.filter((s) => s.id !== bisher.id);
913
- return andere[Math.floor(Math.random() * andere.length)];
914
- });
915
- setVersatz(Math.floor(Math.random() * 20));
916
- setFrameTick(0);
917
- }, SZENENWECHSEL_MS);
918
- return () => window.clearInterval(timer);
919
- }, []);
920
- useEffect4(() => {
921
- const timer = window.setInterval(() => setFrameTick((value) => value + 1), GRUNDTAKT_MS);
922
- return () => window.clearInterval(timer);
923
- }, []);
924
- const bild = szene.zeichne(Math.floor(frameTick / szene.takt) + (szene.laufend ? versatz : 0));
925
- return /* @__PURE__ */ jsx6("div", { "aria-label": label, className: "qsl-working", role: "img", children: /* @__PURE__ */ jsx6("span", { "aria-hidden": "true", className: "qsl-working__stage", children: /* @__PURE__ */ jsx6("span", { className: "qsl-working__character", children: typeof bild === "string" ? bild : bild.map((zelle, i) => /* @__PURE__ */ jsx6("span", { className: "qsl-working__cell", children: zelle }, i)) }) }, szene.id) });
746
+ ];
747
+ var COMPACT_SCENES = SZENEN.filter((scene) => scene.compact);
748
+ function WorkingScenes({ label = "Arbeitet ...", compact = false }) {
749
+ const scenes = compact ? COMPACT_SCENES : SZENEN;
750
+ const [sceneIndex, setSceneIndex] = useState2(() => Math.floor(Math.random() * scenes.length));
751
+ const [offset, setOffset] = useState2(() => Math.floor(Math.random() * 20));
752
+ const [frameTick, setFrameTick] = useState2(0);
753
+ const [reducedMotion, setReducedMotion] = useState2(() => typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches);
754
+ const scene = scenes[sceneIndex % scenes.length];
755
+ useEffect2(() => {
756
+ const preference = window.matchMedia("(prefers-reduced-motion: reduce)");
757
+ const update = () => setReducedMotion(preference.matches);
758
+ preference.addEventListener("change", update);
759
+ update();
760
+ return () => preference.removeEventListener("change", update);
761
+ }, []);
762
+ useEffect2(() => {
763
+ if (reducedMotion) return;
764
+ const timer = window.setInterval(() => {
765
+ setSceneIndex((previous) => (previous + 1 + Math.floor(Math.random() * (scenes.length - 1))) % scenes.length);
766
+ setOffset(Math.floor(Math.random() * 20));
767
+ setFrameTick(0);
768
+ }, SZENENWECHSEL_MS);
769
+ return () => window.clearInterval(timer);
770
+ }, [reducedMotion, scenes]);
771
+ useEffect2(() => {
772
+ if (reducedMotion) return;
773
+ const timer = window.setInterval(() => setFrameTick((value) => value + 1), GRUNDTAKT_MS);
774
+ return () => window.clearInterval(timer);
775
+ }, [reducedMotion]);
776
+ const frame = scene.zeichne(Math.floor(frameTick / scene.takt) + (scene.laufend ? offset : 0));
777
+ return /* @__PURE__ */ jsxs4("span", { "aria-label": label, className: cn("qsl:flex qsl:items-center qsl:gap-2.5 qsl:text-[13px]", compact ? "qsl:text-inherit" : "qsl:mt-4 qsl:text-foreground"), "data-chat": "working", role: "status", children: [
778
+ !compact && /* @__PURE__ */ jsx11("span", { "aria-hidden": "true", className: "qsl:text-muted-foreground qsl:opacity-70", children: ">" }),
779
+ /* @__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)
780
+ ] });
781
+ }
782
+
783
+ // packages/quassel/src/attachments.ts
784
+ var maxAttachmentSizeLabel = `${MAX_CHAT_ATTACHMENT_BYTES / (1024 * 1024)} MiB`;
785
+ function attachmentMediaType(file) {
786
+ return chatAttachmentMediaType(file.name, file.type);
787
+ }
788
+ function attachmentCapabilityError(files, capabilities, texts = defaultTexts) {
789
+ for (const file of files) {
790
+ const required = file.mediaType.startsWith("image/") ? "image" : file.mediaType.startsWith("video/") ? "video" : file.mediaType === "application/pdf" ? "file" : void 0;
791
+ if (required && !capabilities.input.includes(required)) {
792
+ const kind = required === "image" ? texts.attachmentKindImage : required === "video" ? texts.attachmentKindVideo : texts.attachmentKindPdf;
793
+ if (!capabilities.model) return fillText(texts.attachmentUnsupported, { name: file.name, kind });
794
+ return fillText(texts.attachmentUnsupportedByModel, { model: capabilities.model, name: file.name, kind });
795
+ }
796
+ }
797
+ }
798
+ function validateAttachmentSelection(existing, incoming, texts = defaultTexts) {
799
+ if (existing.length + incoming.length > MAX_CHAT_ATTACHMENTS) {
800
+ throw new Error(fillText(texts.tooManyAttachments, { count: MAX_CHAT_ATTACHMENTS }));
801
+ }
802
+ if ([...existing, ...incoming].reduce((total, file) => total + file.size, 0) > MAX_CHAT_ATTACHMENT_BYTES) {
803
+ throw new Error(fillText(texts.attachmentsTooLarge, { size: maxAttachmentSizeLabel }));
804
+ }
805
+ }
806
+ async function encodeAttachment(file) {
807
+ const bytes = new Uint8Array(await file.arrayBuffer());
808
+ const chunks = [];
809
+ for (let offset = 0; offset < bytes.length; offset += 8192) {
810
+ chunks.push(String.fromCharCode(...bytes.subarray(offset, offset + 8192)));
811
+ }
812
+ return { name: file.name, mediaType: attachmentMediaType(file), data: btoa(chunks.join("")) };
813
+ }
814
+ function formatAttachmentSize(bytes) {
815
+ if (bytes < 1024) return `${bytes} B`;
816
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
817
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
818
+ }
819
+ function attachmentDownloadUrl(url) {
820
+ if (/^(data|blob):/i.test(url)) return url;
821
+ const [resource, fragment] = url.split(/#(.*)/s);
822
+ return `${resource}${resource.includes("?") ? "&" : "?"}download=1${fragment === void 0 ? "" : `#${fragment}`}`;
823
+ }
824
+
825
+ // packages/quassel/src/ChatMessages.tsx
826
+ import { CheckIcon as CheckIcon4, ChevronDownIcon, ChevronRightIcon, ChevronUpIcon, LayersIcon, SparklesIcon as SparklesIcon2, WrenchIcon as WrenchIcon2 } from "lucide-react";
827
+
828
+ // packages/quassel/src/options.ts
829
+ var cssLength = (value) => typeof value === "number" ? `${value}px` : value;
830
+ var appearanceStyle = (appearance) => ({
831
+ "--qsl-chat-font-size": cssLength(appearance?.fontSize),
832
+ "--qsl-chat-line-height": appearance?.lineHeight,
833
+ "--qsl-chat-message-gap": cssLength(appearance?.messageGap),
834
+ "--qsl-chat-dense-message-gap": cssLength(appearance?.denseMessageGap)
835
+ });
836
+
837
+ // packages/quassel/src/timestamps.ts
838
+ function messageDate(at) {
839
+ if (!at) return void 0;
840
+ const date = new Date(at);
841
+ return Number.isNaN(date.getTime()) ? void 0 : date;
842
+ }
843
+ function timestampLabel(date, options = {}, now = Date.now()) {
844
+ if ((!options.format || options.format === "time") && !options.locale && !options.timeZone) {
845
+ return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
846
+ }
847
+ if (options.format === "relative") {
848
+ const seconds = (date.getTime() - now) / 1e3;
849
+ const units = [
850
+ ["year", 365 * 86400],
851
+ ["month", 30 * 86400],
852
+ ["day", 86400],
853
+ ["hour", 3600],
854
+ ["minute", 60]
855
+ ];
856
+ const [unit, divisor] = units.find(([, value]) => Math.abs(seconds) >= value) ?? ["second", 1];
857
+ return new Intl.RelativeTimeFormat(options.locale, { numeric: "auto" }).format(Math.round(seconds / divisor), unit);
858
+ }
859
+ return new Intl.DateTimeFormat(options.locale, {
860
+ timeZone: options.timeZone,
861
+ hour: "2-digit",
862
+ minute: "2-digit",
863
+ hourCycle: "h23",
864
+ ...options.format === "date-time" ? { year: "numeric", month: "2-digit", day: "2-digit" } : {}
865
+ }).format(date);
866
+ }
867
+ function timestampDay(date, options = {}) {
868
+ const key = new Intl.DateTimeFormat("en-CA", {
869
+ timeZone: options.timeZone,
870
+ year: "numeric",
871
+ month: "2-digit",
872
+ day: "2-digit"
873
+ }).format(date);
874
+ const label = new Intl.DateTimeFormat(options.locale, {
875
+ timeZone: options.timeZone,
876
+ dateStyle: "long"
877
+ }).format(date);
878
+ return { key, label };
879
+ }
880
+
881
+ // packages/quassel/src/MessageActions.tsx
882
+ import { CheckIcon as CheckIcon3, CopyIcon as CopyIcon2, PencilIcon, RotateCcwIcon } from "lucide-react";
883
+ import { useState as useState3 } from "react";
884
+ import { Fragment, jsx as jsx12, jsxs as jsxs5 } from "react/jsx-runtime";
885
+ var actionClasses = cn(
886
+ "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",
887
+ "qsl:shadow-[0_2px_6px_color-mix(in_srgb,var(--qsl-foreground)_15%,transparent)]",
888
+ "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",
889
+ "qsl:[@media(hover:none)]:pointer-events-auto qsl:[@media(hover:none)]:opacity-100"
890
+ );
891
+ function MessageActions({ message, options, texts }) {
892
+ const action = useChatAction();
893
+ const { Button: Button2 } = useQuasselComponents();
894
+ const [lastAction, setLastAction] = useState3();
895
+ const copy = typeof options?.copy === "function" ? options.copy(message) : options?.copy ?? message.role === "user";
896
+ const actions = [
897
+ ...copy && message.text.trim() ? [{ id: "copy", label: texts.copyMessage, icon: /* @__PURE__ */ jsx12(CopyIcon2, {}), onClick: () => copyChatText(message.text, texts.clipboardUnavailable) }] : [],
898
+ ...options?.edit && message.role === "user" ? [{ id: "edit", label: texts.editMessage, icon: /* @__PURE__ */ jsx12(PencilIcon, {}), onClick: options.edit }] : [],
899
+ ...options?.retry && message.role === "assistant" ? [{ id: "retry", label: texts.retryMessage, icon: /* @__PURE__ */ jsx12(RotateCcwIcon, {}), onClick: options.retry }] : [],
900
+ ...options?.custom?.(message) ?? []
901
+ ];
902
+ return /* @__PURE__ */ jsxs5(Fragment, { children: [
903
+ actions.length > 0 && /* @__PURE__ */ jsx12("div", { className: actionClasses, children: actions.map((item) => {
904
+ const copied = item.id === "copy" && lastAction === item.id && action.status === "done";
905
+ const label = copied ? texts.copied : item.label;
906
+ return /* @__PURE__ */ jsx12(
907
+ Button2,
908
+ {
909
+ "aria-label": label,
910
+ className: "qsl:text-inherit",
911
+ disabled: item.disabled || action.status === "pending",
912
+ size: item.icon ? "icon-sm" : "sm",
913
+ title: label,
914
+ variant: "ghost",
915
+ onClick: () => {
916
+ setLastAction(item.id);
917
+ void action.invoke(() => item.onClick(message), texts.actionFailed);
918
+ },
919
+ children: copied ? /* @__PURE__ */ jsx12(CheckIcon3, {}) : item.icon ?? item.label
920
+ },
921
+ item.id
922
+ );
923
+ }) }),
924
+ action.error && /* @__PURE__ */ jsx12("p", { className: "qsl:mt-2 qsl:text-sm qsl:text-destructive", role: "alert", children: action.error })
925
+ ] });
926
+ }
927
+
928
+ // packages/quassel/src/ChatSendContext.ts
929
+ import { createContext as createContext3 } from "react";
930
+ var ChatSendContext = createContext3(void 0);
931
+ function createChatSendScope(enabled) {
932
+ const listeners = /* @__PURE__ */ new Set();
933
+ return {
934
+ notify: () => {
935
+ if (enabled.current) listeners.forEach((listener) => listener());
936
+ },
937
+ subscribe: (listener) => {
938
+ listeners.add(listener);
939
+ return () => {
940
+ listeners.delete(listener);
941
+ };
942
+ }
943
+ };
944
+ }
945
+
946
+ // packages/quassel/src/announce.ts
947
+ var abstandMs = 400;
948
+ var hoechstensWartend = 5;
949
+ var region;
950
+ var wartend = [];
951
+ var laeuft = false;
952
+ function ansageRegion() {
953
+ if (region?.isConnected) {
954
+ return region;
955
+ }
956
+ const element = document.createElement("div");
957
+ element.setAttribute("role", "status");
958
+ element.setAttribute("aria-live", "polite");
959
+ element.setAttribute("aria-atomic", "true");
960
+ Object.assign(element.style, {
961
+ position: "absolute",
962
+ width: "1px",
963
+ height: "1px",
964
+ margin: "-1px",
965
+ padding: "0",
966
+ border: "0",
967
+ overflow: "hidden",
968
+ clipPath: "inset(50%)",
969
+ whiteSpace: "nowrap"
970
+ });
971
+ document.body.append(element);
972
+ region = element;
973
+ return element;
974
+ }
975
+ function naechste() {
976
+ if (wartend.length === 0) {
977
+ laeuft = false;
978
+ return;
979
+ }
980
+ if (document.visibilityState === "hidden") {
981
+ document.addEventListener("visibilitychange", naechste, { once: true });
982
+ return;
983
+ }
984
+ const [text, ...rest] = wartend;
985
+ wartend = rest;
986
+ const ziel = ansageRegion();
987
+ ziel.textContent = ziel.textContent === text ? `${text}\xA0` : text;
988
+ window.setTimeout(naechste, abstandMs);
989
+ }
990
+ function announce(text) {
991
+ const inhalt = text.trim();
992
+ if (!inhalt || typeof document === "undefined") {
993
+ return;
994
+ }
995
+ wartend = [...wartend, inhalt].slice(-hoechstensWartend);
996
+ if (laeuft) {
997
+ return;
998
+ }
999
+ laeuft = true;
1000
+ ansageRegion();
1001
+ window.setTimeout(naechste, abstandMs);
926
1002
  }
927
1003
 
928
1004
  // packages/quassel/src/ChatMessages.tsx
929
- import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1005
+ import { Fragment as Fragment2, jsx as jsx13, jsxs as jsxs6 } from "react/jsx-runtime";
1006
+ var stepClasses = "qsl:mt-[var(--qsl-chat-message-gap,16px)]";
1007
+ var denseStepClasses = "qsl:mt-[var(--qsl-chat-dense-message-gap,8px)]";
1008
+ var traceClasses = "qsl:flex qsl:w-full qsl:gap-2 qsl:pl-6 qsl:text-left qsl:text-[11px] qsl:leading-[1.375] qsl:text-muted-foreground";
1009
+ var traceButtonClasses = "qsl:cursor-pointer qsl:rounded-md qsl:py-1 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";
1010
+ var traceIconClasses = "qsl:mt-0.5 qsl:flex-none qsl:opacity-60";
1011
+ var traceErrorIconClasses = "qsl:mt-0.5 qsl:flex-none qsl:text-destructive qsl:opacity-100";
1012
+ var traceLineClasses = "qsl:min-w-0 qsl:self-baseline qsl:font-mono qsl:[overflow-wrap:anywhere]";
1013
+ var traceRunningClasses = "qsl:ml-1.5 qsl:opacity-60";
1014
+ var chipClasses = cn(
1015
+ "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",
1016
+ "qsl:font-mono qsl:text-[10.5px] qsl:leading-[1.6] qsl:text-muted-foreground qsl:transition-colors",
1017
+ "qsl:data-[state=running]:border-[color-mix(in_srgb,var(--qsl-primary)_40%,var(--qsl-border-soft))]",
1018
+ "qsl:data-[state=thinking]:border-[color-mix(in_srgb,var(--qsl-primary)_40%,var(--qsl-border-soft))]",
1019
+ "qsl:data-[state=error]:border-[color-mix(in_srgb,var(--qsl-destructive)_45%,var(--qsl-border-soft))]",
1020
+ "qsl:data-[state=done]:opacity-85"
1021
+ );
1022
+ 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";
930
1023
  var collapseAtBottomFrom = 11;
931
1024
  var hoechstensAnsagenAufEinmal = 2;
1025
+ var bubbleBodyClasses = "qsl:relative qsl:max-w-[86%] qsl:px-3 qsl:py-2 qsl:leading-[var(--qsl-chat-line-height,1.625)] qsl:[overflow-wrap:anywhere] qsl:@max-[620px]/chat:max-w-full";
932
1026
  function istSchritt(message) {
933
1027
  return !!message && (message.role === "thinking" || message.role === "tool");
934
1028
  }
@@ -937,22 +1031,25 @@ function defaultArgumentsText(tool) {
937
1031
  }
938
1032
  function ChatMessages({
939
1033
  messages,
940
- detailMode = "compact",
1034
+ owner,
1035
+ detailMode = "current",
941
1036
  running = false,
942
1037
  working,
943
- workingTimeoutMs = 2e4,
944
1038
  stepsExpandable = true,
945
1039
  texts,
946
1040
  toolArgumentsText = defaultArgumentsText,
947
1041
  renderTool,
948
- onAnswerQuestion,
1042
+ renderAction,
1043
+ onDismissAction,
949
1044
  onLinkClick,
950
- announce: announce2,
951
1045
  emptyState,
952
1046
  className,
953
1047
  showTimestamps = false,
954
- bottomThreshold = 120,
1048
+ announce: announce2,
955
1049
  scrollerRef,
1050
+ maxWidth,
1051
+ horizontalPadding,
1052
+ jumpToEndThreshold = 120,
956
1053
  appearance,
957
1054
  timestampOptions,
958
1055
  codeBlockOptions,
@@ -960,33 +1057,24 @@ function ChatMessages({
960
1057
  messageActions
961
1058
  }) {
962
1059
  const alleTexte = { ...defaultTexts, ...texts };
963
- const scrollBereich = useRef5(null);
964
- const [amEnde, setAmEnde] = useState7(true);
965
- const folgt = useRef5(amEnde);
966
- folgt.current = amEnde;
967
- const panel = useContext3(ChatSendContext);
968
- useLayoutEffect3(() => panel?.registerJump(() => {
969
- setAmEnde(true);
970
- const viewport = scrollBereich.current;
971
- if (viewport) viewport.scrollTop = viewport.scrollHeight;
972
- }), [panel]);
973
- const [now, setNow] = useState7(Date.now);
974
- const relative = showTimestamps && timestampOptions?.format === "relative";
975
- useEffect5(() => {
976
- if (!relative) return;
1060
+ const { Button: Button2 } = useQuasselComponents();
1061
+ const scrollBereich = useRef3(null);
1062
+ const threadRef = useRef3(null);
1063
+ const scrollbarDragging = useRef3(false);
1064
+ const touchY = useRef3(void 0);
1065
+ const [showJumpToEnd, setShowJumpToEnd] = useState4(false);
1066
+ const [scroll] = useState4(() => createChatScroll());
1067
+ const sendScope = useContext3(ChatSendContext);
1068
+ const [now, setNow] = useState4(() => Date.now());
1069
+ useEffect3(() => {
1070
+ if (!showTimestamps || timestampOptions?.format !== "relative") return;
977
1071
  setNow(Date.now());
978
- const timer = window.setInterval(() => setNow(Date.now()), 1e3);
1072
+ const timer = window.setInterval(() => setNow(Date.now()), 15e3);
979
1073
  return () => window.clearInterval(timer);
980
- }, [relative]);
981
- const [stromAktiv, setStromAktiv] = useState7(true);
982
- useEffect5(() => {
983
- setStromAktiv(true);
984
- const timer = window.setTimeout(() => setStromAktiv(false), workingTimeoutMs);
985
- return () => window.clearTimeout(timer);
986
- }, [messages, workingTimeoutMs]);
987
- const angesagt = useRef5(new Set(messages.map((message) => message.key)));
988
- useEffect5(() => {
989
- const faellig = messages.filter((message, index) => !angesagt.current.has(message.key) && (message.role === "question" ? message.question !== void 0 && message.question.answer === void 0 : message.role === "assistant" && message.text.trim() !== "" && (message.closed === true || !running && index === messages.length - 1)));
1074
+ }, [showTimestamps, timestampOptions?.format]);
1075
+ const angesagt = useRef3(new Set(messages.map((message) => message.key)));
1076
+ useEffect3(() => {
1077
+ const faellig = messages.filter((message, index) => !angesagt.current.has(message.key) && (message.role === "action" ? message.action !== void 0 && message.action.status === void 0 : message.role === "assistant" && message.text.trim() !== "" && (message.closed === true || !running && index === messages.length - 1)));
990
1078
  if (faellig.length === 0) {
991
1079
  return;
992
1080
  }
@@ -994,68 +1082,92 @@ function ChatMessages({
994
1082
  if (announce2 === false || faellig.length > hoechstensAnsagenAufEinmal) {
995
1083
  return;
996
1084
  }
997
- faellig.map((message) => announce2 ? announce2({ kind: message.role === "question" ? "question" : "reply", message }) : markdownPlainText(message.text)).forEach((text) => text && announce(text));
1085
+ faellig.map((message) => announce2 ? announce2({ kind: message.role === "action" ? "action" : "reply", message }) : markdownPlainText(message.text)).forEach((text) => text && announce(text));
998
1086
  }, [messages, running, announce2]);
1087
+ const updateJumpVisibility = useCallback2((viewport) => {
1088
+ if (viewport.clientHeight > 0) setShowJumpToEnd(viewport.scrollHeight - viewport.clientHeight - viewport.scrollTop > jumpToEndThreshold);
1089
+ }, [jumpToEndThreshold]);
1090
+ useLayoutEffect(() => sendScope?.subscribe(() => {
1091
+ const viewport = scrollBereich.current;
1092
+ if (!viewport) return;
1093
+ scroll.jump(viewport);
1094
+ updateJumpVisibility(viewport);
1095
+ }), [sendScope, scroll, updateJumpVisibility]);
1096
+ const pauseUp = (viewport, target) => {
1097
+ let element = target instanceof Element ? target : null;
1098
+ while (element && element !== viewport) {
1099
+ if (element.scrollTop > 0 && /auto|scroll/.test(getComputedStyle(element).overflowY)) return;
1100
+ element = element.parentElement;
1101
+ }
1102
+ scroll.pause(viewport);
1103
+ };
999
1104
  const pruefeEnde = useCallback2(() => {
1000
1105
  const bereich = scrollBereich.current;
1001
1106
  if (bereich) {
1002
- const abstandZumEnde = bereich.scrollHeight - bereich.scrollTop - bereich.clientHeight;
1003
- setAmEnde(abstandZumEnde <= bottomThreshold);
1107
+ scroll.scroll(bereich, scrollbarDragging.current);
1108
+ updateJumpVisibility(bereich);
1004
1109
  }
1005
- }, [bottomThreshold]);
1006
- useEffect5(() => {
1110
+ }, [scroll, updateJumpVisibility]);
1111
+ useEffect3(() => {
1112
+ const finishDrag = () => {
1113
+ if (scrollbarDragging.current && scrollBereich.current) scroll.scroll(scrollBereich.current, true);
1114
+ scrollbarDragging.current = false;
1115
+ };
1116
+ window.addEventListener("pointerup", finishDrag);
1117
+ window.addEventListener("pointercancel", finishDrag);
1118
+ return () => {
1119
+ window.removeEventListener("pointerup", finishDrag);
1120
+ window.removeEventListener("pointercancel", finishDrag);
1121
+ };
1122
+ }, [scroll]);
1123
+ useLayoutEffect(() => {
1007
1124
  const bereich = scrollBereich.current;
1008
- if (!bereich) {
1009
- return;
1010
- }
1011
- const beobachter = new ResizeObserver(() => {
1012
- if (folgt.current) {
1013
- bereich.scrollTop = bereich.scrollHeight;
1014
- } else {
1015
- pruefeEnde();
1016
- }
1017
- });
1125
+ if (!bereich) return;
1126
+ const align = () => {
1127
+ scroll.layout(bereich, scrollbarDragging.current);
1128
+ updateJumpVisibility(bereich);
1129
+ };
1130
+ align();
1131
+ const beobachter = new ResizeObserver(align);
1018
1132
  beobachter.observe(bereich);
1019
- for (const kind of bereich.children) {
1020
- beobachter.observe(kind);
1021
- }
1133
+ if (threadRef.current) beobachter.observe(threadRef.current, { box: "border-box" });
1022
1134
  return () => beobachter.disconnect();
1023
- }, [pruefeEnde, messages.length]);
1024
- useEffect5(() => {
1025
- if (!amEnde) {
1026
- return;
1027
- }
1028
- scrollBereich.current?.scrollTo({ top: scrollBereich.current.scrollHeight, behavior: running ? "auto" : "smooth" });
1029
- }, [messages, running, amEnde]);
1030
- const visible = useMemo(
1031
- () => messages.filter((message) => {
1032
- if ((message.role === "thinking" || message.role === "assistant") && message.text.trim() === "") {
1033
- return false;
1034
- }
1035
- if (message.role === "thinking" || message.role === "tool") {
1036
- return detailMode !== "off";
1037
- }
1038
- return true;
1039
- }),
1040
- [messages, detailMode]
1135
+ });
1136
+ const visible = useMemo3(
1137
+ () => {
1138
+ const letzterFremder = messages.reduce((last, message, index) => message.role === "user" ? last : index, -1);
1139
+ return messages.filter((message, index) => {
1140
+ if (detailMode === "current" && istSchritt(message)) {
1141
+ return running && index === letzterFremder && (message.role === "thinking" ? !message.closed : stepState(message) === "running");
1142
+ }
1143
+ if ((message.role === "thinking" || message.role === "assistant") && message.text.trim() === "" && !message.attachments?.length) {
1144
+ return false;
1145
+ }
1146
+ if (message.role === "thinking" || message.role === "tool") {
1147
+ return detailMode !== "off";
1148
+ }
1149
+ return true;
1150
+ });
1151
+ },
1152
+ [messages, detailMode, running]
1041
1153
  );
1042
- const zeitVon = (at) => {
1043
- const date = timestampDate(at);
1044
- return date ? formatTimestamp(date, timestampOptions, now) : "";
1045
- };
1046
- const mitZeit = (inhalt, key, at) => showTimestamps ? /* @__PURE__ */ jsxs6("div", { className: "qsl-line", children: [
1047
- /* @__PURE__ */ jsx7("time", { className: "qsl-time", dateTime: timestampDate(at) ? at : void 0, style: timestampOptions?.format && timestampOptions.format !== "time" ? { width: "auto", maxWidth: "35%" } : void 0, children: zeitVon(at) }),
1048
- /* @__PURE__ */ jsx7("div", { className: "qsl-line-body", children: inhalt })
1154
+ const mitZeit = (inhalt, key, at, dicht = false) => showTimestamps ? /* @__PURE__ */ jsxs6("div", { className: cn("qsl:flex qsl:items-baseline qsl:gap-2", dicht ? denseStepClasses : stepClasses), children: [
1155
+ /* @__PURE__ */ jsx13("time", { dateTime: messageDate(at)?.toISOString(), className: cn(
1156
+ "qsl:flex-none qsl:text-[10.5px] qsl:tabular-nums qsl:text-muted-foreground qsl:opacity-85",
1157
+ timestampOptions?.format && timestampOptions.format !== "time" ? "qsl:w-[clamp(60px,20%,120px)]" : "qsl:w-[34px]"
1158
+ ), children: messageDate(at) ? timestampLabel(messageDate(at), timestampOptions, now) : "" }),
1159
+ /* @__PURE__ */ jsx13("div", { className: "qsl:min-w-0 qsl:flex-1 qsl:[&>*:first-child]:mt-0", children: inhalt })
1049
1160
  ] }, `zeit-${key}`) : inhalt;
1050
- const chipModus = detailMode === "chips" || detailMode === "icons";
1161
+ const chipModus = detailMode === "current" || detailMode === "chips" || detailMode === "icons";
1051
1162
  const gruppenModus = detailMode === "grouped";
1052
1163
  const bloecke = [];
1164
+ let lastDay;
1053
1165
  let gruppe = [];
1054
1166
  const schliesseGruppe = () => {
1055
1167
  if (gruppe.length > 0) {
1056
1168
  const letzterZustand = stepState(gruppe[gruppe.length - 1]);
1057
1169
  bloecke.push(mitZeit(
1058
- gruppenModus ? /* @__PURE__ */ jsx7(
1170
+ gruppenModus ? /* @__PURE__ */ jsx13(
1059
1171
  StepGroup,
1060
1172
  {
1061
1173
  aktiv: running && (letzterZustand === "running" || letzterZustand === "thinking"),
@@ -1065,14 +1177,15 @@ function ChatMessages({
1065
1177
  toolArgumentsText
1066
1178
  },
1067
1179
  gruppe[0].key
1068
- ) : /* @__PURE__ */ jsx7(
1180
+ ) : /* @__PURE__ */ jsx13(
1069
1181
  StepRow,
1070
1182
  {
1071
1183
  expandierbar: stepsExpandable,
1072
1184
  messages: gruppe,
1073
- mitText: detailMode === "chips",
1185
+ mitText: detailMode !== "icons",
1074
1186
  texts: alleTexte,
1075
- toolArgumentsText
1187
+ toolArgumentsText,
1188
+ toolLabel: detailMode === "current" && !stepsExpandable ? alleTexte.currentTool : void 0
1076
1189
  },
1077
1190
  gruppe[0].key
1078
1191
  ),
@@ -1082,22 +1195,21 @@ function ChatMessages({
1082
1195
  gruppe = [];
1083
1196
  }
1084
1197
  };
1085
- let currentDay;
1086
1198
  visible.forEach((message, index) => {
1087
- const date = timestampOptions?.showDaySeparators ? timestampDate(message.at) : void 0;
1199
+ const date = timestampOptions?.showDaySeparators ? messageDate(message.at) : void 0;
1088
1200
  if (date) {
1089
- const day = timestampDay(date, timestampOptions?.timeZone);
1090
- if (day !== currentDay) {
1201
+ const day = timestampDay(date, timestampOptions);
1202
+ if (day.key !== lastDay) {
1091
1203
  schliesseGruppe();
1092
- bloecke.push(/* @__PURE__ */ jsx7("div", { className: "qsl-day-separator", role: "separator", "aria-label": formatDay(date, timestampOptions), children: formatDay(date, timestampOptions) }, `day-${message.key}`));
1093
- currentDay = day;
1204
+ bloecke.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}`));
1205
+ lastDay = day.key;
1094
1206
  }
1095
1207
  }
1096
- const eigeneDarstellung = message.role === "tool" && message.tool ? renderTool?.(message.tool) : void 0;
1208
+ const eigeneDarstellung = message.role === "tool" && message.tool && detailMode !== "current" ? renderTool?.(message.tool) : void 0;
1097
1209
  if (eigeneDarstellung !== void 0) {
1098
1210
  schliesseGruppe();
1099
1211
  bloecke.push(mitZeit(
1100
- /* @__PURE__ */ jsx7("div", { className: "qsl-step", children: eigeneDarstellung }, message.key),
1212
+ /* @__PURE__ */ jsx13("div", { className: stepClasses, children: eigeneDarstellung }, message.key),
1101
1213
  message.key,
1102
1214
  message.at
1103
1215
  ));
@@ -1108,77 +1220,134 @@ function ChatMessages({
1108
1220
  return;
1109
1221
  }
1110
1222
  schliesseGruppe();
1223
+ const dicht = istSchritt(message) && istSchritt(visible[index - 1]);
1111
1224
  bloecke.push(mitZeit(
1112
- /* @__PURE__ */ jsx7(
1225
+ /* @__PURE__ */ jsx13(
1113
1226
  Bubble,
1114
1227
  {
1115
1228
  detailMode,
1116
- dicht: istSchritt(message) && istSchritt(visible[index - 1]),
1229
+ dicht,
1117
1230
  expandierbar: stepsExpandable,
1118
1231
  message,
1119
- bubbleOptions,
1120
- messageActions,
1232
+ plain: owner != null && message.sender === owner,
1121
1233
  texts: alleTexte,
1122
1234
  toolArgumentsText,
1123
- onAnswerQuestion
1235
+ renderAction,
1236
+ onDismissAction,
1237
+ bubbleOptions,
1238
+ messageActions
1124
1239
  },
1125
1240
  message.key
1126
1241
  ),
1127
1242
  message.key,
1128
- message.at
1243
+ message.at,
1244
+ dicht
1129
1245
  ));
1130
1246
  });
1131
1247
  schliesseGruppe();
1132
- const verlauf = /* @__PURE__ */ jsxs6("div", { className: className ? `qsl-chat-wrap ${className}` : "qsl-chat-wrap", style: appearanceStyle(appearance), children: [
1133
- /* @__PURE__ */ jsx7(
1248
+ const verlauf = /* @__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: {
1249
+ ...appearanceStyle(appearance),
1250
+ "--qsl-chat-content-max-width": typeof maxWidth === "number" ? `${maxWidth}px` : maxWidth,
1251
+ "--qsl-chat-horizontal-padding": typeof horizontalPadding === "number" ? `${horizontalPadding}px` : horizontalPadding
1252
+ }, children: [
1253
+ /* @__PURE__ */ jsx13(
1134
1254
  "div",
1135
1255
  {
1136
- className: "qsl-chat",
1256
+ "aria-label": alleTexte.transcript,
1257
+ className: cn(
1258
+ "qsl:min-h-0 qsl:flex-1 qsl:cursor-default qsl:overflow-y-auto qsl:text-[length:var(--qsl-chat-font-size,13px)] qsl:text-foreground",
1259
+ "qsl:[--qsl-fade-edge:calc(100%_-_var(--qsl-composer-height,0px))]",
1260
+ "qsl:[-webkit-mask-image:linear-gradient(to_bottom,#000_calc(var(--qsl-fade-edge)_-_40px),transparent_calc(var(--qsl-fade-edge)_+_16px))]",
1261
+ "qsl:[mask-image:linear-gradient(to_bottom,#000_calc(var(--qsl-fade-edge)_-_40px),transparent_calc(var(--qsl-fade-edge)_+_16px))]",
1262
+ "qsl:in-data-[tone=material]:p-0"
1263
+ ),
1137
1264
  onScroll: pruefeEnde,
1265
+ onWheel: (event) => {
1266
+ if (!event.ctrlKey && event.deltaY < 0) pauseUp(event.currentTarget, event.target);
1267
+ },
1268
+ onKeyDown: (event) => {
1269
+ const editable = event.target instanceof Element && event.target.closest("input, textarea, select, [contenteditable=true]");
1270
+ if (!editable && (["ArrowUp", "PageUp", "Home"].includes(event.key) || event.target === event.currentTarget && event.key === " " && event.shiftKey)) pauseUp(event.currentTarget, event.target);
1271
+ },
1272
+ onPointerDown: (event) => {
1273
+ const viewport = event.currentTarget;
1274
+ scrollbarDragging.current = event.clientX >= viewport.getBoundingClientRect().right - Math.max(16, viewport.offsetWidth - viewport.clientWidth);
1275
+ },
1276
+ onTouchStart: (event) => {
1277
+ touchY.current = event.touches[0]?.clientY;
1278
+ },
1279
+ onTouchMove: (event) => {
1280
+ const next = event.touches[0]?.clientY;
1281
+ if (next !== void 0 && touchY.current !== void 0 && next > touchY.current) pauseUp(event.currentTarget, event.target);
1282
+ touchY.current = next;
1283
+ },
1284
+ role: "region",
1285
+ tabIndex: 0,
1138
1286
  ref: (element) => {
1139
1287
  scrollBereich.current = element;
1140
1288
  scrollerRef?.(element);
1141
1289
  },
1142
- children: visible.length === 0 && !running ? emptyState ?? null : /* @__PURE__ */ jsxs6("div", { "aria-busy": running || void 0, className: "qsl-thread", children: [
1143
- bloecke,
1144
- running && stromAktiv && (working ?? /* @__PURE__ */ jsx7(WorkingScenes, { label: alleTexte.working }))
1145
- ] })
1290
+ children: visible.length === 0 && !running ? emptyState ?? null : /* @__PURE__ */ jsxs6(
1291
+ "div",
1292
+ {
1293
+ "aria-busy": running || void 0,
1294
+ className: cn(
1295
+ "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",
1296
+ "qsl:pb-[calc(var(--qsl-composer-height,0px)_+_max(3.25em,40px))]",
1297
+ "qsl:in-data-[tone=material]:[&>*:first-child]:mt-0"
1298
+ ),
1299
+ ref: threadRef,
1300
+ children: [
1301
+ bloecke,
1302
+ running && (working ?? /* @__PURE__ */ jsx13(WorkingScenes, { label: alleTexte.working }))
1303
+ ]
1304
+ }
1305
+ )
1146
1306
  }
1147
1307
  ),
1148
- !amEnde && /* @__PURE__ */ jsx7("div", { className: "qsl-jump", children: /* @__PURE__ */ jsx7("button", { "aria-label": alleTexte.jumpToEnd, onClick: () => scrollBereich.current?.scrollTo({ top: scrollBereich.current.scrollHeight, behavior: "smooth" }), type: "button", children: /* @__PURE__ */ jsx7(IconChevronDown, {}) }) })
1308
+ 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": alleTexte.jumpToEnd, className: "qsl:pointer-events-auto qsl:rounded-full qsl:border qsl:border-primary-foreground/20 qsl:shadow-md", size: "icon", title: alleTexte.jumpToEnd, variant: "default", onClick: () => {
1309
+ if (scrollBereich.current) {
1310
+ scroll.jump(scrollBereich.current);
1311
+ updateJumpVisibility(scrollBereich.current);
1312
+ }
1313
+ }, children: /* @__PURE__ */ jsx13(ChevronDownIcon, {}) }) })
1149
1314
  ] });
1150
- return /* @__PURE__ */ jsx7(MarkdownLinks, { onLinkClick, children: /* @__PURE__ */ jsx7(MarkdownCodeBlocks, { options: codeBlockOptions, texts, children: verlauf }) });
1315
+ return /* @__PURE__ */ jsx13(MarkdownLinks, { onLinkClick, children: /* @__PURE__ */ jsx13(MarkdownCodeBlocks, { options: codeBlockOptions, texts: alleTexte, children: verlauf }) });
1151
1316
  }
1152
1317
  function StepRow({
1153
1318
  messages,
1154
1319
  mitText,
1155
1320
  expandierbar,
1156
1321
  texts,
1157
- toolArgumentsText
1322
+ toolArgumentsText,
1323
+ toolLabel
1158
1324
  }) {
1159
- const [detail, setDetail] = useState7();
1325
+ const [detail, setDetail] = useState4();
1160
1326
  const offen = expandierbar && detail && messages.find((message) => message.key === detail.key);
1161
- return /* @__PURE__ */ jsxs6("div", { className: "qsl-step qsl-steprow", children: [
1327
+ 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: [
1162
1328
  messages.map((message) => {
1163
1329
  const thinking = message.role === "thinking";
1164
1330
  const tool = message.tool;
1165
1331
  const state = stepState(message);
1166
1332
  const running = state === "running" || state === "thinking";
1167
- const label = thinking ? texts.thinkingChip : tool?.name ?? message.text;
1168
- const content = /* @__PURE__ */ jsxs6(Fragment3, { children: [
1169
- thinking ? /* @__PURE__ */ jsx7(IconSpark, { size: 11 }) : /* @__PURE__ */ jsx7(IconTool, { className: tool?.isError ? "qsl-chip__icon--error" : void 0, size: 11 }),
1170
- mitText && /* @__PURE__ */ jsx7("span", { className: "qsl-chip__label", children: label }),
1171
- mitText && /* @__PURE__ */ jsx7("span", { className: "qsl-chip__state", children: state === "done" && /* @__PURE__ */ jsx7(IconCheck, { size: 10 }) })
1333
+ const label = thinking ? texts.thinkingChip : toolLabel ?? (tool?.name || message.text || texts.toolChip);
1334
+ const content = /* @__PURE__ */ jsxs6(Fragment2, { children: [
1335
+ thinking ? /* @__PURE__ */ jsx13(SparklesIcon2, { size: 11 }) : /* @__PURE__ */ jsx13(WrenchIcon2, { className: tool?.isError ? "qsl:text-destructive" : void 0, size: 11 }),
1336
+ mitText && /* @__PURE__ */ jsx13("span", { className: "qsl:min-w-0 qsl:self-baseline qsl:overflow-hidden qsl:text-ellipsis qsl:whitespace-nowrap", children: label }),
1337
+ mitText && /* @__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 }) })
1172
1338
  ] });
1173
- const classes = `${mitText ? "" : " qsl-chip--icon"} qsl-chip--${state}${running ? " qsl-pulse" : ""}`;
1339
+ const classes = cn(chipClasses, !mitText && "qsl:p-1", running && "qsl:animate-fade-pulse qsl:motion-reduce:animate-none");
1174
1340
  if (!expandierbar) {
1175
- return /* @__PURE__ */ jsx7("span", { className: `qsl-chip qsl-chip--still${classes}`, title: label, children: content }, message.key);
1341
+ return /* @__PURE__ */ jsx13("span", { className: classes, "data-kind": thinking ? "thinking" : "tool", "data-state": state, "data-step": "chip", title: label, children: content }, message.key);
1176
1342
  }
1177
- return /* @__PURE__ */ jsx7(
1343
+ return /* @__PURE__ */ jsx13(
1178
1344
  "button",
1179
1345
  {
1180
1346
  "aria-haspopup": "dialog",
1181
- className: `qsl-chip${classes}`,
1347
+ className: cn(classes, chipInteractiveClasses),
1348
+ "data-kind": thinking ? "thinking" : "tool",
1349
+ "data-state": state,
1350
+ "data-step": "chip",
1182
1351
  onClick: (event) => setDetail({ key: message.key, position: { x: event.clientX, y: event.clientY } }),
1183
1352
  title: label,
1184
1353
  type: "button",
@@ -1187,7 +1356,7 @@ function StepRow({
1187
1356
  message.key
1188
1357
  );
1189
1358
  }),
1190
- offen && detail && /* @__PURE__ */ jsx7(
1359
+ offen && detail && /* @__PURE__ */ jsx13(
1191
1360
  StepPopover,
1192
1361
  {
1193
1362
  message: offen,
@@ -1206,10 +1375,10 @@ function StepGroup({
1206
1375
  texts,
1207
1376
  toolArgumentsText
1208
1377
  }) {
1209
- const [offen, setOffen] = useState7(false);
1210
- const kopf = useRef5(null);
1211
- const vonUntenEingeklappt = useRef5(false);
1212
- useLayoutEffect3(() => {
1378
+ const [offen, setOffen] = useState4(false);
1379
+ const kopf = useRef3(null);
1380
+ const vonUntenEingeklappt = useRef3(false);
1381
+ useLayoutEffect(() => {
1213
1382
  if (!offen && vonUntenEingeklappt.current) {
1214
1383
  vonUntenEingeklappt.current = false;
1215
1384
  kopf.current?.scrollIntoView({ block: "nearest" });
@@ -1218,22 +1387,22 @@ function StepGroup({
1218
1387
  const letzte = messages[messages.length - 1];
1219
1388
  const fehler = messages.some((message) => message.tool?.isError);
1220
1389
  const anzahl = messages.length === 1 ? texts.stepGroupOne : texts.stepGroupMany.replace("{count}", String(messages.length));
1221
- const laufend = aktiv ? letzte.role === "thinking" ? texts.thinkingChip : letzte.tool?.name ?? letzte.text : void 0;
1222
- return /* @__PURE__ */ jsxs6("div", { className: "qsl-step qsl-stepgroup", children: [
1390
+ const laufend = aktiv ? letzte.role === "thinking" ? texts.thinkingChip : letzte.tool?.name || letzte.text || texts.toolChip : void 0;
1391
+ return /* @__PURE__ */ jsxs6("div", { className: stepClasses, "data-step": "group", children: [
1223
1392
  /* @__PURE__ */ jsxs6(
1224
1393
  "button",
1225
1394
  {
1226
1395
  "aria-expanded": offen,
1227
- className: `qsl-trace qsl-trace--compact qsl-stepgroup__toggle${aktiv ? " qsl-pulse" : ""}`,
1396
+ className: cn(traceClasses, traceButtonClasses, "qsl:items-center qsl:pl-1", aktiv && "qsl:animate-fade-pulse qsl:motion-reduce:animate-none"),
1228
1397
  onClick: () => setOffen((wert) => !wert),
1229
1398
  ref: kopf,
1230
1399
  type: "button",
1231
1400
  children: [
1232
- /* @__PURE__ */ jsx7(IconChevronRight, { className: "qsl-stepgroup__chevron", size: 12 }),
1233
- /* @__PURE__ */ jsx7(IconLayers, { className: fehler ? "qsl-trace__icon qsl-trace__icon--error" : "qsl-trace__icon", size: 12 }),
1234
- /* @__PURE__ */ jsxs6("span", { className: "qsl-trace__line", children: [
1401
+ /* @__PURE__ */ jsx13(ChevronRightIcon, { className: cn("qsl:flex-none qsl:opacity-60 qsl:transition-transform", offen && "qsl:rotate-90"), size: 12 }),
1402
+ /* @__PURE__ */ jsx13(LayersIcon, { className: fehler ? traceErrorIconClasses : traceIconClasses, size: 12 }),
1403
+ /* @__PURE__ */ jsxs6("span", { className: cn(traceLineClasses, "qsl:overflow-hidden qsl:text-ellipsis qsl:whitespace-nowrap"), children: [
1235
1404
  anzahl,
1236
- laufend && /* @__PURE__ */ jsxs6("span", { className: "qsl-trace__running", children: [
1405
+ laufend && /* @__PURE__ */ jsxs6("span", { className: traceRunningClasses, children: [
1237
1406
  laufend,
1238
1407
  " ",
1239
1408
  texts.toolRunning
@@ -1242,24 +1411,23 @@ function StepGroup({
1242
1411
  ]
1243
1412
  }
1244
1413
  ),
1245
- offen && /* @__PURE__ */ jsxs6("div", { className: "qsl-stepgroup__lines", children: [
1246
- messages.map((message) => /* @__PURE__ */ jsx7(TraceLine, { expandierbar, message, texts, toolArgumentsText }, message.key)),
1247
- messages.length >= collapseAtBottomFrom && /* @__PURE__ */ jsxs6(
1248
- "button",
1249
- {
1250
- className: "qsl-trace qsl-trace--compact qsl-stepgroup__toggle qsl-stepgroup__collapse",
1251
- onClick: () => {
1252
- vonUntenEingeklappt.current = true;
1253
- setOffen(false);
1254
- },
1255
- type: "button",
1256
- children: [
1257
- /* @__PURE__ */ jsx7(IconChevronRight, { className: "qsl-stepgroup__chevron", size: 12 }),
1258
- /* @__PURE__ */ jsx7("span", { className: "qsl-trace__line", children: texts.stepGroupCollapse })
1259
- ]
1260
- }
1261
- )
1262
- ] })
1414
+ offen && /* @__PURE__ */ jsx13("div", { className: "qsl:mt-0.5 qsl:flex qsl:flex-col qsl:gap-0.5 qsl:pl-3", children: messages.map((message) => /* @__PURE__ */ jsx13(TraceLine, { className: "qsl:pl-3", expandierbar, message, texts, toolArgumentsText }, message.key)) }),
1415
+ offen && messages.length >= collapseAtBottomFrom && /* @__PURE__ */ jsxs6(
1416
+ "button",
1417
+ {
1418
+ className: cn(traceClasses, traceButtonClasses, "qsl:mt-0.5 qsl:items-center qsl:pl-1"),
1419
+ "data-step": "group-collapse",
1420
+ onClick: () => {
1421
+ vonUntenEingeklappt.current = true;
1422
+ setOffen(false);
1423
+ },
1424
+ type: "button",
1425
+ children: [
1426
+ /* @__PURE__ */ jsx13(ChevronUpIcon, { className: "qsl:flex-none qsl:opacity-60", size: 12 }),
1427
+ /* @__PURE__ */ jsx13("span", { className: traceLineClasses, children: texts.stepGroupCollapse })
1428
+ ]
1429
+ }
1430
+ )
1263
1431
  ] });
1264
1432
  }
1265
1433
  function TraceLine({
@@ -1269,28 +1437,36 @@ function TraceLine({
1269
1437
  texts,
1270
1438
  toolArgumentsText
1271
1439
  }) {
1272
- const [detailPosition, setDetailPosition] = useState7();
1273
- const classes = `${className ? `${className} ` : ""}qsl-trace qsl-trace--compact${message.role === "thinking" ? " qsl-trace--thinking" : ""}`;
1274
- const inhalt = /* @__PURE__ */ jsxs6(Fragment3, { children: [
1275
- /* @__PURE__ */ jsx7(TraceIcon, { message }),
1276
- /* @__PURE__ */ jsx7("span", { className: "qsl-trace__line", children: /* @__PURE__ */ jsx7(TraceText, { message, texts }) })
1440
+ const [detailPosition, setDetailPosition] = useState4();
1441
+ const thinking = message.role === "thinking";
1442
+ const tool = message.tool;
1443
+ const classes = cn(traceClasses, className);
1444
+ const zeile = cn(traceLineClasses, thinking && "qsl:font-sans qsl:italic qsl:whitespace-pre-wrap", "qsl:overflow-hidden qsl:text-ellipsis qsl:whitespace-nowrap");
1445
+ const inhalt = /* @__PURE__ */ jsxs6(Fragment2, { children: [
1446
+ thinking ? /* @__PURE__ */ jsx13(SparklesIcon2, { className: traceIconClasses, size: 12 }) : /* @__PURE__ */ jsx13(WrenchIcon2, { className: tool?.isError ? traceErrorIconClasses : traceIconClasses, size: 12 }),
1447
+ /* @__PURE__ */ jsxs6("span", { className: zeile, children: [
1448
+ message.text,
1449
+ tool && tool.result === void 0 && /* @__PURE__ */ jsx13("span", { className: traceRunningClasses, children: texts.toolRunning })
1450
+ ] })
1277
1451
  ] });
1452
+ const marks = { "data-compact": "true", "data-kind": thinking ? "thinking" : "tool", "data-step": "line" };
1278
1453
  if (!expandierbar) {
1279
- return /* @__PURE__ */ jsx7("div", { className: classes, children: inhalt });
1454
+ return /* @__PURE__ */ jsx13("div", { className: classes, ...marks, children: inhalt });
1280
1455
  }
1281
- return /* @__PURE__ */ jsxs6(Fragment3, { children: [
1282
- /* @__PURE__ */ jsx7(
1456
+ return /* @__PURE__ */ jsxs6(Fragment2, { children: [
1457
+ /* @__PURE__ */ jsx13(
1283
1458
  "button",
1284
1459
  {
1285
1460
  "aria-expanded": detailPosition !== void 0,
1286
1461
  "aria-haspopup": "dialog",
1287
- className: classes,
1462
+ className: cn(classes, traceButtonClasses),
1463
+ ...marks,
1288
1464
  onClick: (event) => setDetailPosition({ x: event.clientX, y: event.clientY }),
1289
1465
  type: "button",
1290
1466
  children: inhalt
1291
1467
  }
1292
1468
  ),
1293
- detailPosition && /* @__PURE__ */ jsx7(
1469
+ detailPosition && /* @__PURE__ */ jsx13(
1294
1470
  StepPopover,
1295
1471
  {
1296
1472
  message,
@@ -1302,126 +1478,161 @@ function TraceLine({
1302
1478
  )
1303
1479
  ] });
1304
1480
  }
1305
- function TraceIcon({ message }) {
1306
- return message.role === "thinking" ? /* @__PURE__ */ jsx7(IconSpark, { className: "qsl-trace__icon", size: 12 }) : /* @__PURE__ */ jsx7(IconTool, { className: message.tool?.isError ? "qsl-trace__icon qsl-trace__icon--error" : "qsl-trace__icon", size: 12 });
1307
- }
1308
- function TraceText({ message, texts }) {
1309
- return /* @__PURE__ */ jsxs6(Fragment3, { children: [
1310
- message.text,
1311
- message.tool && message.tool.result === void 0 && /* @__PURE__ */ jsx7("span", { className: "qsl-trace__running", children: texts.toolRunning })
1312
- ] });
1313
- }
1314
1481
  function Bubble({
1315
1482
  message,
1316
- bubbleOptions,
1317
- messageActions,
1483
+ plain,
1318
1484
  detailMode,
1319
1485
  dicht,
1320
1486
  expandierbar,
1321
1487
  texts,
1322
1488
  toolArgumentsText,
1323
- onAnswerQuestion
1489
+ renderAction,
1490
+ onDismissAction,
1491
+ bubbleOptions,
1492
+ messageActions
1324
1493
  }) {
1325
- const schritt = dicht ? "qsl-step qsl-step--dense" : "qsl-step";
1494
+ const schritt = dicht ? denseStepClasses : stepClasses;
1326
1495
  if (message.role === "thinking" || message.role === "tool" && message.tool) {
1327
1496
  if (detailMode === "compact") {
1328
- return /* @__PURE__ */ jsx7(TraceLine, { className: schritt, expandierbar, message, texts, toolArgumentsText });
1497
+ return /* @__PURE__ */ jsx13(TraceLine, { className: schritt, expandierbar, message, texts, toolArgumentsText });
1329
1498
  }
1499
+ const thinking = message.role === "thinking";
1330
1500
  const tool = message.tool;
1331
- return /* @__PURE__ */ jsxs6("div", { className: `${schritt} qsl-trace${message.role === "thinking" ? " qsl-trace--thinking" : ""}`, children: [
1332
- /* @__PURE__ */ jsx7(TraceIcon, { message }),
1333
- /* @__PURE__ */ jsxs6("div", { className: "qsl-trace__body", children: [
1334
- /* @__PURE__ */ jsx7("div", { className: "qsl-trace__line", children: /* @__PURE__ */ jsx7(TraceText, { message, texts }) }),
1335
- tool && /* @__PURE__ */ jsxs6("div", { className: "qsl-details", children: [
1336
- /* @__PURE__ */ jsx7("pre", { children: toolArgumentsText(tool) }),
1337
- tool.result !== void 0 && /* @__PURE__ */ jsx7("pre", { className: tool.isError ? "qsl-details__pre--error" : void 0, children: prettyJson(tool.result) })
1501
+ const icon = thinking ? /* @__PURE__ */ jsx13(SparklesIcon2, { className: traceIconClasses, size: 12 }) : /* @__PURE__ */ jsx13(WrenchIcon2, { className: tool?.isError ? traceErrorIconClasses : traceIconClasses, size: 12 });
1502
+ const zeile = /* @__PURE__ */ jsxs6(Fragment2, { children: [
1503
+ message.text,
1504
+ tool && tool.result === void 0 && /* @__PURE__ */ jsx13("span", { className: traceRunningClasses, children: texts.toolRunning })
1505
+ ] });
1506
+ return /* @__PURE__ */ jsxs6("div", { className: cn(traceClasses, schritt), "data-kind": thinking ? "thinking" : "tool", "data-step": "detail", children: [
1507
+ icon,
1508
+ /* @__PURE__ */ jsxs6("div", { className: "qsl:min-w-0 qsl:flex-1", children: [
1509
+ /* @__PURE__ */ jsx13("div", { className: cn(traceLineClasses, thinking && "qsl:font-sans qsl:italic qsl:whitespace-pre-wrap"), children: zeile }),
1510
+ tool && /* @__PURE__ */ jsxs6("div", { className: "qsl:mt-0.5 qsl:border-l-2 qsl:border-border-soft qsl:pl-2", children: [
1511
+ /* @__PURE__ */ jsx13("pre", { className: "qsl:mb-0.5 qsl:max-h-[208px] qsl:overflow-auto qsl:font-mono qsl:text-[10.5px] qsl:leading-[1.375] qsl:break-all qsl:whitespace-pre-wrap qsl:opacity-80", children: toolArgumentsText(tool) }),
1512
+ tool.result !== void 0 && /* @__PURE__ */ jsx13("pre", { className: cn(
1513
+ "qsl:mb-0.5 qsl:max-h-[208px] qsl:overflow-auto qsl:font-mono qsl:text-[10.5px] qsl:leading-[1.375] qsl:break-all qsl:whitespace-pre-wrap qsl:opacity-80",
1514
+ tool.isError && "qsl:text-destructive qsl:opacity-100"
1515
+ ), children: prettyJson(tool.result) })
1338
1516
  ] })
1339
1517
  ] })
1340
1518
  ] });
1341
1519
  }
1342
- if (message.role === "question" && message.question) {
1343
- return /* @__PURE__ */ jsx7("div", { className: schritt, children: /* @__PURE__ */ jsx7(
1344
- QuestionCard,
1520
+ if (message.role === "action" && message.action) {
1521
+ const action = message.action;
1522
+ return /* @__PURE__ */ jsx13("div", { className: schritt, children: renderAction?.(action, message.text) ?? /* @__PURE__ */ jsx13(
1523
+ PendingActionCard,
1345
1524
  {
1346
- question: message.question,
1525
+ action,
1526
+ dismissedLabel: texts.actionDismissed,
1527
+ dismissLabel: texts.dismissAction,
1528
+ onDismiss: onDismissAction ? () => onDismissAction(action.actionId) : void 0,
1347
1529
  text: message.text,
1348
- texts,
1349
- onAnswer: (text) => onAnswerQuestion?.(message.question.callId, text)
1530
+ waitingLabel: texts.pendingAction
1350
1531
  }
1351
1532
  ) });
1352
1533
  }
1353
1534
  if (message.role === "system") {
1354
- return /* @__PURE__ */ jsx7("div", { className: `${schritt} qsl-system`, children: /* @__PURE__ */ jsx7(Markdown, { text: message.text }) });
1535
+ return /* @__PURE__ */ jsxs6("div", { className: cn(schritt, "qsl:border-l-2 qsl:border-destructive/50 qsl:pl-3 qsl:text-sm qsl:text-muted-foreground"), "data-message": "system", children: [
1536
+ /* @__PURE__ */ jsx13(Markdown, { text: message.text }),
1537
+ /* @__PURE__ */ jsx13(MessageAttachments, { label: texts.attachments, message })
1538
+ ] });
1355
1539
  }
1356
- const content = message.text.trim();
1357
- if (!content && message.role !== "user") return null;
1358
- const plain = bubbleOptions?.variant === "plain";
1359
- const isBubble = !plain && (message.role === "user" || message.bubble || bubbleOptions?.variant === "bubbles");
1540
+ if (!message.text.trim() && !message.attachments?.length) return null;
1541
+ const variant = bubbleOptions?.variant ?? "default";
1542
+ const useBubble = !plain && variant !== "plain" && (variant === "bubbles" || message.role === "user" || !!message.bubble);
1360
1543
  const side = (message.role === "user" ? bubbleOptions?.userSide : bubbleOptions?.assistantSide) ?? message.bubble?.side ?? (message.role === "user" ? "end" : "start");
1361
- const label = bubbleOptions?.showSender === false ? void 0 : bubbleOptions?.senderLabel?.(message) ?? message.bubble?.label ?? (bubbleOptions?.showSender ? message.role : void 0);
1362
- const body = /* @__PURE__ */ jsxs6(Fragment3, { children: [
1363
- label && /* @__PURE__ */ jsx7("span", { className: "qsl-bubble__label", children: label }),
1364
- /* @__PURE__ */ jsx7(Markdown, { text: message.role === "user" ? message.text : content }),
1365
- /* @__PURE__ */ jsx7(MessageActions, { message, options: messageActions, texts })
1544
+ const sender = bubbleOptions?.senderLabel?.(message) ?? message.bubble?.label ?? message.sender;
1545
+ const showSender = bubbleOptions?.showSender ?? (!!message.bubble?.label && !plain);
1546
+ const content = /* @__PURE__ */ jsxs6(Fragment2, { children: [
1547
+ showSender && sender && /* @__PURE__ */ jsx13("span", { className: cn(
1548
+ "qsl:mb-[5px] qsl:table qsl:text-[0.72rem] qsl:leading-[1.45] qsl:font-semibold",
1549
+ 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]"
1550
+ ), "data-chat": "sender", children: sender }),
1551
+ /* @__PURE__ */ jsx13(Markdown, { text: message.text, streaming: message.role === "assistant" && !message.closed }),
1552
+ /* @__PURE__ */ jsx13(MessageAttachments, { label: texts.attachments, message }),
1553
+ message.steered && /* @__PURE__ */ jsx13("small", { className: "qsl:mt-1 qsl:block qsl:text-[0.72rem] qsl:leading-[1.45] qsl:opacity-70", "data-chat": "steered", children: texts.steered }),
1554
+ /* @__PURE__ */ jsx13(MessageActions, { message, options: messageActions, texts })
1366
1555
  ] });
1367
- if (isBubble) {
1368
- const user = message.role === "user" && !message.bubble;
1369
- return /* @__PURE__ */ jsx7("div", { className: `${schritt} ${user ? "qsl-user" : "qsl-bubble"}`, style: { justifyContent: side === "end" ? "flex-end" : "flex-start" }, children: /* @__PURE__ */ jsx7("div", { style: { background: message.bubble?.color ?? (user ? void 0 : "var(--qsl-accent)"), maxWidth: bubbleOptions?.maxWidth }, children: body }) });
1370
- }
1371
- return /* @__PURE__ */ jsx7("div", { className: `${schritt} qsl-answer`, children: body });
1556
+ const cursor = message.role === "assistant" && message.textCursor ? JSON.stringify(message.textCursor) : void 0;
1557
+ if (!useBubble) return /* @__PURE__ */ jsx13("div", { "data-chat-text-cursor": cursor, className: cn(schritt, "qsl:group qsl:relative qsl:w-full qsl:leading-[var(--qsl-chat-line-height,1.625)] qsl:text-foreground"), "data-message": "answer", children: content });
1558
+ return /* @__PURE__ */ jsx13(
1559
+ "div",
1560
+ {
1561
+ "data-chat-text-cursor": cursor,
1562
+ className: cn(schritt, "qsl:flex", side === "end" ? "qsl:justify-end" : "qsl:justify-start"),
1563
+ "data-message": message.role === "user" && !message.bubble && variant === "default" ? "user" : "bubble",
1564
+ "data-side": side,
1565
+ children: /* @__PURE__ */ jsx13(
1566
+ "div",
1567
+ {
1568
+ className: cn(bubbleBodyClasses, "qsl:group", message.bubble ? "qsl:rounded-lg qsl:text-white qsl:[--qsl-scroll-cover:var(--qsl-primary)]" : "qsl:rounded-[10px_10px_4px_10px] qsl:border qsl:border-border qsl:bg-secondary qsl:text-foreground qsl:[--qsl-scroll-cover:var(--qsl-secondary)] qsl:in-data-[tone=material]:rounded-[9px] qsl:in-data-[tone=material]:border-glass-edge/20 qsl:in-data-[tone=material]:bg-primary/15 qsl:in-data-[tone=material]:text-foreground"),
1569
+ "data-tone": message.bubble ? "on-color" : void 0,
1570
+ style: { background: message.bubble?.color, maxWidth: bubbleOptions?.maxWidth },
1571
+ children: content
1572
+ }
1573
+ )
1574
+ }
1575
+ );
1576
+ }
1577
+ function MessageAttachments({ message, label }) {
1578
+ if (!message.attachments?.length) return null;
1579
+ 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: [
1580
+ 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" }) }),
1581
+ 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 }),
1582
+ /* @__PURE__ */ jsxs6("div", { className: "qsl:flex qsl:flex-col qsl:gap-1 qsl:p-2 qsl:[overflow-wrap:anywhere]", children: [
1583
+ /* @__PURE__ */ jsx13("a", { className: "qsl:font-medium", href: attachmentDownloadUrl(attachment.url), download: attachment.name, children: attachment.name }),
1584
+ /* @__PURE__ */ jsx13("small", { children: formatAttachmentSize(attachment.size) })
1585
+ ] })
1586
+ ] }, `${attachment.url}-${index}`)) });
1372
1587
  }
1373
1588
 
1374
1589
  // packages/quassel/src/ChatPanel.tsx
1375
- import { useLayoutEffect as useLayoutEffect4, useMemo as useMemo2, useRef as useRef6 } from "react";
1376
- import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1590
+ import { useEffect as useEffect4, useLayoutEffect as useLayoutEffect2, useRef as useRef4, useState as useState5 } from "react";
1591
+ import { jsx as jsx14, jsxs as jsxs7 } from "react/jsx-runtime";
1377
1592
  function ChatPanel({
1378
1593
  children,
1379
1594
  composer,
1380
1595
  className,
1381
1596
  nodeRef,
1597
+ maxWidth,
1598
+ horizontalPadding,
1382
1599
  appearance,
1383
1600
  scrollOnSend = false
1384
1601
  }) {
1385
- const jump = useRef6(void 0);
1386
- const scrollOnSendRef = useRef6(scrollOnSend);
1387
- useLayoutEffect4(() => {
1602
+ const wurzel = useRef4(null);
1603
+ const eingabe = useRef4(null);
1604
+ const hasComposer = composer !== void 0;
1605
+ const scrollOnSendRef = useRef4(scrollOnSend);
1606
+ const [sendScope] = useState5(() => createChatSendScope(scrollOnSendRef));
1607
+ useLayoutEffect2(() => {
1388
1608
  scrollOnSendRef.current = scrollOnSend;
1389
1609
  }, [scrollOnSend]);
1390
- const sendContext = useMemo2(() => ({
1391
- onSent: () => {
1392
- if (scrollOnSendRef.current) jump.current?.();
1393
- },
1394
- registerJump: (callback) => {
1395
- jump.current = callback;
1396
- return () => {
1397
- if (jump.current === callback) jump.current = void 0;
1398
- };
1399
- }
1400
- }), []);
1401
- const wurzel = useRef6(null);
1402
- const eingabe = useRef6(null);
1403
- const mitEingabe = composer !== void 0;
1404
- useLayoutEffect4(() => {
1610
+ useEffect4(() => {
1405
1611
  const element = eingabe.current;
1406
1612
  const rahmen = wurzel.current;
1407
- if (!rahmen) {
1408
- return;
1409
- }
1410
- if (!element) {
1411
- rahmen.style.removeProperty("--qsl-composer-height");
1613
+ if (!element || !rahmen) {
1412
1614
  return;
1413
1615
  }
1414
1616
  const messen = () => rahmen.style.setProperty("--qsl-composer-height", `${element.offsetHeight}px`);
1415
1617
  const beobachter = new ResizeObserver(messen);
1416
1618
  beobachter.observe(element);
1417
1619
  messen();
1418
- return () => beobachter.disconnect();
1419
- }, [mitEingabe]);
1420
- return /* @__PURE__ */ jsx8(ChatSendContext.Provider, { value: sendContext, children: /* @__PURE__ */ jsxs7(
1620
+ return () => {
1621
+ beobachter.disconnect();
1622
+ rahmen.style.removeProperty("--qsl-composer-height");
1623
+ };
1624
+ }, [hasComposer]);
1625
+ return /* @__PURE__ */ jsx14(ChatSendContext.Provider, { value: sendScope, children: /* @__PURE__ */ jsxs7(
1421
1626
  "div",
1422
1627
  {
1423
- className: className ? `qsl-chat-panel ${className}` : "qsl-chat-panel",
1424
- style: appearanceStyle(appearance),
1628
+ className: cn("qsl:relative qsl:flex qsl:min-h-0 qsl:min-w-0 qsl:flex-col qsl:overflow-hidden", className),
1629
+ "data-chat": "panel",
1630
+ "data-quassel": "",
1631
+ style: {
1632
+ ...appearanceStyle(appearance),
1633
+ "--qsl-chat-content-max-width": typeof maxWidth === "number" ? `${maxWidth}px` : maxWidth,
1634
+ "--qsl-chat-horizontal-padding": typeof horizontalPadding === "number" ? `${horizontalPadding}px` : horizontalPadding
1635
+ },
1425
1636
  ref: (element) => {
1426
1637
  wurzel.current = element;
1427
1638
  if (typeof nodeRef === "function") {
@@ -1432,83 +1643,290 @@ function ChatPanel({
1432
1643
  },
1433
1644
  children: [
1434
1645
  children,
1435
- composer !== void 0 && /* @__PURE__ */ jsx8("div", { className: "qsl-chat-panel__composer", ref: eingabe, children: composer })
1646
+ hasComposer && /* @__PURE__ */ jsx14("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: eingabe, children: composer })
1436
1647
  ]
1437
1648
  }
1438
1649
  ) });
1439
1650
  }
1440
1651
 
1441
- // packages/quassel/src/TimestampSwitch.tsx
1442
- import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1443
- function TimestampSwitch({
1444
- showTimestamps,
1445
- onChange,
1446
- collapsible = true,
1447
- texts,
1448
- className
1449
- }) {
1450
- const label = texts?.timestamps ?? defaultTexts.timestamps;
1451
- const classes = showTimestamps ? "qsl-icon-button qsl-icon-button--active" : "qsl-icon-button";
1452
- return /* @__PURE__ */ jsxs8(
1453
- "button",
1454
- {
1455
- "aria-label": label,
1456
- "aria-pressed": showTimestamps,
1457
- className: className ? `${classes} ${className}` : classes,
1458
- onClick: () => onChange(!showTimestamps),
1459
- title: label,
1460
- type: "button",
1461
- children: [
1462
- /* @__PURE__ */ jsx9(IconClock, {}),
1463
- /* @__PURE__ */ jsx9("span", { className: collapsible ? "qsl-collapsible" : void 0, children: label })
1464
- ]
1652
+ // packages/quassel/src/ChatInputToolbar.tsx
1653
+ import { PaperclipIcon, SendIcon, XIcon as XIcon2 } from "lucide-react";
1654
+
1655
+ // packages/quassel/src/ui/useFileInput.ts
1656
+ import { useRef as useRef5, useState as useState6 } from "react";
1657
+ function useFileInput({ disabled, onFiles, onPasteText }) {
1658
+ const inputRef = useRef5(null);
1659
+ const [dragging, setDragging] = useState6(false);
1660
+ const receive = (files) => {
1661
+ if (files.length) void onFiles(files);
1662
+ };
1663
+ return {
1664
+ inputRef,
1665
+ dragging,
1666
+ clearDragging: () => setDragging(false),
1667
+ inputProps: {
1668
+ ref: inputRef,
1669
+ onChange: (event) => {
1670
+ const files = Array.from(event.currentTarget.files ?? []);
1671
+ event.currentTarget.value = "";
1672
+ receive(files);
1673
+ }
1674
+ },
1675
+ dropProps: {
1676
+ onDragOver: (event) => {
1677
+ if (!event.dataTransfer.types.includes("Files")) return;
1678
+ event.preventDefault();
1679
+ event.dataTransfer.dropEffect = disabled ? "none" : "copy";
1680
+ setDragging(!disabled);
1681
+ },
1682
+ onDragLeave: (event) => {
1683
+ if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
1684
+ },
1685
+ onDrop: (event) => {
1686
+ if (!event.dataTransfer.types.includes("Files")) return;
1687
+ event.preventDefault();
1688
+ setDragging(false);
1689
+ receive(Array.from(event.dataTransfer.files));
1690
+ },
1691
+ onPaste: (event) => {
1692
+ const files = Array.from(event.clipboardData.files);
1693
+ if (!files.length) return;
1694
+ event.preventDefault();
1695
+ receive(files);
1696
+ const text = event.clipboardData.getData("text/plain");
1697
+ if (text) onPasteText?.(text);
1698
+ }
1465
1699
  }
1466
- );
1700
+ };
1467
1701
  }
1468
1702
 
1469
1703
  // packages/quassel/src/ChatInputToolbar.tsx
1470
- import { useCallback as useCallback3, useEffect as useEffect6, useRef as useRef7, useState as useState8 } from "react";
1471
- import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1704
+ import { useCallback as useCallback3, useContext as useContext4, useEffect as useEffect5, useImperativeHandle, useLayoutEffect as useLayoutEffect3, useRef as useRef6, useState as useState7 } from "react";
1705
+ import { createPortal } from "react-dom";
1706
+ import { Fragment as Fragment3, jsx as jsx15, jsxs as jsxs8 } from "react/jsx-runtime";
1472
1707
  var COMPACT_WIDTH_PX = 480;
1708
+ var errorClasses = "qsl:px-3 qsl:py-2 qsl:text-sm qsl:text-destructive";
1709
+ 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";
1710
+ var attachmentMediaClasses = "qsl:block qsl:h-[112px] qsl:w-full qsl:bg-secondary qsl:object-contain";
1473
1711
  function ChatInputToolbar({
1474
1712
  onSend,
1475
1713
  onStop,
1476
1714
  running = false,
1477
1715
  disabled = false,
1716
+ sendDisabled = false,
1717
+ layout = "card",
1718
+ detailsContainer,
1719
+ onErrorChange,
1720
+ inputAriaControls,
1478
1721
  rows = 3,
1479
1722
  maxRows,
1480
1723
  actions,
1481
1724
  toolbarLeft,
1482
1725
  toolbarRight,
1483
1726
  texts,
1484
- sendShortcut = "enter"
1727
+ handleRef,
1728
+ attachments: attachmentsEnabled = true,
1729
+ attachmentCapabilities,
1730
+ attachmentCapabilitiesError,
1731
+ onAttachmentsChange,
1732
+ initialValue = "",
1733
+ onDraftChange,
1734
+ sendShortcut = "enter",
1735
+ autoFocus = false,
1736
+ onAutoFocusSettled
1485
1737
  }) {
1738
+ const sendScope = useContext4(ChatSendContext);
1486
1739
  const alleTexte = { ...defaultTexts, ...texts };
1487
- const [draft, setDraft] = useState8("");
1488
- const { submit, sending, error } = useChatSubmit(onSend);
1489
- const rootRef = useRef7(null);
1490
- const eingabeRef = useRef7(null);
1491
- const [compact, setCompact] = useState8(false);
1740
+ const { Button: Button2, StopButton: StopButton2, Toggle: Toggle2 } = useQuasselComponents();
1741
+ const [draft, setDraft] = useState7(initialValue);
1742
+ const [sending, setSending] = useState7(false);
1743
+ const [sendError, setSendError] = useState7();
1744
+ const [failedSubmissions, setFailedSubmissions] = useState7([]);
1745
+ const nextFailedId = useRef6(0);
1746
+ const pendingSend = useRef6(false);
1747
+ const generation = useRef6(0);
1748
+ useLayoutEffect3(() => () => {
1749
+ generation.current += 1;
1750
+ }, []);
1751
+ const draftRevision = useRef6(0);
1752
+ const [attachments, setAttachments] = useState7([]);
1753
+ const attachmentsRef = useRef6([]);
1754
+ const nextAttachmentId = useRef6(0);
1755
+ const [attachmentError, setAttachmentError] = useState7();
1756
+ const updateAttachments = (update) => {
1757
+ attachmentsRef.current = update(attachmentsRef.current);
1758
+ setAttachments(attachmentsRef.current);
1759
+ };
1760
+ const addFiles = (files) => {
1761
+ if (!files.length) return;
1762
+ if (disabled || pendingSend.current) {
1763
+ setAttachmentError(alleTexte.attachmentsWhileBusy);
1764
+ return;
1765
+ }
1766
+ try {
1767
+ validateAttachmentSelection(attachmentsRef.current.map((entry) => entry.file), files, alleTexte);
1768
+ } catch (error) {
1769
+ setAttachmentError(error instanceof Error ? error.message : String(error));
1770
+ return;
1771
+ }
1772
+ setAttachmentError(void 0);
1773
+ const entries = files.map((file) => ({ id: ++nextAttachmentId.current, file, status: "reading" }));
1774
+ updateAttachments((current) => [...current, ...entries]);
1775
+ for (const entry of entries) {
1776
+ void encodeAttachment(entry.file).then(
1777
+ (input2) => updateAttachments((current) => current.map((item) => item.id === entry.id ? { ...entry, status: "ready", input: input2 } : item)),
1778
+ () => updateAttachments((current) => current.map((item) => item.id === entry.id ? {
1779
+ ...entry,
1780
+ status: "error",
1781
+ error: alleTexte.attachmentReadFailed
1782
+ } : item))
1783
+ );
1784
+ }
1785
+ };
1786
+ const rootRef = useRef6(null);
1787
+ const eingabeRef = useRef6(null);
1788
+ const autoFocusSettled = useRef6(false);
1789
+ useEffect5(() => {
1790
+ if (!autoFocus || autoFocusSettled.current) return;
1791
+ const input2 = eingabeRef.current;
1792
+ if (!input2) return;
1793
+ let frame = 0;
1794
+ const settle = () => {
1795
+ if (autoFocusSettled.current) return;
1796
+ autoFocusSettled.current = true;
1797
+ window.cancelAnimationFrame(frame);
1798
+ onAutoFocusSettled?.();
1799
+ };
1800
+ const onFocus = (event) => {
1801
+ if (event.target !== input2) settle();
1802
+ };
1803
+ const focusWhenVisible = () => {
1804
+ if (autoFocusSettled.current) return;
1805
+ const dialog = [...document.querySelectorAll('[role="dialog"], dialog[open]')].find((element) => element.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true }) && !element.contains(input2));
1806
+ if (dialog) {
1807
+ settle();
1808
+ return;
1809
+ }
1810
+ const rect = input2.getBoundingClientRect();
1811
+ 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;
1812
+ if (visible && !input2.disabled && !input2.readOnly && document.visibilityState === "visible") {
1813
+ input2.focus({ preventScroll: true });
1814
+ if (document.activeElement === input2) {
1815
+ settle();
1816
+ return;
1817
+ }
1818
+ }
1819
+ frame = window.requestAnimationFrame(focusWhenVisible);
1820
+ };
1821
+ window.addEventListener("pointerdown", settle, true);
1822
+ window.addEventListener("keydown", settle, true);
1823
+ window.addEventListener("focusin", onFocus);
1824
+ window.addEventListener("blur", settle);
1825
+ if (!disabled) frame = window.requestAnimationFrame(focusWhenVisible);
1826
+ return () => {
1827
+ window.cancelAnimationFrame(frame);
1828
+ window.removeEventListener("pointerdown", settle, true);
1829
+ window.removeEventListener("keydown", settle, true);
1830
+ window.removeEventListener("focusin", onFocus);
1831
+ window.removeEventListener("blur", settle);
1832
+ };
1833
+ }, [autoFocus, disabled, onAutoFocusSettled]);
1834
+ const [compact, setCompact] = useState7(false);
1835
+ const pendingFocus = useRef6(false);
1836
+ const fileInput = useFileInput({
1837
+ disabled: disabled || sending,
1838
+ onFiles: addFiles,
1839
+ onPasteText: (text) => {
1840
+ const input2 = eingabeRef.current;
1841
+ if (!input2 || disabled || pendingSend.current) return;
1842
+ const start = input2.selectionStart;
1843
+ const end = input2.selectionEnd;
1844
+ draftRevision.current += 1;
1845
+ setDraft((current) => current.slice(0, start) + text + current.slice(end));
1846
+ pendingFocus.current = true;
1847
+ }
1848
+ });
1849
+ const toolbar = layout === "toolbar";
1850
+ const inline = layout === "inline";
1851
+ const inputRows = inline || toolbar ? 1 : rows;
1852
+ const inputMaxRows = inline || toolbar ? 1 : maxRows;
1853
+ const rootClasses = cn(
1854
+ "qsl:flex qsl:shrink-0 qsl:flex-col qsl:overflow-hidden qsl:rounded-[var(--qsl-input-card-radius,var(--qsl-radius-xl))] qsl:border qsl:border-border-strong qsl:bg-background qsl:shadow-bar qsl:transition-colors qsl:focus-within:border-primary",
1855
+ "qsl:in-data-[tone=material]:rounded-xl qsl:in-data-[tone=material]:border-glass-edge/50 qsl:in-data-[tone=material]:bg-background",
1856
+ 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",
1857
+ fileInput.dragging && "qsl:outline-2 qsl:outline-offset-[3px] qsl:outline-dashed qsl:outline-primary"
1858
+ );
1859
+ const textareaClasses = cn(
1860
+ "qsl:resize-none qsl:border-0 qsl:bg-transparent qsl:text-[length:var(--qsl-chat-font-size,13px)] qsl:leading-[var(--qsl-chat-line-height,1.5)] qsl:text-foreground qsl:placeholder:text-muted-foreground qsl:focus:outline-none",
1861
+ 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"
1862
+ );
1863
+ const toolbarRowClasses = cn(
1864
+ "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",
1865
+ inline && "qsl:flex-none qsl:gap-1 qsl:border-t-0 qsl:p-1",
1866
+ toolbar && "qsl:flex-wrap qsl:border-t-0"
1867
+ );
1868
+ const toolbarSideClasses = "qsl:flex qsl:min-w-0 qsl:items-center qsl:gap-2 qsl:in-data-[tone=overseer-details]:flex-nowrap";
1869
+ const changeDraft = useCallback3((text) => {
1870
+ draftRevision.current += 1;
1871
+ setDraft(text);
1872
+ }, []);
1873
+ useImperativeHandle(handleRef, () => ({
1874
+ insert: (text) => {
1875
+ pendingFocus.current = true;
1876
+ changeDraft(text);
1877
+ const element = eingabeRef.current;
1878
+ element?.focus();
1879
+ if (element?.value === text) element.setSelectionRange(text.length, text.length);
1880
+ },
1881
+ focus: () => eingabeRef.current?.focus(),
1882
+ submit: (onSubmit, options) => send(onSubmit, options?.allowEmpty),
1883
+ reset: () => {
1884
+ generation.current += 1;
1885
+ pendingSend.current = false;
1886
+ pendingFocus.current = false;
1887
+ changeDraft("");
1888
+ updateAttachments(() => []);
1889
+ setSendError(void 0);
1890
+ setFailedSubmissions([]);
1891
+ setAttachmentError(void 0);
1892
+ setSending(false);
1893
+ fileInput.clearDragging();
1894
+ }
1895
+ }));
1896
+ useEffect5(() => {
1897
+ onDraftChange?.(draft);
1898
+ }, [draft, onDraftChange]);
1899
+ useEffect5(() => {
1900
+ if (!pendingFocus.current) {
1901
+ return;
1902
+ }
1903
+ pendingFocus.current = false;
1904
+ const element = eingabeRef.current;
1905
+ if (element) {
1906
+ element.focus();
1907
+ element.setSelectionRange(element.value.length, element.value.length);
1908
+ }
1909
+ }, [draft, sending]);
1492
1910
  const applyAutoGrow = useCallback3(() => {
1493
1911
  const element = eingabeRef.current;
1494
- if (!element || maxRows === void 0) {
1912
+ if (!element || inputMaxRows === void 0) {
1495
1913
  return;
1496
1914
  }
1497
1915
  const stil = getComputedStyle(element);
1498
1916
  const zeilenhoehe = Number.parseFloat(stil.lineHeight) || Number.parseFloat(stil.fontSize) * 1.4;
1499
1917
  const polster = Number.parseFloat(stil.paddingTop) + Number.parseFloat(stil.paddingBottom);
1500
- const maximum = zeilenhoehe * Math.max(maxRows, rows) + polster;
1918
+ const maximum = zeilenhoehe * Math.max(inputMaxRows, inputRows) + polster;
1501
1919
  element.style.height = "auto";
1502
1920
  const inhalt = element.scrollHeight;
1503
1921
  element.style.height = `${Math.min(inhalt, maximum)}px`;
1504
1922
  element.style.overflowY = inhalt > maximum ? "auto" : "hidden";
1505
- }, [maxRows, rows]);
1506
- useEffect6(() => {
1923
+ }, [inputMaxRows, inputRows]);
1924
+ useEffect5(() => {
1507
1925
  applyAutoGrow();
1508
1926
  }, [draft, applyAutoGrow]);
1509
- useEffect6(() => {
1927
+ useEffect5(() => {
1510
1928
  const element = eingabeRef.current;
1511
- if (!element || maxRows === void 0) {
1929
+ if (!element || inputMaxRows === void 0) {
1512
1930
  return;
1513
1931
  }
1514
1932
  let lastWidth = element.clientWidth;
@@ -1520,8 +1938,8 @@ function ChatInputToolbar({
1520
1938
  });
1521
1939
  observer.observe(element);
1522
1940
  return () => observer.disconnect();
1523
- }, [maxRows, applyAutoGrow]);
1524
- useEffect6(() => {
1941
+ }, [inputMaxRows, applyAutoGrow]);
1942
+ useEffect5(() => {
1525
1943
  const element = rootRef.current;
1526
1944
  if (!element) {
1527
1945
  return;
@@ -1532,79 +1950,245 @@ function ChatInputToolbar({
1532
1950
  beobachter.observe(element);
1533
1951
  return () => beobachter.disconnect();
1534
1952
  }, []);
1535
- const hatText = draft.trim().length > 0;
1536
- const send = async () => {
1953
+ const hasAttachments = attachments.length > 0;
1954
+ useEffect5(() => {
1955
+ onAttachmentsChange?.(hasAttachments);
1956
+ }, [hasAttachments, onAttachmentsChange]);
1957
+ const capabilityError = hasAttachments && attachmentCapabilitiesError ? attachmentCapabilitiesError : attachmentCapabilities ? attachmentCapabilityError(attachments.map((entry) => ({
1958
+ name: entry.file.name,
1959
+ mediaType: attachmentMediaType(entry.file)
1960
+ })), attachmentCapabilities, alleTexte) : void 0;
1961
+ const hasContent = draft.trim().length > 0 || attachments.length > 0;
1962
+ const preparing = attachments.some((entry) => entry.status === "reading");
1963
+ const attachmentBlocked = attachments.some((entry) => entry.status !== "ready");
1964
+ const currentError = sendError ?? attachmentError ?? capabilityError ?? attachments.find((entry) => entry.status === "error")?.error;
1965
+ useEffect5(() => {
1966
+ onErrorChange?.(currentError);
1967
+ }, [currentError, onErrorChange]);
1968
+ const send = async (onSubmit = onSend, allowEmpty = false) => {
1537
1969
  const text = draft.trim();
1538
- if (!text || disabled || sending) {
1970
+ const snapshot = attachmentsRef.current;
1971
+ if (!allowEmpty && !text && !snapshot.length || disabled || sendDisabled || pendingSend.current || capabilityError || snapshot.some((entry) => entry.status !== "ready")) {
1539
1972
  return;
1540
1973
  }
1541
- if (await submit(text)) setDraft((current) => current === draft ? "" : current);
1974
+ pendingSend.current = true;
1975
+ const sentGeneration = generation.current;
1976
+ const sentRevision = draftRevision.current;
1977
+ const sentDraft = draft;
1978
+ const focusOrigin = document.activeElement;
1979
+ const startedFocused = focusOrigin === eingabeRef.current || rootRef.current?.contains(focusOrigin);
1980
+ let leftComposer = false;
1981
+ const observeFocus = (event) => {
1982
+ const target = event.target;
1983
+ if (target instanceof Node && !rootRef.current?.contains(target) && !detailsContainer?.contains(target)) leftComposer = true;
1984
+ };
1985
+ document.addEventListener("focusin", observeFocus);
1986
+ document.addEventListener("pointerdown", observeFocus);
1987
+ setSending(true);
1988
+ setSendError(void 0);
1989
+ setDraft("");
1990
+ updateAttachments(() => []);
1991
+ try {
1992
+ const inputs = snapshot.flatMap((entry) => entry.status === "ready" ? [entry.input] : []);
1993
+ await onSubmit(text, inputs.length ? inputs : void 0);
1994
+ if (generation.current === sentGeneration && (text || inputs.length)) sendScope?.notify();
1995
+ } catch (error) {
1996
+ if (generation.current !== sentGeneration) return;
1997
+ if (draftRevision.current === sentRevision) {
1998
+ setDraft(sentDraft);
1999
+ updateAttachments((current) => [...snapshot, ...current]);
2000
+ } else if (sentDraft || snapshot.length) {
2001
+ const failed = { id: ++nextFailedId.current, draft: sentDraft, attachments: snapshot };
2002
+ setFailedSubmissions((current) => [...current, failed]);
2003
+ }
2004
+ setSendError(error instanceof Error ? error.message : String(error));
2005
+ } finally {
2006
+ document.removeEventListener("focusin", observeFocus);
2007
+ document.removeEventListener("pointerdown", observeFocus);
2008
+ if (generation.current === sentGeneration) {
2009
+ pendingSend.current = false;
2010
+ pendingFocus.current = !!startedFocused && !leftComposer && document.hasFocus() && (!toolbar || focusOrigin !== eingabeRef.current) && (document.activeElement === focusOrigin || !toolbar && document.activeElement === document.body);
2011
+ setSending(false);
2012
+ }
2013
+ }
1542
2014
  };
1543
2015
  const onKeyDown = (event) => {
1544
- if (isSendKey(event, sendShortcut)) {
2016
+ if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing && event.nativeEvent.keyCode !== 229 && (sendShortcut === "enter" || event.metaKey || event.ctrlKey)) {
1545
2017
  event.preventDefault();
1546
2018
  if (!event.repeat) void send();
1547
2019
  }
1548
2020
  };
1549
- return /* @__PURE__ */ jsxs9("div", { className: compact ? "qsl-input-card qsl-input-card--compact" : "qsl-input-card", ref: rootRef, children: [
1550
- error && /* @__PURE__ */ jsx10("p", { className: "qsl-action-error", role: "alert", children: error }),
1551
- /* @__PURE__ */ jsx10(
1552
- "textarea",
2021
+ 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: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": alleTexte.attachments, children: attachments.map((entry) => /* @__PURE__ */ jsxs8("li", { className: attachmentItemClasses, children: [
2022
+ 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 }),
2023
+ 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}` }),
2024
+ /* @__PURE__ */ jsxs8("div", { className: "qsl:flex qsl:flex-col qsl:gap-1 qsl:p-2 qsl:pr-8 qsl:[overflow-wrap:anywhere]", children: [
2025
+ /* @__PURE__ */ jsx15("span", { className: "qsl:font-medium", title: entry.file.name, children: entry.file.name }),
2026
+ /* @__PURE__ */ jsx15("small", { children: formatAttachmentSize(entry.file.size) }),
2027
+ entry.status === "reading" && /* @__PURE__ */ jsx15("small", { role: "status", children: alleTexte.attachmentPreparing }),
2028
+ entry.status === "error" && /* @__PURE__ */ jsx15("small", { role: "alert", children: entry.error })
2029
+ ] }),
2030
+ /* @__PURE__ */ jsx15(
2031
+ Button2,
1553
2032
  {
1554
- "aria-label": alleTexte.placeholder,
1555
- disabled,
1556
- onChange: (event) => setDraft(event.target.value),
1557
- onKeyDown,
1558
- placeholder: running ? alleTexte.steeringPlaceholder : alleTexte.placeholder,
1559
- ref: eingabeRef,
1560
- rows,
1561
- value: draft
2033
+ "aria-label": fillText(alleTexte.removeAttachment, { name: entry.file.name }),
2034
+ className: "qsl:absolute qsl:top-1 qsl:right-1",
2035
+ disabled: disabled || sending,
2036
+ onClick: () => {
2037
+ updateAttachments((current) => current.filter((item) => item.id !== entry.id));
2038
+ setAttachmentError(void 0);
2039
+ eingabeRef.current?.focus();
2040
+ },
2041
+ size: "icon-sm",
2042
+ title: fillText(alleTexte.removeAttachment, { name: entry.file.name }),
2043
+ variant: "outline",
2044
+ children: /* @__PURE__ */ jsx15(XIcon2, {})
1562
2045
  }
1563
- ),
1564
- /* @__PURE__ */ jsxs9("div", { className: "qsl-input-card__toolbar", children: [
1565
- /* @__PURE__ */ jsxs9("div", { className: "qsl-input-card__left", children: [
1566
- actions?.map((action, index) => /* @__PURE__ */ jsxs9(
1567
- "button",
1568
- {
1569
- className: action.active ? "qsl-icon-button qsl-icon-button--active" : "qsl-icon-button",
1570
- disabled: action.disabled,
1571
- onClick: action.onClick,
1572
- title: action.title ?? action.label,
1573
- type: "button",
1574
- children: [
1575
- action.icon,
1576
- action.label && /* @__PURE__ */ jsx10("span", { className: action.icon ? "qsl-collapsible" : void 0, children: action.label })
1577
- ]
1578
- },
1579
- index
1580
- )),
1581
- toolbarLeft
2046
+ )
2047
+ ] }, entry.id)) });
2048
+ const errors = /* @__PURE__ */ jsxs8(Fragment3, { children: [
2049
+ capabilityError && /* @__PURE__ */ jsx15("p", { className: errorClasses, role: "alert", children: capabilityError }),
2050
+ attachmentError && /* @__PURE__ */ jsx15("p", { className: errorClasses, role: "alert", children: attachmentError }),
2051
+ sendError && /* @__PURE__ */ jsx15("p", { className: errorClasses, role: "alert", children: sendError }),
2052
+ failedSubmissions.map((failed) => /* @__PURE__ */ jsxs8("div", { className: cn(errorClasses, "qsl:flex qsl:flex-wrap qsl:items-center qsl:gap-2"), role: "status", children: [
2053
+ /* @__PURE__ */ jsxs8("span", { children: [
2054
+ alleTexte.notSent,
2055
+ " ",
2056
+ failed.draft.slice(0, 100),
2057
+ failed.draft.length > 100 ? " ..." : "",
2058
+ failed.attachments.length > 0 ? ` ${fillText(alleTexte.notSentAttachments, { count: failed.attachments.length })}` : "",
2059
+ " "
1582
2060
  ] }),
1583
- /* @__PURE__ */ jsxs9("div", { className: "qsl-input-card__right", children: [
1584
- toolbarRight,
1585
- running && onStop && /* @__PURE__ */ jsx10("button", { "aria-label": alleTexte.stop, className: "qsl-send qsl-send--stop", onClick: onStop, title: alleTexte.stop, type: "button", children: /* @__PURE__ */ jsx10(IconStop, {}) }),
1586
- (!running || !onStop || hatText) && /* @__PURE__ */ jsx10(
1587
- "button",
1588
- {
1589
- "aria-label": alleTexte.send,
1590
- className: "qsl-send",
1591
- disabled: disabled || sending || !hatText,
1592
- onClick: () => void send(),
1593
- title: running ? alleTexte.sendIntoRun : alleTexte.send,
1594
- type: "button",
1595
- children: /* @__PURE__ */ jsx10(IconSend, {})
1596
- }
1597
- )
1598
- ] })
2061
+ /* @__PURE__ */ jsx15(Button2, { disabled: disabled || sending, size: "sm", variant: "outline", onClick: () => {
2062
+ try {
2063
+ validateAttachmentSelection(attachmentsRef.current.map((entry) => entry.file), failed.attachments.map((entry) => entry.file), alleTexte);
2064
+ } catch (error) {
2065
+ setAttachmentError(error instanceof Error ? error.message : String(error));
2066
+ return;
2067
+ }
2068
+ draftRevision.current += 1;
2069
+ setDraft((current) => [current, failed.draft].filter(Boolean).join("\n\n"));
2070
+ updateAttachments((current) => [...current, ...failed.attachments]);
2071
+ setFailedSubmissions((current) => current.filter((entry) => entry.id !== failed.id));
2072
+ setAttachmentError(void 0);
2073
+ pendingFocus.current = true;
2074
+ }, children: alleTexte.insertUnsent })
2075
+ ] }, failed.id))
2076
+ ] });
2077
+ const sendButtons = /* @__PURE__ */ jsxs8(Fragment3, { children: [
2078
+ running && onStop && !hasContent && /* @__PURE__ */ jsx15(StopButton2, { className: "qsl:flex-none", label: alleTexte.stop, onClick: onStop, size: "icon-sm" }),
2079
+ (!running || !onStop || hasContent) && /* @__PURE__ */ jsx15(
2080
+ Button2,
2081
+ {
2082
+ "aria-label": alleTexte.send,
2083
+ className: "qsl:flex-none",
2084
+ disabled: disabled || sendDisabled || sending || attachmentBlocked || !!capabilityError || !hasContent,
2085
+ onClick: () => {
2086
+ void send();
2087
+ },
2088
+ size: "icon-sm",
2089
+ title: running ? alleTexte.sendIntoRun : alleTexte.send,
2090
+ children: /* @__PURE__ */ jsx15(SendIcon, { size: 14 })
2091
+ }
2092
+ )
2093
+ ] });
2094
+ const detailToolbar = /* @__PURE__ */ jsxs8("div", { className: toolbarRowClasses, "data-input": "controls", children: [
2095
+ /* @__PURE__ */ jsxs8("div", { className: cn(toolbarSideClasses, "qsl:in-data-[tone=overseer-details]:flex-none", inline || toolbar ? "qsl:flex-none" : "qsl:flex-auto"), children: [
2096
+ attachmentsEnabled && /* @__PURE__ */ jsx15(
2097
+ Button2,
2098
+ {
2099
+ "aria-label": alleTexte.attachFiles,
2100
+ disabled: disabled || sending,
2101
+ onClick: () => fileInput.inputRef.current?.click(),
2102
+ size: "icon-sm",
2103
+ title: fillText(alleTexte.attachFilesHint, { count: MAX_CHAT_ATTACHMENTS, size: maxAttachmentSizeLabel }),
2104
+ variant: "ghost",
2105
+ children: /* @__PURE__ */ jsx15(PaperclipIcon, {})
2106
+ }
2107
+ ),
2108
+ preparing && /* @__PURE__ */ jsx15("span", { className: cn("qsl:text-xs qsl:text-muted-foreground", inline && "qsl:sr-only"), role: "status", children: alleTexte.attachmentsPreparing }),
2109
+ 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: [
2110
+ action.icon,
2111
+ action.label && /* @__PURE__ */ jsx15("span", { className: action.icon ? "qsl:in-data-[compact=true]:hidden" : void 0, children: action.label })
2112
+ ] }, index) : /* @__PURE__ */ jsxs8(Toggle2, { disabled: action.disabled, onPressedChange: () => action.onClick(), pressed: action.active, size: "sm", title: action.title ?? action.label, children: [
2113
+ action.icon,
2114
+ action.label && /* @__PURE__ */ jsx15("span", { className: action.icon ? "qsl:in-data-[compact=true]:hidden" : void 0, children: action.label })
2115
+ ] }, index)),
2116
+ toolbarLeft
2117
+ ] }),
2118
+ /* @__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: [
2119
+ toolbarRight,
2120
+ !toolbar && sendButtons
1599
2121
  ] })
1600
2122
  ] });
2123
+ const details = /* @__PURE__ */ jsxs8("div", { className: "qsl:min-w-0", "data-compact": compact, "data-quassel": "", children: [
2124
+ attachmentPreviews,
2125
+ errors,
2126
+ detailToolbar
2127
+ ] });
2128
+ const input = /* @__PURE__ */ jsx15(
2129
+ "textarea",
2130
+ {
2131
+ "aria-controls": inputAriaControls,
2132
+ className: textareaClasses,
2133
+ "aria-label": alleTexte.placeholder,
2134
+ disabled: !toolbar && disabled,
2135
+ readOnly: toolbar && disabled,
2136
+ onChange: (event) => changeDraft(event.target.value),
2137
+ onKeyDown,
2138
+ placeholder: running ? alleTexte.steeringPlaceholder : alleTexte.placeholder,
2139
+ ref: eingabeRef,
2140
+ rows: inputRows,
2141
+ value: draft
2142
+ }
2143
+ );
2144
+ return /* @__PURE__ */ jsxs8(
2145
+ "div",
2146
+ {
2147
+ className: rootClasses,
2148
+ "data-compact": compact,
2149
+ "data-layout": layout,
2150
+ "data-quassel": "",
2151
+ "data-working": running || sending,
2152
+ ref: rootRef,
2153
+ ...attachmentsEnabled ? fileInput.dropProps : {},
2154
+ children: [
2155
+ attachmentsEnabled && /* @__PURE__ */ jsx15(
2156
+ "input",
2157
+ {
2158
+ "aria-label": alleTexte.attachFiles,
2159
+ hidden: true,
2160
+ multiple: true,
2161
+ type: "file",
2162
+ ...fileInput.inputProps
2163
+ }
2164
+ ),
2165
+ !toolbar && attachmentPreviews,
2166
+ inline ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
2167
+ errors,
2168
+ /* @__PURE__ */ jsxs8("div", { className: "qsl:flex qsl:min-w-0 qsl:items-center", children: [
2169
+ input,
2170
+ detailToolbar
2171
+ ] })
2172
+ ] }) : input,
2173
+ toolbar ? /* @__PURE__ */ jsxs8(Fragment3, { children: [
2174
+ /* @__PURE__ */ jsx15("div", { className: "qsl:flex qsl:flex-none qsl:items-center qsl:gap-1 qsl:p-1", children: sendButtons }),
2175
+ detailsContainer && createPortal(details, detailsContainer)
2176
+ ] }) : !inline && /* @__PURE__ */ jsxs8(Fragment3, { children: [
2177
+ errors,
2178
+ detailToolbar
2179
+ ] })
2180
+ ]
2181
+ }
2182
+ );
1601
2183
  }
1602
2184
 
1603
2185
  // packages/quassel/src/DetailModeSwitch.tsx
1604
- import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
1605
- var DETAIL_MODES = ["off", "icons", "chips", "grouped", "compact", "full"];
2186
+ import { SparklesIcon as SparklesIcon3 } from "lucide-react";
2187
+ import { jsx as jsx16, jsxs as jsxs9 } from "react/jsx-runtime";
2188
+ var DETAIL_MODES = ["off", "current", "icons", "chips", "grouped", "compact", "full"];
1606
2189
  var TEXT_KEYS = {
1607
2190
  off: "detailModeOff",
2191
+ current: "detailModeCurrent",
1608
2192
  icons: "detailModeIcons",
1609
2193
  chips: "detailModeChips",
1610
2194
  grouped: "detailModeGrouped",
@@ -1621,19 +2205,46 @@ function DetailModeSwitch({
1621
2205
  className
1622
2206
  }) {
1623
2207
  const alleTexte = { ...defaultTexts, ...texts };
2208
+ const { Button: Button2 } = useQuasselComponents();
1624
2209
  const aktuell = modes.indexOf(mode);
1625
2210
  const naechster = modes[(aktuell < 0 ? 0 : aktuell + 1) % modes.length];
1626
- return /* @__PURE__ */ jsxs10(
1627
- "button",
2211
+ return /* @__PURE__ */ jsxs9(
2212
+ Button2,
1628
2213
  {
1629
2214
  "aria-label": `${alleTexte.detailModeTitle}: ${detailModeLabel(mode, texts)}`,
1630
- className: className ? `qsl-icon-button ${className}` : "qsl-icon-button",
2215
+ className,
1631
2216
  onClick: () => onChange(naechster),
2217
+ size: "sm",
1632
2218
  title: `${alleTexte.detailModeTitle}: ${detailModeLabel(mode, texts)}`,
1633
- type: "button",
2219
+ variant: "ghost",
2220
+ children: [
2221
+ /* @__PURE__ */ jsx16(SparklesIcon3, {}),
2222
+ /* @__PURE__ */ jsx16("span", { className: collapsible ? "qsl:in-data-[compact=true]:hidden" : void 0, children: detailModeLabel(mode, texts) })
2223
+ ]
2224
+ }
2225
+ );
2226
+ }
2227
+
2228
+ // packages/quassel/src/TimestampSwitch.tsx
2229
+ import { ClockIcon } from "lucide-react";
2230
+ import { jsx as jsx17, jsxs as jsxs10 } from "react/jsx-runtime";
2231
+ function TimestampSwitch({ showTimestamps, onChange, collapsible = true, texts, className }) {
2232
+ const labels = { ...defaultTexts, ...texts };
2233
+ const { Button: Button2 } = useQuasselComponents();
2234
+ const title = showTimestamps ? labels.hideTimestamps : labels.showTimestamps;
2235
+ return /* @__PURE__ */ jsxs10(
2236
+ Button2,
2237
+ {
2238
+ "aria-label": title,
2239
+ "aria-pressed": showTimestamps,
2240
+ className,
2241
+ onClick: () => onChange(!showTimestamps),
2242
+ size: "sm",
2243
+ title,
2244
+ variant: "ghost",
1634
2245
  children: [
1635
- /* @__PURE__ */ jsx11(IconSpark, { size: 14 }),
1636
- /* @__PURE__ */ jsx11("span", { className: collapsible ? "qsl-collapsible" : void 0, children: detailModeLabel(mode, texts) })
2246
+ /* @__PURE__ */ jsx17(ClockIcon, {}),
2247
+ /* @__PURE__ */ jsx17("span", { className: collapsible ? "qsl:in-data-[compact=true]:hidden" : void 0, children: labels.timestamps })
1637
2248
  ]
1638
2249
  }
1639
2250
  );
@@ -1642,28 +2253,44 @@ export {
1642
2253
  ChatInputToolbar,
1643
2254
  ChatMessages,
1644
2255
  ChatPanel,
2256
+ ChatSendContext,
1645
2257
  DETAIL_MODES,
1646
2258
  DetailModeSwitch,
1647
- IconCheck,
1648
- IconChevronDown,
1649
- IconChevronRight,
1650
- IconClock,
1651
- IconLayers,
1652
- IconSend,
1653
- IconSpark,
1654
- IconStop,
1655
- IconTool,
1656
- IconX,
2259
+ MAX_CHAT_ATTACHMENTS,
2260
+ MAX_CHAT_ATTACHMENT_BYTES,
1657
2261
  Markdown,
2262
+ MarkdownCodeBlocks,
2263
+ MarkdownLinks,
2264
+ MessageActions,
2265
+ PendingActionCard,
2266
+ QuasselProvider,
2267
+ StepPopover,
1658
2268
  TimestampSwitch,
2269
+ WorkingScenes,
1659
2270
  announce,
2271
+ appearanceStyle,
1660
2272
  applyEvent,
2273
+ attachmentCapabilityError,
2274
+ attachmentDownloadUrl,
2275
+ attachmentMediaType,
1661
2276
  compactToolLine,
2277
+ copyChatText,
2278
+ createChatScroll,
2279
+ createChatSendScope,
2280
+ defaultComponents,
1662
2281
  defaultTexts,
1663
2282
  detailModeLabel,
2283
+ encodeAttachment,
2284
+ fillText,
2285
+ formatAttachmentSize,
1664
2286
  markdownPlainText,
2287
+ messageDate,
1665
2288
  prettyJson,
1666
2289
  stepState,
1667
- useChat
2290
+ timestampDay,
2291
+ timestampLabel,
2292
+ useChatAction,
2293
+ useQuasselComponents,
2294
+ validateAttachmentSelection
1668
2295
  };
1669
2296
  //# sourceMappingURL=index.js.map