quassel 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 -980
  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: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,31 +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 panel = useContext3(ChatSendContext);
966
- useLayoutEffect3(() => panel?.registerJump(() => {
967
- setAmEnde(true);
968
- const viewport = scrollBereich.current;
969
- if (viewport) viewport.scrollTop = viewport.scrollHeight;
970
- }), [panel]);
971
- const [now, setNow] = useState7(Date.now);
972
- const relative = showTimestamps && timestampOptions?.format === "relative";
973
- useEffect5(() => {
974
- 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;
975
1071
  setNow(Date.now());
976
- const timer = window.setInterval(() => setNow(Date.now()), 1e3);
1072
+ const timer = window.setInterval(() => setNow(Date.now()), 15e3);
977
1073
  return () => window.clearInterval(timer);
978
- }, [relative]);
979
- const [stromAktiv, setStromAktiv] = useState7(true);
980
- useEffect5(() => {
981
- setStromAktiv(true);
982
- const timer = window.setTimeout(() => setStromAktiv(false), workingTimeoutMs);
983
- return () => window.clearTimeout(timer);
984
- }, [messages, workingTimeoutMs]);
985
- const angesagt = useRef5(new Set(messages.map((message) => message.key)));
986
- useEffect5(() => {
987
- 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)));
988
1078
  if (faellig.length === 0) {
989
1079
  return;
990
1080
  }
@@ -992,62 +1082,92 @@ function ChatMessages({
992
1082
  if (announce2 === false || faellig.length > hoechstensAnsagenAufEinmal) {
993
1083
  return;
994
1084
  }
995
- 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));
996
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
+ };
997
1104
  const pruefeEnde = useCallback2(() => {
998
1105
  const bereich = scrollBereich.current;
999
1106
  if (bereich) {
1000
- const abstandZumEnde = bereich.scrollHeight - bereich.scrollTop - bereich.clientHeight;
1001
- setAmEnde(abstandZumEnde <= bottomThreshold);
1107
+ scroll.scroll(bereich, scrollbarDragging.current);
1108
+ updateJumpVisibility(bereich);
1002
1109
  }
1003
- }, [bottomThreshold]);
1004
- 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(() => {
1005
1124
  const bereich = scrollBereich.current;
1006
- if (!bereich) {
1007
- return;
1008
- }
1009
- const beobachter = new ResizeObserver(pruefeEnde);
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);
1010
1132
  beobachter.observe(bereich);
1011
- for (const kind of bereich.children) {
1012
- beobachter.observe(kind);
1013
- }
1133
+ if (threadRef.current) beobachter.observe(threadRef.current, { box: "border-box" });
1014
1134
  return () => beobachter.disconnect();
1015
- }, [pruefeEnde, messages.length]);
1016
- useEffect5(() => {
1017
- if (!amEnde) {
1018
- return;
1019
- }
1020
- scrollBereich.current?.scrollTo({ top: scrollBereich.current.scrollHeight, behavior: running ? "auto" : "smooth" });
1021
- }, [messages, running, amEnde]);
1022
- const visible = useMemo(
1023
- () => messages.filter((message) => {
1024
- if ((message.role === "thinking" || message.role === "assistant") && message.text.trim() === "") {
1025
- return false;
1026
- }
1027
- if (message.role === "thinking" || message.role === "tool") {
1028
- return detailMode !== "off";
1029
- }
1030
- return true;
1031
- }),
1032
- [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]
1033
1153
  );
1034
- const zeitVon = (at) => {
1035
- const date = timestampDate(at);
1036
- return date ? formatTimestamp(date, timestampOptions, now) : "";
1037
- };
1038
- const mitZeit = (inhalt, key, at) => showTimestamps ? /* @__PURE__ */ jsxs6("div", { className: "qsl-line", children: [
1039
- /* @__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) }),
1040
- /* @__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 })
1041
1160
  ] }, `zeit-${key}`) : inhalt;
1042
- const chipModus = detailMode === "chips" || detailMode === "icons";
1161
+ const chipModus = detailMode === "current" || detailMode === "chips" || detailMode === "icons";
1043
1162
  const gruppenModus = detailMode === "grouped";
1044
1163
  const bloecke = [];
1164
+ let lastDay;
1045
1165
  let gruppe = [];
1046
1166
  const schliesseGruppe = () => {
1047
1167
  if (gruppe.length > 0) {
1048
1168
  const letzterZustand = stepState(gruppe[gruppe.length - 1]);
1049
1169
  bloecke.push(mitZeit(
1050
- gruppenModus ? /* @__PURE__ */ jsx7(
1170
+ gruppenModus ? /* @__PURE__ */ jsx13(
1051
1171
  StepGroup,
1052
1172
  {
1053
1173
  aktiv: running && (letzterZustand === "running" || letzterZustand === "thinking"),
@@ -1057,14 +1177,15 @@ function ChatMessages({
1057
1177
  toolArgumentsText
1058
1178
  },
1059
1179
  gruppe[0].key
1060
- ) : /* @__PURE__ */ jsx7(
1180
+ ) : /* @__PURE__ */ jsx13(
1061
1181
  StepRow,
1062
1182
  {
1063
1183
  expandierbar: stepsExpandable,
1064
1184
  messages: gruppe,
1065
- mitText: detailMode === "chips",
1185
+ mitText: detailMode !== "icons",
1066
1186
  texts: alleTexte,
1067
- toolArgumentsText
1187
+ toolArgumentsText,
1188
+ toolLabel: detailMode === "current" && !stepsExpandable ? alleTexte.currentTool : void 0
1068
1189
  },
1069
1190
  gruppe[0].key
1070
1191
  ),
@@ -1074,22 +1195,21 @@ function ChatMessages({
1074
1195
  gruppe = [];
1075
1196
  }
1076
1197
  };
1077
- let currentDay;
1078
1198
  visible.forEach((message, index) => {
1079
- const date = timestampOptions?.showDaySeparators ? timestampDate(message.at) : void 0;
1199
+ const date = timestampOptions?.showDaySeparators ? messageDate(message.at) : void 0;
1080
1200
  if (date) {
1081
- const day = timestampDay(date, timestampOptions?.timeZone);
1082
- if (day !== currentDay) {
1201
+ const day = timestampDay(date, timestampOptions);
1202
+ if (day.key !== lastDay) {
1083
1203
  schliesseGruppe();
1084
- bloecke.push(/* @__PURE__ */ jsx7("div", { className: "qsl-day-separator", role: "separator", "aria-label": formatDay(date, timestampOptions), children: formatDay(date, timestampOptions) }, `day-${message.key}`));
1085
- 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;
1086
1206
  }
1087
1207
  }
1088
- 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;
1089
1209
  if (eigeneDarstellung !== void 0) {
1090
1210
  schliesseGruppe();
1091
1211
  bloecke.push(mitZeit(
1092
- /* @__PURE__ */ jsx7("div", { className: "qsl-step", children: eigeneDarstellung }, message.key),
1212
+ /* @__PURE__ */ jsx13("div", { className: stepClasses, children: eigeneDarstellung }, message.key),
1093
1213
  message.key,
1094
1214
  message.at
1095
1215
  ));
@@ -1100,77 +1220,134 @@ function ChatMessages({
1100
1220
  return;
1101
1221
  }
1102
1222
  schliesseGruppe();
1223
+ const dicht = istSchritt(message) && istSchritt(visible[index - 1]);
1103
1224
  bloecke.push(mitZeit(
1104
- /* @__PURE__ */ jsx7(
1225
+ /* @__PURE__ */ jsx13(
1105
1226
  Bubble,
1106
1227
  {
1107
1228
  detailMode,
1108
- dicht: istSchritt(message) && istSchritt(visible[index - 1]),
1229
+ dicht,
1109
1230
  expandierbar: stepsExpandable,
1110
1231
  message,
1111
- bubbleOptions,
1112
- messageActions,
1232
+ plain: owner != null && message.sender === owner,
1113
1233
  texts: alleTexte,
1114
1234
  toolArgumentsText,
1115
- onAnswerQuestion
1235
+ renderAction,
1236
+ onDismissAction,
1237
+ bubbleOptions,
1238
+ messageActions
1116
1239
  },
1117
1240
  message.key
1118
1241
  ),
1119
1242
  message.key,
1120
- message.at
1243
+ message.at,
1244
+ dicht
1121
1245
  ));
1122
1246
  });
1123
1247
  schliesseGruppe();
1124
- const verlauf = /* @__PURE__ */ jsxs6("div", { className: className ? `qsl-chat-wrap ${className}` : "qsl-chat-wrap", style: appearanceStyle(appearance), children: [
1125
- /* @__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(
1126
1254
  "div",
1127
1255
  {
1128
- 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
+ ),
1129
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,
1130
1286
  ref: (element) => {
1131
1287
  scrollBereich.current = element;
1132
1288
  scrollerRef?.(element);
1133
1289
  },
1134
- children: visible.length === 0 && !running ? emptyState ?? null : /* @__PURE__ */ jsxs6("div", { "aria-busy": running || void 0, className: "qsl-thread", children: [
1135
- bloecke,
1136
- running && stromAktiv && (working ?? /* @__PURE__ */ jsx7(WorkingScenes, { label: alleTexte.working }))
1137
- ] })
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
+ )
1138
1306
  }
1139
1307
  ),
1140
- !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, {}) }) })
1141
1314
  ] });
1142
- 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 }) });
1143
1316
  }
1144
1317
  function StepRow({
1145
1318
  messages,
1146
1319
  mitText,
1147
1320
  expandierbar,
1148
1321
  texts,
1149
- toolArgumentsText
1322
+ toolArgumentsText,
1323
+ toolLabel
1150
1324
  }) {
1151
- const [detail, setDetail] = useState7();
1325
+ const [detail, setDetail] = useState4();
1152
1326
  const offen = expandierbar && detail && messages.find((message) => message.key === detail.key);
1153
- 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: [
1154
1328
  messages.map((message) => {
1155
1329
  const thinking = message.role === "thinking";
1156
1330
  const tool = message.tool;
1157
1331
  const state = stepState(message);
1158
1332
  const running = state === "running" || state === "thinking";
1159
- const label = thinking ? texts.thinkingChip : tool?.name ?? message.text;
1160
- const content = /* @__PURE__ */ jsxs6(Fragment3, { children: [
1161
- thinking ? /* @__PURE__ */ jsx7(IconSpark, { size: 11 }) : /* @__PURE__ */ jsx7(IconTool, { className: tool?.isError ? "qsl-chip__icon--error" : void 0, size: 11 }),
1162
- mitText && /* @__PURE__ */ jsx7("span", { className: "qsl-chip__label", children: label }),
1163
- 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 }) })
1164
1338
  ] });
1165
- 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");
1166
1340
  if (!expandierbar) {
1167
- 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);
1168
1342
  }
1169
- return /* @__PURE__ */ jsx7(
1343
+ return /* @__PURE__ */ jsx13(
1170
1344
  "button",
1171
1345
  {
1172
1346
  "aria-haspopup": "dialog",
1173
- className: `qsl-chip${classes}`,
1347
+ className: cn(classes, chipInteractiveClasses),
1348
+ "data-kind": thinking ? "thinking" : "tool",
1349
+ "data-state": state,
1350
+ "data-step": "chip",
1174
1351
  onClick: (event) => setDetail({ key: message.key, position: { x: event.clientX, y: event.clientY } }),
1175
1352
  title: label,
1176
1353
  type: "button",
@@ -1179,7 +1356,7 @@ function StepRow({
1179
1356
  message.key
1180
1357
  );
1181
1358
  }),
1182
- offen && detail && /* @__PURE__ */ jsx7(
1359
+ offen && detail && /* @__PURE__ */ jsx13(
1183
1360
  StepPopover,
1184
1361
  {
1185
1362
  message: offen,
@@ -1198,10 +1375,10 @@ function StepGroup({
1198
1375
  texts,
1199
1376
  toolArgumentsText
1200
1377
  }) {
1201
- const [offen, setOffen] = useState7(false);
1202
- const kopf = useRef5(null);
1203
- const vonUntenEingeklappt = useRef5(false);
1204
- useLayoutEffect3(() => {
1378
+ const [offen, setOffen] = useState4(false);
1379
+ const kopf = useRef3(null);
1380
+ const vonUntenEingeklappt = useRef3(false);
1381
+ useLayoutEffect(() => {
1205
1382
  if (!offen && vonUntenEingeklappt.current) {
1206
1383
  vonUntenEingeklappt.current = false;
1207
1384
  kopf.current?.scrollIntoView({ block: "nearest" });
@@ -1210,22 +1387,22 @@ function StepGroup({
1210
1387
  const letzte = messages[messages.length - 1];
1211
1388
  const fehler = messages.some((message) => message.tool?.isError);
1212
1389
  const anzahl = messages.length === 1 ? texts.stepGroupOne : texts.stepGroupMany.replace("{count}", String(messages.length));
1213
- const laufend = aktiv ? letzte.role === "thinking" ? texts.thinkingChip : letzte.tool?.name ?? letzte.text : void 0;
1214
- 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: [
1215
1392
  /* @__PURE__ */ jsxs6(
1216
1393
  "button",
1217
1394
  {
1218
1395
  "aria-expanded": offen,
1219
- 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"),
1220
1397
  onClick: () => setOffen((wert) => !wert),
1221
1398
  ref: kopf,
1222
1399
  type: "button",
1223
1400
  children: [
1224
- /* @__PURE__ */ jsx7(IconChevronRight, { className: "qsl-stepgroup__chevron", size: 12 }),
1225
- /* @__PURE__ */ jsx7(IconLayers, { className: fehler ? "qsl-trace__icon qsl-trace__icon--error" : "qsl-trace__icon", size: 12 }),
1226
- /* @__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: [
1227
1404
  anzahl,
1228
- laufend && /* @__PURE__ */ jsxs6("span", { className: "qsl-trace__running", children: [
1405
+ laufend && /* @__PURE__ */ jsxs6("span", { className: traceRunningClasses, children: [
1229
1406
  laufend,
1230
1407
  " ",
1231
1408
  texts.toolRunning
@@ -1234,24 +1411,23 @@ function StepGroup({
1234
1411
  ]
1235
1412
  }
1236
1413
  ),
1237
- offen && /* @__PURE__ */ jsxs6("div", { className: "qsl-stepgroup__lines", children: [
1238
- messages.map((message) => /* @__PURE__ */ jsx7(TraceLine, { expandierbar, message, texts, toolArgumentsText }, message.key)),
1239
- messages.length >= collapseAtBottomFrom && /* @__PURE__ */ jsxs6(
1240
- "button",
1241
- {
1242
- className: "qsl-trace qsl-trace--compact qsl-stepgroup__toggle qsl-stepgroup__collapse",
1243
- onClick: () => {
1244
- vonUntenEingeklappt.current = true;
1245
- setOffen(false);
1246
- },
1247
- type: "button",
1248
- children: [
1249
- /* @__PURE__ */ jsx7(IconChevronRight, { className: "qsl-stepgroup__chevron", size: 12 }),
1250
- /* @__PURE__ */ jsx7("span", { className: "qsl-trace__line", children: texts.stepGroupCollapse })
1251
- ]
1252
- }
1253
- )
1254
- ] })
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
+ )
1255
1431
  ] });
1256
1432
  }
1257
1433
  function TraceLine({
@@ -1261,28 +1437,36 @@ function TraceLine({
1261
1437
  texts,
1262
1438
  toolArgumentsText
1263
1439
  }) {
1264
- const [detailPosition, setDetailPosition] = useState7();
1265
- const classes = `${className ? `${className} ` : ""}qsl-trace qsl-trace--compact${message.role === "thinking" ? " qsl-trace--thinking" : ""}`;
1266
- const inhalt = /* @__PURE__ */ jsxs6(Fragment3, { children: [
1267
- /* @__PURE__ */ jsx7(TraceIcon, { message }),
1268
- /* @__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
+ ] })
1269
1451
  ] });
1452
+ const marks = { "data-compact": "true", "data-kind": thinking ? "thinking" : "tool", "data-step": "line" };
1270
1453
  if (!expandierbar) {
1271
- return /* @__PURE__ */ jsx7("div", { className: classes, children: inhalt });
1454
+ return /* @__PURE__ */ jsx13("div", { className: classes, ...marks, children: inhalt });
1272
1455
  }
1273
- return /* @__PURE__ */ jsxs6(Fragment3, { children: [
1274
- /* @__PURE__ */ jsx7(
1456
+ return /* @__PURE__ */ jsxs6(Fragment2, { children: [
1457
+ /* @__PURE__ */ jsx13(
1275
1458
  "button",
1276
1459
  {
1277
1460
  "aria-expanded": detailPosition !== void 0,
1278
1461
  "aria-haspopup": "dialog",
1279
- className: classes,
1462
+ className: cn(classes, traceButtonClasses),
1463
+ ...marks,
1280
1464
  onClick: (event) => setDetailPosition({ x: event.clientX, y: event.clientY }),
1281
1465
  type: "button",
1282
1466
  children: inhalt
1283
1467
  }
1284
1468
  ),
1285
- detailPosition && /* @__PURE__ */ jsx7(
1469
+ detailPosition && /* @__PURE__ */ jsx13(
1286
1470
  StepPopover,
1287
1471
  {
1288
1472
  message,
@@ -1294,126 +1478,161 @@ function TraceLine({
1294
1478
  )
1295
1479
  ] });
1296
1480
  }
1297
- function TraceIcon({ message }) {
1298
- 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 });
1299
- }
1300
- function TraceText({ message, texts }) {
1301
- return /* @__PURE__ */ jsxs6(Fragment3, { children: [
1302
- message.text,
1303
- message.tool && message.tool.result === void 0 && /* @__PURE__ */ jsx7("span", { className: "qsl-trace__running", children: texts.toolRunning })
1304
- ] });
1305
- }
1306
1481
  function Bubble({
1307
1482
  message,
1308
- bubbleOptions,
1309
- messageActions,
1483
+ plain,
1310
1484
  detailMode,
1311
1485
  dicht,
1312
1486
  expandierbar,
1313
1487
  texts,
1314
1488
  toolArgumentsText,
1315
- onAnswerQuestion
1489
+ renderAction,
1490
+ onDismissAction,
1491
+ bubbleOptions,
1492
+ messageActions
1316
1493
  }) {
1317
- const schritt = dicht ? "qsl-step qsl-step--dense" : "qsl-step";
1494
+ const schritt = dicht ? denseStepClasses : stepClasses;
1318
1495
  if (message.role === "thinking" || message.role === "tool" && message.tool) {
1319
1496
  if (detailMode === "compact") {
1320
- return /* @__PURE__ */ jsx7(TraceLine, { className: schritt, expandierbar, message, texts, toolArgumentsText });
1497
+ return /* @__PURE__ */ jsx13(TraceLine, { className: schritt, expandierbar, message, texts, toolArgumentsText });
1321
1498
  }
1499
+ const thinking = message.role === "thinking";
1322
1500
  const tool = message.tool;
1323
- return /* @__PURE__ */ jsxs6("div", { className: `${schritt} qsl-trace${message.role === "thinking" ? " qsl-trace--thinking" : ""}`, children: [
1324
- /* @__PURE__ */ jsx7(TraceIcon, { message }),
1325
- /* @__PURE__ */ jsxs6("div", { className: "qsl-trace__body", children: [
1326
- /* @__PURE__ */ jsx7("div", { className: "qsl-trace__line", children: /* @__PURE__ */ jsx7(TraceText, { message, texts }) }),
1327
- tool && /* @__PURE__ */ jsxs6("div", { className: "qsl-details", children: [
1328
- /* @__PURE__ */ jsx7("pre", { children: toolArgumentsText(tool) }),
1329
- 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) })
1330
1516
  ] })
1331
1517
  ] })
1332
1518
  ] });
1333
1519
  }
1334
- if (message.role === "question" && message.question) {
1335
- return /* @__PURE__ */ jsx7("div", { className: schritt, children: /* @__PURE__ */ jsx7(
1336
- 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,
1337
1524
  {
1338
- question: message.question,
1525
+ action,
1526
+ dismissedLabel: texts.actionDismissed,
1527
+ dismissLabel: texts.dismissAction,
1528
+ onDismiss: onDismissAction ? () => onDismissAction(action.actionId) : void 0,
1339
1529
  text: message.text,
1340
- texts,
1341
- onAnswer: (text) => onAnswerQuestion?.(message.question.callId, text)
1530
+ waitingLabel: texts.pendingAction
1342
1531
  }
1343
1532
  ) });
1344
1533
  }
1345
1534
  if (message.role === "system") {
1346
- 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
+ ] });
1347
1539
  }
1348
- const content = message.text.trim();
1349
- if (!content && message.role !== "user") return null;
1350
- const plain = bubbleOptions?.variant === "plain";
1351
- 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);
1352
1543
  const side = (message.role === "user" ? bubbleOptions?.userSide : bubbleOptions?.assistantSide) ?? message.bubble?.side ?? (message.role === "user" ? "end" : "start");
1353
- const label = bubbleOptions?.showSender === false ? void 0 : bubbleOptions?.senderLabel?.(message) ?? message.bubble?.label ?? (bubbleOptions?.showSender ? message.role : void 0);
1354
- const body = /* @__PURE__ */ jsxs6(Fragment3, { children: [
1355
- label && /* @__PURE__ */ jsx7("span", { className: "qsl-bubble__label", children: label }),
1356
- /* @__PURE__ */ jsx7(Markdown, { text: message.role === "user" ? message.text : content }),
1357
- /* @__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 })
1358
1555
  ] });
1359
- if (isBubble) {
1360
- const user = message.role === "user" && !message.bubble;
1361
- 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 }) });
1362
- }
1363
- 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}`)) });
1364
1587
  }
1365
1588
 
1366
1589
  // packages/quassel/src/ChatPanel.tsx
1367
- import { useEffect as useEffect6, useLayoutEffect as useLayoutEffect4, useMemo as useMemo2, useRef as useRef6 } from "react";
1368
- 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";
1369
1592
  function ChatPanel({
1370
1593
  children,
1371
1594
  composer,
1372
1595
  className,
1373
1596
  nodeRef,
1597
+ maxWidth,
1598
+ horizontalPadding,
1374
1599
  appearance,
1375
1600
  scrollOnSend = false
1376
1601
  }) {
1377
- const jump = useRef6(void 0);
1378
- const scrollOnSendRef = useRef6(scrollOnSend);
1379
- 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(() => {
1380
1608
  scrollOnSendRef.current = scrollOnSend;
1381
1609
  }, [scrollOnSend]);
1382
- const sendContext = useMemo2(() => ({
1383
- onSent: () => {
1384
- if (scrollOnSendRef.current) jump.current?.();
1385
- },
1386
- registerJump: (callback) => {
1387
- jump.current = callback;
1388
- return () => {
1389
- if (jump.current === callback) jump.current = void 0;
1390
- };
1391
- }
1392
- }), []);
1393
- const wurzel = useRef6(null);
1394
- const eingabe = useRef6(null);
1395
- const mitEingabe = composer !== void 0;
1396
- useEffect6(() => {
1610
+ useEffect4(() => {
1397
1611
  const element = eingabe.current;
1398
1612
  const rahmen = wurzel.current;
1399
- if (!rahmen) {
1400
- return;
1401
- }
1402
- if (!element) {
1403
- rahmen.style.removeProperty("--qsl-composer-height");
1613
+ if (!element || !rahmen) {
1404
1614
  return;
1405
1615
  }
1406
1616
  const messen = () => rahmen.style.setProperty("--qsl-composer-height", `${element.offsetHeight}px`);
1407
1617
  const beobachter = new ResizeObserver(messen);
1408
1618
  beobachter.observe(element);
1409
1619
  messen();
1410
- return () => beobachter.disconnect();
1411
- }, [mitEingabe]);
1412
- 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(
1413
1626
  "div",
1414
1627
  {
1415
- className: className ? `qsl-chat-panel ${className}` : "qsl-chat-panel",
1416
- 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
+ },
1417
1636
  ref: (element) => {
1418
1637
  wurzel.current = element;
1419
1638
  if (typeof nodeRef === "function") {
@@ -1424,83 +1643,290 @@ function ChatPanel({
1424
1643
  },
1425
1644
  children: [
1426
1645
  children,
1427
- 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 })
1428
1647
  ]
1429
1648
  }
1430
1649
  ) });
1431
1650
  }
1432
1651
 
1433
- // packages/quassel/src/TimestampSwitch.tsx
1434
- import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1435
- function TimestampSwitch({
1436
- showTimestamps,
1437
- onChange,
1438
- collapsible = true,
1439
- texts,
1440
- className
1441
- }) {
1442
- const label = texts?.timestamps ?? defaultTexts.timestamps;
1443
- const classes = showTimestamps ? "qsl-icon-button qsl-icon-button--active" : "qsl-icon-button";
1444
- return /* @__PURE__ */ jsxs8(
1445
- "button",
1446
- {
1447
- "aria-label": label,
1448
- "aria-pressed": showTimestamps,
1449
- className: className ? `${classes} ${className}` : classes,
1450
- onClick: () => onChange(!showTimestamps),
1451
- title: label,
1452
- type: "button",
1453
- children: [
1454
- /* @__PURE__ */ jsx9(IconClock, {}),
1455
- /* @__PURE__ */ jsx9("span", { className: collapsible ? "qsl-collapsible" : void 0, children: label })
1456
- ]
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
+ }
1457
1699
  }
1458
- );
1700
+ };
1459
1701
  }
1460
1702
 
1461
1703
  // packages/quassel/src/ChatInputToolbar.tsx
1462
- import { useCallback as useCallback3, useEffect as useEffect7, useRef as useRef7, useState as useState8 } from "react";
1463
- 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";
1464
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";
1465
1711
  function ChatInputToolbar({
1466
1712
  onSend,
1467
1713
  onStop,
1468
1714
  running = false,
1469
1715
  disabled = false,
1716
+ sendDisabled = false,
1717
+ layout = "card",
1718
+ detailsContainer,
1719
+ onErrorChange,
1720
+ inputAriaControls,
1470
1721
  rows = 3,
1471
1722
  maxRows,
1472
1723
  actions,
1473
1724
  toolbarLeft,
1474
1725
  toolbarRight,
1475
1726
  texts,
1476
- 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
1477
1737
  }) {
1738
+ const sendScope = useContext4(ChatSendContext);
1478
1739
  const alleTexte = { ...defaultTexts, ...texts };
1479
- const [draft, setDraft] = useState8("");
1480
- const { submit, sending, error } = useChatSubmit(onSend);
1481
- const rootRef = useRef7(null);
1482
- const eingabeRef = useRef7(null);
1483
- 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]);
1484
1910
  const applyAutoGrow = useCallback3(() => {
1485
1911
  const element = eingabeRef.current;
1486
- if (!element || maxRows === void 0) {
1912
+ if (!element || inputMaxRows === void 0) {
1487
1913
  return;
1488
1914
  }
1489
1915
  const stil = getComputedStyle(element);
1490
1916
  const zeilenhoehe = Number.parseFloat(stil.lineHeight) || Number.parseFloat(stil.fontSize) * 1.4;
1491
1917
  const polster = Number.parseFloat(stil.paddingTop) + Number.parseFloat(stil.paddingBottom);
1492
- const maximum = zeilenhoehe * Math.max(maxRows, rows) + polster;
1918
+ const maximum = zeilenhoehe * Math.max(inputMaxRows, inputRows) + polster;
1493
1919
  element.style.height = "auto";
1494
1920
  const inhalt = element.scrollHeight;
1495
1921
  element.style.height = `${Math.min(inhalt, maximum)}px`;
1496
1922
  element.style.overflowY = inhalt > maximum ? "auto" : "hidden";
1497
- }, [maxRows, rows]);
1498
- useEffect7(() => {
1923
+ }, [inputMaxRows, inputRows]);
1924
+ useEffect5(() => {
1499
1925
  applyAutoGrow();
1500
1926
  }, [draft, applyAutoGrow]);
1501
- useEffect7(() => {
1927
+ useEffect5(() => {
1502
1928
  const element = eingabeRef.current;
1503
- if (!element || maxRows === void 0) {
1929
+ if (!element || inputMaxRows === void 0) {
1504
1930
  return;
1505
1931
  }
1506
1932
  let lastWidth = element.clientWidth;
@@ -1512,8 +1938,8 @@ function ChatInputToolbar({
1512
1938
  });
1513
1939
  observer.observe(element);
1514
1940
  return () => observer.disconnect();
1515
- }, [maxRows, applyAutoGrow]);
1516
- useEffect7(() => {
1941
+ }, [inputMaxRows, applyAutoGrow]);
1942
+ useEffect5(() => {
1517
1943
  const element = rootRef.current;
1518
1944
  if (!element) {
1519
1945
  return;
@@ -1524,79 +1950,245 @@ function ChatInputToolbar({
1524
1950
  beobachter.observe(element);
1525
1951
  return () => beobachter.disconnect();
1526
1952
  }, []);
1527
- const hatText = draft.trim().length > 0;
1528
- 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) => {
1529
1969
  const text = draft.trim();
1530
- 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")) {
1531
1972
  return;
1532
1973
  }
1533
- 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
+ }
1534
2014
  };
1535
2015
  const onKeyDown = (event) => {
1536
- if (isSendKey(event, sendShortcut)) {
2016
+ if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing && event.nativeEvent.keyCode !== 229 && (sendShortcut === "enter" || event.metaKey || event.ctrlKey)) {
1537
2017
  event.preventDefault();
1538
2018
  if (!event.repeat) void send();
1539
2019
  }
1540
2020
  };
1541
- return /* @__PURE__ */ jsxs9("div", { className: compact ? "qsl-input-card qsl-input-card--compact" : "qsl-input-card", ref: rootRef, children: [
1542
- error && /* @__PURE__ */ jsx10("p", { className: "qsl-action-error", role: "alert", children: error }),
1543
- /* @__PURE__ */ jsx10(
1544
- "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,
1545
2032
  {
1546
- "aria-label": alleTexte.placeholder,
1547
- disabled,
1548
- onChange: (event) => setDraft(event.target.value),
1549
- onKeyDown,
1550
- placeholder: running ? alleTexte.steeringPlaceholder : alleTexte.placeholder,
1551
- ref: eingabeRef,
1552
- rows,
1553
- 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, {})
1554
2045
  }
1555
- ),
1556
- /* @__PURE__ */ jsxs9("div", { className: "qsl-input-card__toolbar", children: [
1557
- /* @__PURE__ */ jsxs9("div", { className: "qsl-input-card__left", children: [
1558
- actions?.map((action, index) => /* @__PURE__ */ jsxs9(
1559
- "button",
1560
- {
1561
- className: action.active ? "qsl-icon-button qsl-icon-button--active" : "qsl-icon-button",
1562
- disabled: action.disabled,
1563
- onClick: action.onClick,
1564
- title: action.title ?? action.label,
1565
- type: "button",
1566
- children: [
1567
- action.icon,
1568
- action.label && /* @__PURE__ */ jsx10("span", { className: action.icon ? "qsl-collapsible" : void 0, children: action.label })
1569
- ]
1570
- },
1571
- index
1572
- )),
1573
- 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
+ " "
1574
2060
  ] }),
1575
- /* @__PURE__ */ jsxs9("div", { className: "qsl-input-card__right", children: [
1576
- toolbarRight,
1577
- 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, {}) }),
1578
- (!running || !onStop || hatText) && /* @__PURE__ */ jsx10(
1579
- "button",
1580
- {
1581
- "aria-label": alleTexte.send,
1582
- className: "qsl-send",
1583
- disabled: disabled || sending || !hatText,
1584
- onClick: () => void send(),
1585
- title: running ? alleTexte.sendIntoRun : alleTexte.send,
1586
- type: "button",
1587
- children: /* @__PURE__ */ jsx10(IconSend, {})
1588
- }
1589
- )
1590
- ] })
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
1591
2121
  ] })
1592
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
+ );
1593
2183
  }
1594
2184
 
1595
2185
  // packages/quassel/src/DetailModeSwitch.tsx
1596
- import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
1597
- 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"];
1598
2189
  var TEXT_KEYS = {
1599
2190
  off: "detailModeOff",
2191
+ current: "detailModeCurrent",
1600
2192
  icons: "detailModeIcons",
1601
2193
  chips: "detailModeChips",
1602
2194
  grouped: "detailModeGrouped",
@@ -1613,19 +2205,46 @@ function DetailModeSwitch({
1613
2205
  className
1614
2206
  }) {
1615
2207
  const alleTexte = { ...defaultTexts, ...texts };
2208
+ const { Button: Button2 } = useQuasselComponents();
1616
2209
  const aktuell = modes.indexOf(mode);
1617
2210
  const naechster = modes[(aktuell < 0 ? 0 : aktuell + 1) % modes.length];
1618
- return /* @__PURE__ */ jsxs10(
1619
- "button",
2211
+ return /* @__PURE__ */ jsxs9(
2212
+ Button2,
1620
2213
  {
1621
2214
  "aria-label": `${alleTexte.detailModeTitle}: ${detailModeLabel(mode, texts)}`,
1622
- className: className ? `qsl-icon-button ${className}` : "qsl-icon-button",
2215
+ className,
1623
2216
  onClick: () => onChange(naechster),
2217
+ size: "sm",
1624
2218
  title: `${alleTexte.detailModeTitle}: ${detailModeLabel(mode, texts)}`,
1625
- 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",
1626
2245
  children: [
1627
- /* @__PURE__ */ jsx11(IconSpark, { size: 14 }),
1628
- /* @__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 })
1629
2248
  ]
1630
2249
  }
1631
2250
  );
@@ -1634,28 +2253,44 @@ export {
1634
2253
  ChatInputToolbar,
1635
2254
  ChatMessages,
1636
2255
  ChatPanel,
2256
+ ChatSendContext,
1637
2257
  DETAIL_MODES,
1638
2258
  DetailModeSwitch,
1639
- IconCheck,
1640
- IconChevronDown,
1641
- IconChevronRight,
1642
- IconClock,
1643
- IconLayers,
1644
- IconSend,
1645
- IconSpark,
1646
- IconStop,
1647
- IconTool,
1648
- IconX,
2259
+ MAX_CHAT_ATTACHMENTS,
2260
+ MAX_CHAT_ATTACHMENT_BYTES,
1649
2261
  Markdown,
2262
+ MarkdownCodeBlocks,
2263
+ MarkdownLinks,
2264
+ MessageActions,
2265
+ PendingActionCard,
2266
+ QuasselProvider,
2267
+ StepPopover,
1650
2268
  TimestampSwitch,
2269
+ WorkingScenes,
1651
2270
  announce,
2271
+ appearanceStyle,
1652
2272
  applyEvent,
2273
+ attachmentCapabilityError,
2274
+ attachmentDownloadUrl,
2275
+ attachmentMediaType,
1653
2276
  compactToolLine,
2277
+ copyChatText,
2278
+ createChatScroll,
2279
+ createChatSendScope,
2280
+ defaultComponents,
1654
2281
  defaultTexts,
1655
2282
  detailModeLabel,
2283
+ encodeAttachment,
2284
+ fillText,
2285
+ formatAttachmentSize,
1656
2286
  markdownPlainText,
2287
+ messageDate,
1657
2288
  prettyJson,
1658
2289
  stepState,
1659
- useChat
2290
+ timestampDay,
2291
+ timestampLabel,
2292
+ useChatAction,
2293
+ useQuasselComponents,
2294
+ validateAttachmentSelection
1660
2295
  };
1661
2296
  //# sourceMappingURL=index.js.map