@lalternative/assistant 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,654 @@
1
+ // src/assistant-launcher.tsx
2
+ import { useState as useState3 } from "react";
3
+
4
+ // src/assistant-panel.tsx
5
+ import {
6
+ useCallback as useCallback2,
7
+ useEffect as useEffect2,
8
+ useRef as useRef2,
9
+ useState as useState2
10
+ } from "react";
11
+
12
+ // src/attachments.ts
13
+ import { useCallback, useEffect, useRef, useState } from "react";
14
+
15
+ // src/format.ts
16
+ function relativeTime(iso, now = Date.now()) {
17
+ if (!iso) return "";
18
+ const minutes = Math.round((now - new Date(iso).getTime()) / 6e4);
19
+ if (minutes < 1) return "\xE0 l'instant";
20
+ if (minutes < 60) return `il y a ${minutes} min`;
21
+ const hours = Math.round(minutes / 60);
22
+ if (hours < 24) return `il y a ${hours} heure${hours > 1 ? "s" : ""}`;
23
+ const days = Math.round(hours / 24);
24
+ return `il y a ${days} jour${days > 1 ? "s" : ""}`;
25
+ }
26
+ function formatSize(bytes) {
27
+ if (bytes < 1024) return `${bytes} o`;
28
+ if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} Ko`;
29
+ return `${(bytes / (1024 * 1024)).toFixed(1).replace(/\.0$/, "")} Mo`;
30
+ }
31
+ var SCREEN_TEXT_LIMIT = 12e3;
32
+ function readMainScreen() {
33
+ if (typeof document === "undefined") return void 0;
34
+ const main = document.querySelector("main");
35
+ const text = (main?.innerText ?? "").replace(/\n{3,}/g, "\n\n").trim().slice(0, SCREEN_TEXT_LIMIT);
36
+ return { path: window.location.pathname + window.location.search, title: document.title, text };
37
+ }
38
+ function errorMessage(error, fallback) {
39
+ const e = error;
40
+ return e?.message || fallback;
41
+ }
42
+
43
+ // src/attachments.ts
44
+ var typesByExtension = {
45
+ txt: "text/plain",
46
+ log: "text/plain",
47
+ md: "text/markdown",
48
+ markdown: "text/markdown",
49
+ csv: "text/csv",
50
+ json: "application/json",
51
+ xml: "application/xml",
52
+ yaml: "application/yaml",
53
+ yml: "application/yaml",
54
+ html: "text/html"
55
+ };
56
+ function withType(file) {
57
+ if (file.type) return file;
58
+ const type = typesByExtension[file.name.split(".").pop()?.toLowerCase() ?? ""];
59
+ return type ? new File([file], file.name, { type }) : file;
60
+ }
61
+ function fileError(file, limits, labels) {
62
+ if (!limits) return void 0;
63
+ const type = file.type.toLowerCase();
64
+ if (limits.imageTypes.includes(type)) {
65
+ return file.size > limits.maxImageBytes ? labels.fileTooLarge(file.name, formatSize(limits.maxImageBytes)) : void 0;
66
+ }
67
+ if (limits.fileTypes.includes(type)) {
68
+ return file.size > limits.maxFileBytes ? labels.fileTooLarge(file.name, formatSize(limits.maxFileBytes)) : void 0;
69
+ }
70
+ return labels.fileTypeRefused(file.name);
71
+ }
72
+ function pastedFiles(data, pasteAsFileThreshold, name) {
73
+ if (!data) return [];
74
+ const files = Array.from(data.items ?? []).filter((item) => item.kind === "file").map((item) => item.getAsFile()).filter((f) => f !== null);
75
+ if (files.length > 0) return files;
76
+ const text = data.getData("text/plain");
77
+ if (pasteAsFileThreshold !== void 0 && text.length > pasteAsFileThreshold) {
78
+ return [new File([text], name, { type: "text/plain" })];
79
+ }
80
+ return [];
81
+ }
82
+ function previewOf(file) {
83
+ if (!file.type.startsWith("image/") || typeof URL.createObjectURL !== "function") return void 0;
84
+ return URL.createObjectURL(file);
85
+ }
86
+ function revoke(item) {
87
+ if (item.previewUrl && typeof URL.revokeObjectURL === "function") URL.revokeObjectURL(item.previewUrl);
88
+ }
89
+ var nextId = 0;
90
+ function useAttachments({
91
+ attach,
92
+ scopeId,
93
+ limits,
94
+ labels
95
+ }) {
96
+ const [items, setItems] = useState([]);
97
+ const [notice, setNotice] = useState(null);
98
+ const itemsRef = useRef(items);
99
+ itemsRef.current = items;
100
+ useEffect(() => () => itemsRef.current.forEach(revoke), []);
101
+ const update = (localId, patch) => setItems((list) => list.map((i) => i.localId === localId ? { ...i, ...patch } : i));
102
+ const add = useCallback(
103
+ (incoming) => {
104
+ if (!attach || incoming.length === 0) return;
105
+ setNotice(null);
106
+ const room = limits ? limits.maxCount - itemsRef.current.length : incoming.length;
107
+ if (incoming.length > room) setNotice(labels.tooManyFiles(limits?.maxCount ?? 0));
108
+ const accepted = incoming.slice(0, Math.max(0, room)).map((raw) => {
109
+ const file = withType(raw);
110
+ const error = fileError(file, limits, labels);
111
+ const item = {
112
+ localId: `a${nextId++}`,
113
+ file,
114
+ status: error ? "error" : "uploading",
115
+ error,
116
+ previewUrl: error ? void 0 : previewOf(file)
117
+ };
118
+ return item;
119
+ });
120
+ if (accepted.length === 0) return;
121
+ itemsRef.current = [...itemsRef.current, ...accepted];
122
+ setItems(itemsRef.current);
123
+ for (const item of accepted) {
124
+ if (item.status !== "uploading") continue;
125
+ attach(item.file, scopeId).then(
126
+ (attachment) => update(item.localId, { status: "done", attachment }),
127
+ (err) => update(item.localId, { status: "error", error: err?.message || labels.uploadFailed })
128
+ );
129
+ }
130
+ },
131
+ [attach, scopeId, limits, labels]
132
+ );
133
+ const remove = useCallback((localId) => {
134
+ setItems((list) => {
135
+ list.filter((i) => i.localId === localId).forEach(revoke);
136
+ return list.filter((i) => i.localId !== localId);
137
+ });
138
+ setNotice(null);
139
+ }, []);
140
+ const clear = useCallback(() => {
141
+ itemsRef.current.forEach(revoke);
142
+ itemsRef.current = [];
143
+ setItems([]);
144
+ setNotice(null);
145
+ }, []);
146
+ const done = items.filter((i) => i.status === "done" && i.attachment).map((i) => i.attachment);
147
+ return {
148
+ enabled: !!attach,
149
+ items,
150
+ notice,
151
+ done,
152
+ uploading: items.some((i) => i.status === "uploading"),
153
+ add,
154
+ remove,
155
+ clear
156
+ };
157
+ }
158
+
159
+ // src/icons.tsx
160
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
161
+ var paths = {
162
+ bot: /* @__PURE__ */ jsxs(Fragment, { children: [
163
+ /* @__PURE__ */ jsx("path", { d: "M12 8V4H8" }),
164
+ /* @__PURE__ */ jsx("rect", { width: "16", height: "12", x: "4", y: "8", rx: "2" }),
165
+ /* @__PURE__ */ jsx("path", { d: "M2 14h2M20 14h2M15 13v2M9 13v2" })
166
+ ] }),
167
+ clock: /* @__PURE__ */ jsxs(Fragment, { children: [
168
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
169
+ /* @__PURE__ */ jsx("path", { d: "M12 6v6l4 2" })
170
+ ] }),
171
+ chevronDown: /* @__PURE__ */ jsx("path", { d: "m6 9 6 6 6-6" }),
172
+ plus: /* @__PURE__ */ jsx("path", { d: "M5 12h14M12 5v14" }),
173
+ x: /* @__PURE__ */ jsx("path", { d: "M18 6 6 18M6 6l12 12" }),
174
+ maximize: /* @__PURE__ */ jsx("path", { d: "M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7" }),
175
+ minimize: /* @__PURE__ */ jsx("path", { d: "M4 14h6v6M20 10h-6V4M14 10l7-7M3 21l7-7" }),
176
+ arrowUp: /* @__PURE__ */ jsx("path", { d: "m5 12 7-7 7 7M12 19V5" }),
177
+ paperclip: /* @__PURE__ */ jsx("path", { d: "m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48" }),
178
+ file: /* @__PURE__ */ jsxs(Fragment, { children: [
179
+ /* @__PURE__ */ jsx("path", { d: "M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" }),
180
+ /* @__PURE__ */ jsx("path", { d: "M14 2v4a2 2 0 0 0 2 2h4" })
181
+ ] })
182
+ };
183
+ function Icon({ name, size = 16, className }) {
184
+ return /* @__PURE__ */ jsx(
185
+ "svg",
186
+ {
187
+ width: size,
188
+ height: size,
189
+ viewBox: "0 0 24 24",
190
+ fill: "none",
191
+ stroke: "currentColor",
192
+ strokeWidth: 2,
193
+ strokeLinecap: "round",
194
+ strokeLinejoin: "round",
195
+ "aria-hidden": "true",
196
+ className,
197
+ children: paths[name]
198
+ }
199
+ );
200
+ }
201
+
202
+ // src/labels.ts
203
+ var defaultLabels = {
204
+ launcher: "Assistant",
205
+ history: "Historique",
206
+ newConversation: "Nouvelle conversation",
207
+ recent: "R\xE9centes",
208
+ allHistory: "Voir tout l'historique",
209
+ emptyTitle: "Que puis-je faire ?",
210
+ intro: "",
211
+ noConversation: "Aucune conversation pour l'instant.",
212
+ placeholder: "D\xE9crivez votre besoin\u2026",
213
+ send: "Envoyer",
214
+ attach: "Joindre un fichier",
215
+ removeAttachment: "Retirer",
216
+ close: "Fermer",
217
+ expand: "Agrandir",
218
+ collapse: "R\xE9duire",
219
+ resize: "Redimensionner l'assistant",
220
+ scope: "Projet",
221
+ loading: "Chargement\u2026",
222
+ thinking: "L'assistant regarde \u2014 \xE7a peut prendre un moment.",
223
+ dropHere: "D\xE9posez vos fichiers ici",
224
+ uploading: "Envoi\u2026",
225
+ tooManyFiles: (max) => `Au plus ${max} fichiers par message.`,
226
+ fileTooLarge: (name, max) => `${name} d\xE9passe ${max}.`,
227
+ fileTypeRefused: (name) => `${name} : type de fichier non accept\xE9.`,
228
+ uploadFailed: "L'envoi du fichier a \xE9chou\xE9.",
229
+ requestFailed: "La requ\xEAte \xE0 l'assistant a \xE9chou\xE9."
230
+ };
231
+
232
+ // src/assistant-panel.tsx
233
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
234
+ var MIN_WIDTH = 360;
235
+ var DEFAULT_WIDTH = 520;
236
+ var PASTED_TEXT_NAME = "texte-colle.txt";
237
+ function storedWidth(key) {
238
+ try {
239
+ const v = Number(localStorage.getItem(key));
240
+ return v >= MIN_WIDTH ? v : DEFAULT_WIDTH;
241
+ } catch {
242
+ return DEFAULT_WIDTH;
243
+ }
244
+ }
245
+ function usePanelWidth(key) {
246
+ const [width, setWidth] = useState2(() => storedWidth(key));
247
+ const [expanded, setExpanded] = useState2(false);
248
+ const startResize = (e) => {
249
+ e.preventDefault();
250
+ setExpanded(false);
251
+ let latest = width;
252
+ const move = (ev) => {
253
+ latest = Math.max(MIN_WIDTH, Math.min(window.innerWidth - 80, window.innerWidth - ev.clientX));
254
+ setWidth(latest);
255
+ };
256
+ const up = () => {
257
+ window.removeEventListener("pointermove", move);
258
+ window.removeEventListener("pointerup", up);
259
+ try {
260
+ localStorage.setItem(key, String(latest));
261
+ } catch {
262
+ return;
263
+ }
264
+ };
265
+ window.addEventListener("pointermove", move);
266
+ window.addEventListener("pointerup", up);
267
+ };
268
+ return { width, expanded, setExpanded, startResize };
269
+ }
270
+ function AssistantPanel({
271
+ api,
272
+ scopes,
273
+ scopeId,
274
+ onScopeChange,
275
+ scopeLocked = false,
276
+ quickActions = [],
277
+ attachmentLimits,
278
+ pasteAsFileThreshold,
279
+ labels: labelOverrides,
280
+ renderMarkdown,
281
+ renderMessageFooter,
282
+ readScreen,
283
+ widthStorageKey = "lalt.assistant.width",
284
+ onClose,
285
+ initialHistory = false
286
+ }) {
287
+ const labels = { ...defaultLabels, ...labelOverrides };
288
+ const scopeLabel = scopes.find((s) => s.id === scopeId)?.label ?? "";
289
+ const [conversationId, setConversationId] = useState2();
290
+ const [thread, setThread] = useState2([]);
291
+ const [draft, setDraft] = useState2("");
292
+ const [error, setError] = useState2(null);
293
+ const [historyOpen, setHistoryOpen] = useState2(initialHistory);
294
+ const [menuOpen, setMenuOpen] = useState2(false);
295
+ const [pending, setPending] = useState2(false);
296
+ const [loading, setLoading] = useState2(false);
297
+ const [dragging, setDragging] = useState2(false);
298
+ const [conversations, setConversations] = useState2([]);
299
+ const fileInput = useRef2(null);
300
+ const endRef = useRef2(null);
301
+ const generation = useRef2(0);
302
+ const attachments = useAttachments({ attach: api.attach?.bind(api), scopeId, limits: attachmentLimits, labels });
303
+ const refreshConversations = useCallback2(() => {
304
+ api.listConversations().then(setConversations, (err) => setError(errorMessage(err, labels.requestFailed)));
305
+ }, [api, labels.requestFailed]);
306
+ useEffect2(refreshConversations, [refreshConversations]);
307
+ useEffect2(() => {
308
+ endRef.current?.scrollIntoView?.({ block: "end" });
309
+ }, [thread.length, pending]);
310
+ const newConversation = () => {
311
+ generation.current++;
312
+ setThread([]);
313
+ setConversationId(void 0);
314
+ setError(null);
315
+ setHistoryOpen(false);
316
+ setMenuOpen(false);
317
+ setPending(false);
318
+ attachments.clear();
319
+ };
320
+ const openConversation = (c) => {
321
+ const current = ++generation.current;
322
+ setHistoryOpen(false);
323
+ setMenuOpen(false);
324
+ setError(null);
325
+ setLoading(true);
326
+ api.getConversation(c.id).then(
327
+ (detail) => {
328
+ if (current !== generation.current) return;
329
+ setThread(detail.messages);
330
+ setConversationId(detail.id);
331
+ if (detail.scopeId && detail.scopeId !== scopeId) onScopeChange?.(detail.scopeId);
332
+ setLoading(false);
333
+ },
334
+ (err) => {
335
+ if (current !== generation.current) return;
336
+ setError(errorMessage(err, labels.requestFailed));
337
+ setLoading(false);
338
+ }
339
+ );
340
+ };
341
+ const canSend = !!scopeId && !pending && !attachments.uploading;
342
+ const send = (message, shownAs) => {
343
+ const ready = attachments.done;
344
+ if (!canSend || !message.trim() && ready.length === 0) return false;
345
+ const current = generation.current;
346
+ setError(null);
347
+ setHistoryOpen(false);
348
+ setPending(true);
349
+ setThread((t) => [...t, { role: "user", content: shownAs, attachments: ready }]);
350
+ attachments.clear();
351
+ api.send({
352
+ scopeId,
353
+ conversationId,
354
+ message,
355
+ attachmentIds: ready.map((a) => a.id),
356
+ screen: readScreen?.()
357
+ }).then(
358
+ (res) => {
359
+ if (current !== generation.current) return;
360
+ setConversationId(res.conversationId);
361
+ setThread((t) => [...t, { role: "assistant", content: res.answer, meta: res.meta }]);
362
+ setPending(false);
363
+ refreshConversations();
364
+ },
365
+ (err) => {
366
+ if (current !== generation.current) return;
367
+ setError(errorMessage(err, labels.requestFailed));
368
+ setPending(false);
369
+ }
370
+ );
371
+ return true;
372
+ };
373
+ const submit = () => {
374
+ const text = draft.trim();
375
+ if (send(text, text)) setDraft("");
376
+ };
377
+ const onKeyDown = (e) => {
378
+ if (e.key === "Enter" && !e.shiftKey) {
379
+ e.preventDefault();
380
+ submit();
381
+ }
382
+ };
383
+ const onPaste = (e) => {
384
+ if (!attachments.enabled) return;
385
+ const files = pastedFiles(e.clipboardData, pasteAsFileThreshold, PASTED_TEXT_NAME);
386
+ if (files.length === 0) return;
387
+ e.preventDefault();
388
+ attachments.add(files);
389
+ };
390
+ const onDragOver = (e) => {
391
+ if (!attachments.enabled || !Array.from(e.dataTransfer?.types ?? []).includes("Files")) return;
392
+ e.preventDefault();
393
+ setDragging(true);
394
+ };
395
+ const onDrop = (e) => {
396
+ if (!attachments.enabled) return;
397
+ e.preventDefault();
398
+ setDragging(false);
399
+ attachments.add(Array.from(e.dataTransfer?.files ?? []));
400
+ };
401
+ const empty = thread.length === 0 && !pending && !loading;
402
+ const panel = usePanelWidth(widthStorageKey);
403
+ return /* @__PURE__ */ jsxs2(
404
+ "aside",
405
+ {
406
+ role: "dialog",
407
+ "aria-label": labels.launcher,
408
+ className: "lalt-assistant lalt-assistant-panel",
409
+ "data-expanded": panel.expanded || void 0,
410
+ style: { ["--lalt-assistant-width"]: panel.expanded ? "min(1100px, 92vw)" : `${panel.width}px` },
411
+ onDragOver,
412
+ onDragLeave: (e) => {
413
+ if (e.currentTarget === e.target) setDragging(false);
414
+ },
415
+ onDrop,
416
+ children: [
417
+ /* @__PURE__ */ jsx2(
418
+ "div",
419
+ {
420
+ onPointerDown: panel.startResize,
421
+ role: "separator",
422
+ "aria-orientation": "vertical",
423
+ "aria-label": labels.resize,
424
+ className: "lalt-assistant-resize"
425
+ }
426
+ ),
427
+ dragging && /* @__PURE__ */ jsx2("div", { className: "lalt-assistant-dropzone", children: labels.dropHere }),
428
+ /* @__PURE__ */ jsxs2("header", { className: "lalt-assistant-header", children: [
429
+ /* @__PURE__ */ jsxs2("div", { className: "lalt-assistant-menu", children: [
430
+ /* @__PURE__ */ jsxs2("button", { type: "button", className: "lalt-assistant-title", onClick: () => setMenuOpen((v) => !v), "aria-expanded": menuOpen, children: [
431
+ /* @__PURE__ */ jsx2(Icon, { name: "bot" }),
432
+ labels.launcher,
433
+ /* @__PURE__ */ jsx2(Icon, { name: "chevronDown", size: 14, className: "lalt-assistant-muted" })
434
+ ] }),
435
+ menuOpen && /* @__PURE__ */ jsxs2("div", { role: "menu", className: "lalt-assistant-menu-content", children: [
436
+ /* @__PURE__ */ jsxs2("button", { type: "button", role: "menuitem", onClick: newConversation, children: [
437
+ /* @__PURE__ */ jsx2(Icon, { name: "plus" }),
438
+ labels.newConversation
439
+ ] }),
440
+ conversations.length > 0 && /* @__PURE__ */ jsxs2(Fragment2, { children: [
441
+ /* @__PURE__ */ jsx2("div", { className: "lalt-assistant-menu-label", children: labels.recent }),
442
+ conversations.slice(0, 5).map((c) => /* @__PURE__ */ jsxs2("button", { type: "button", role: "menuitem", onClick: () => openConversation(c), children: [
443
+ /* @__PURE__ */ jsx2("span", { className: "lalt-assistant-grow lalt-assistant-truncate", children: c.title }),
444
+ /* @__PURE__ */ jsx2("span", { className: "lalt-assistant-muted lalt-assistant-small", children: relativeTime(c.updatedAt) })
445
+ ] }, c.id))
446
+ ] }),
447
+ /* @__PURE__ */ jsx2("div", { className: "lalt-assistant-menu-separator" }),
448
+ /* @__PURE__ */ jsxs2(
449
+ "button",
450
+ {
451
+ type: "button",
452
+ role: "menuitem",
453
+ onClick: () => {
454
+ setMenuOpen(false);
455
+ setHistoryOpen(true);
456
+ },
457
+ children: [
458
+ /* @__PURE__ */ jsx2(Icon, { name: "clock" }),
459
+ labels.allHistory
460
+ ]
461
+ }
462
+ )
463
+ ] })
464
+ ] }),
465
+ /* @__PURE__ */ jsx2("div", { className: "lalt-assistant-grow" }),
466
+ scopeLocked || scopes.length <= 1 ? /* @__PURE__ */ jsx2("span", { className: "lalt-assistant-muted lalt-assistant-small lalt-assistant-truncate lalt-assistant-scope-label", children: scopeLabel }) : /* @__PURE__ */ jsx2(
467
+ "select",
468
+ {
469
+ value: scopeId,
470
+ onChange: (e) => {
471
+ onScopeChange?.(e.target.value);
472
+ newConversation();
473
+ },
474
+ "aria-label": labels.scope,
475
+ className: "lalt-assistant-select",
476
+ children: scopes.map((s) => /* @__PURE__ */ jsx2("option", { value: s.id, children: s.label }, s.id))
477
+ }
478
+ ),
479
+ /* @__PURE__ */ jsx2(
480
+ "button",
481
+ {
482
+ type: "button",
483
+ onClick: () => panel.setExpanded((v) => !v),
484
+ "aria-label": panel.expanded ? labels.collapse : labels.expand,
485
+ className: "lalt-assistant-icon-button lalt-assistant-wide-only",
486
+ children: /* @__PURE__ */ jsx2(Icon, { name: panel.expanded ? "minimize" : "maximize" })
487
+ }
488
+ ),
489
+ /* @__PURE__ */ jsx2("button", { type: "button", onClick: onClose, "aria-label": labels.close, className: "lalt-assistant-icon-button", children: /* @__PURE__ */ jsx2(Icon, { name: "x" }) })
490
+ ] }),
491
+ /* @__PURE__ */ jsxs2("div", { className: "lalt-assistant-body", children: [
492
+ historyOpen ? /* @__PURE__ */ jsx2(HistoryList, { conversations, scopes, labels, onOpen: openConversation }) : empty ? /* @__PURE__ */ jsxs2("div", { children: [
493
+ /* @__PURE__ */ jsx2("h2", { className: "lalt-assistant-empty-title", children: labels.emptyTitle }),
494
+ labels.intro && /* @__PURE__ */ jsx2("p", { className: "lalt-assistant-intro", children: labels.intro }),
495
+ /* @__PURE__ */ jsx2("div", { className: "lalt-assistant-actions", children: quickActions.map((a) => /* @__PURE__ */ jsxs2(
496
+ "button",
497
+ {
498
+ type: "button",
499
+ onClick: () => send(a.prompt, a.label),
500
+ disabled: !canSend,
501
+ className: "lalt-assistant-action",
502
+ children: [
503
+ /* @__PURE__ */ jsx2("div", { className: "lalt-assistant-action-label", children: a.label }),
504
+ a.hint && /* @__PURE__ */ jsx2("div", { className: "lalt-assistant-muted lalt-assistant-small", children: a.hint })
505
+ ]
506
+ },
507
+ a.id
508
+ )) })
509
+ ] }) : /* @__PURE__ */ jsxs2("div", { className: "lalt-assistant-thread", children: [
510
+ thread.map(
511
+ (m, i) => m.role === "user" ? /* @__PURE__ */ jsxs2("div", { className: "lalt-assistant-user", children: [
512
+ m.content,
513
+ m.attachments && m.attachments.length > 0 && /* @__PURE__ */ jsx2("div", { className: "lalt-assistant-sent-files", children: m.attachments.map((a) => /* @__PURE__ */ jsxs2("span", { className: "lalt-assistant-sent-file", children: [
514
+ /* @__PURE__ */ jsx2(Icon, { name: "file", size: 12 }),
515
+ a.filename
516
+ ] }, a.id)) })
517
+ ] }, i) : /* @__PURE__ */ jsxs2("div", { className: "lalt-assistant-answer", children: [
518
+ renderMarkdown ? renderMarkdown(m.content) : /* @__PURE__ */ jsx2("div", { className: "lalt-assistant-plain", children: m.content }),
519
+ renderMessageFooter?.(m)
520
+ ] }, i)
521
+ ),
522
+ (pending || loading) && /* @__PURE__ */ jsxs2("div", { className: "lalt-assistant-muted lalt-assistant-small lalt-assistant-status", children: [
523
+ /* @__PURE__ */ jsx2("span", { className: "lalt-assistant-spinner" }),
524
+ loading ? labels.loading : labels.thinking
525
+ ] })
526
+ ] }),
527
+ error && /* @__PURE__ */ jsx2("p", { role: "alert", className: "lalt-assistant-error", children: error }),
528
+ /* @__PURE__ */ jsx2("div", { ref: endRef })
529
+ ] }),
530
+ /* @__PURE__ */ jsx2("div", { className: "lalt-assistant-footer", children: /* @__PURE__ */ jsxs2("div", { className: "lalt-assistant-composer", children: [
531
+ (attachments.items.length > 0 || attachments.notice) && /* @__PURE__ */ jsxs2("div", { className: "lalt-assistant-chips", children: [
532
+ attachments.items.map((item) => /* @__PURE__ */ jsx2(AttachmentChip, { item, labels, onRemove: () => attachments.remove(item.localId) }, item.localId)),
533
+ attachments.notice && /* @__PURE__ */ jsx2("p", { className: "lalt-assistant-error", children: attachments.notice })
534
+ ] }),
535
+ /* @__PURE__ */ jsx2(
536
+ "textarea",
537
+ {
538
+ value: draft,
539
+ onChange: (e) => setDraft(e.target.value),
540
+ onKeyDown,
541
+ onPaste,
542
+ rows: 3,
543
+ placeholder: labels.placeholder,
544
+ "aria-label": "Message",
545
+ className: "lalt-assistant-textarea"
546
+ }
547
+ ),
548
+ /* @__PURE__ */ jsxs2("div", { className: "lalt-assistant-composer-row", children: [
549
+ attachments.enabled && /* @__PURE__ */ jsxs2(Fragment2, { children: [
550
+ /* @__PURE__ */ jsx2(
551
+ "button",
552
+ {
553
+ type: "button",
554
+ onClick: () => fileInput.current?.click(),
555
+ "aria-label": labels.attach,
556
+ title: labels.attach,
557
+ className: "lalt-assistant-icon-button",
558
+ children: /* @__PURE__ */ jsx2(Icon, { name: "paperclip" })
559
+ }
560
+ ),
561
+ /* @__PURE__ */ jsx2(
562
+ "input",
563
+ {
564
+ ref: fileInput,
565
+ type: "file",
566
+ multiple: true,
567
+ hidden: true,
568
+ "data-testid": "lalt-assistant-file-input",
569
+ accept: attachmentLimits ? [...attachmentLimits.imageTypes, ...attachmentLimits.fileTypes].join(",") : void 0,
570
+ onChange: (e) => {
571
+ attachments.add(Array.from(e.target.files ?? []));
572
+ e.target.value = "";
573
+ }
574
+ }
575
+ )
576
+ ] }),
577
+ /* @__PURE__ */ jsx2("div", { className: "lalt-assistant-grow" }),
578
+ /* @__PURE__ */ jsx2(
579
+ "button",
580
+ {
581
+ type: "button",
582
+ onClick: submit,
583
+ disabled: !canSend || !draft.trim() && attachments.done.length === 0,
584
+ "aria-label": labels.send,
585
+ className: "lalt-assistant-send",
586
+ children: /* @__PURE__ */ jsx2(Icon, { name: "arrowUp" })
587
+ }
588
+ )
589
+ ] })
590
+ ] }) })
591
+ ]
592
+ }
593
+ );
594
+ }
595
+ function AttachmentChip({ item, labels, onRemove }) {
596
+ return /* @__PURE__ */ jsxs2("div", { className: "lalt-assistant-chip", "data-status": item.status, title: item.error, children: [
597
+ item.previewUrl ? /* @__PURE__ */ jsx2("img", { src: item.previewUrl, alt: "", className: "lalt-assistant-chip-preview" }) : /* @__PURE__ */ jsx2(Icon, { name: "file", size: 14 }),
598
+ /* @__PURE__ */ jsxs2("span", { className: "lalt-assistant-chip-text", children: [
599
+ /* @__PURE__ */ jsx2("span", { className: "lalt-assistant-truncate", children: item.file.name }),
600
+ /* @__PURE__ */ jsx2("span", { className: "lalt-assistant-muted lalt-assistant-small", children: item.status === "error" ? item.error : item.status === "uploading" ? labels.uploading : formatSize(item.file.size) })
601
+ ] }),
602
+ item.status === "uploading" && /* @__PURE__ */ jsx2("span", { className: "lalt-assistant-spinner" }),
603
+ /* @__PURE__ */ jsx2("button", { type: "button", onClick: onRemove, "aria-label": `${labels.removeAttachment} ${item.file.name}`, className: "lalt-assistant-chip-remove", children: /* @__PURE__ */ jsx2(Icon, { name: "x", size: 12 }) })
604
+ ] });
605
+ }
606
+ function HistoryList({
607
+ conversations,
608
+ scopes,
609
+ labels,
610
+ onOpen
611
+ }) {
612
+ if (conversations.length === 0) {
613
+ return /* @__PURE__ */ jsx2("p", { className: "lalt-assistant-muted lalt-assistant-history-empty", children: labels.noConversation });
614
+ }
615
+ return /* @__PURE__ */ jsx2("ul", { className: "lalt-assistant-history", children: conversations.map((c) => {
616
+ const scope = scopes.find((s) => s.id === c.scopeId)?.label;
617
+ return /* @__PURE__ */ jsx2("li", { children: /* @__PURE__ */ jsxs2("button", { type: "button", onClick: () => onOpen(c), children: [
618
+ /* @__PURE__ */ jsx2("div", { className: "lalt-assistant-truncate", children: c.title }),
619
+ /* @__PURE__ */ jsx2("div", { className: "lalt-assistant-muted lalt-assistant-small", children: [scope, relativeTime(c.updatedAt)].filter(Boolean).join(" \xB7 ") })
620
+ ] }) }, c.id);
621
+ }) });
622
+ }
623
+
624
+ // src/assistant-launcher.tsx
625
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
626
+ function AssistantLauncher(props) {
627
+ const [open, setOpen] = useState3(false);
628
+ const [showHistory, setShowHistory] = useState3(false);
629
+ const labels = { ...defaultLabels, ...props.labels };
630
+ const openPanel = (history) => {
631
+ setShowHistory(history);
632
+ setOpen(true);
633
+ };
634
+ if (open) return /* @__PURE__ */ jsx3(AssistantPanel, { ...props, initialHistory: showHistory, onClose: () => setOpen(false) });
635
+ return /* @__PURE__ */ jsxs3("div", { className: "lalt-assistant lalt-assistant-launcher", children: [
636
+ /* @__PURE__ */ jsxs3("button", { type: "button", onClick: () => openPanel(true), className: "lalt-assistant-launch lalt-assistant-launch-secondary", children: [
637
+ /* @__PURE__ */ jsx3(Icon, { name: "clock" }),
638
+ labels.history
639
+ ] }),
640
+ /* @__PURE__ */ jsxs3("button", { type: "button", onClick: () => openPanel(false), className: "lalt-assistant-launch lalt-assistant-launch-primary", children: [
641
+ /* @__PURE__ */ jsx3(Icon, { name: "bot" }),
642
+ labels.launcher
643
+ ] })
644
+ ] });
645
+ }
646
+ export {
647
+ AssistantLauncher,
648
+ AssistantPanel,
649
+ defaultLabels,
650
+ formatSize,
651
+ readMainScreen,
652
+ relativeTime
653
+ };
654
+ //# sourceMappingURL=index.js.map