@lalternative/assistant 0.0.0-stage → 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.
package/dist/index.js ADDED
@@ -0,0 +1,663 @@
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
+ renderScopePicker,
282
+ renderMessageFooter,
283
+ readScreen,
284
+ widthStorageKey = "lalt.assistant.width",
285
+ onClose,
286
+ initialHistory = false
287
+ }) {
288
+ const labels = { ...defaultLabels, ...labelOverrides };
289
+ const scopeLabel = scopes.find((s) => s.id === scopeId)?.label ?? "";
290
+ const [conversationId, setConversationId] = useState2();
291
+ const [thread, setThread] = useState2([]);
292
+ const [draft, setDraft] = useState2("");
293
+ const [error, setError] = useState2(null);
294
+ const [historyOpen, setHistoryOpen] = useState2(initialHistory);
295
+ const [menuOpen, setMenuOpen] = useState2(false);
296
+ const [pending, setPending] = useState2(false);
297
+ const [loading, setLoading] = useState2(false);
298
+ const [dragging, setDragging] = useState2(false);
299
+ const [conversations, setConversations] = useState2([]);
300
+ const fileInput = useRef2(null);
301
+ const endRef = useRef2(null);
302
+ const generation = useRef2(0);
303
+ const attachments = useAttachments({ attach: api.attach?.bind(api), scopeId, limits: attachmentLimits, labels });
304
+ const refreshConversations = useCallback2(() => {
305
+ api.listConversations().then(setConversations, (err) => setError(errorMessage(err, labels.requestFailed)));
306
+ }, [api, labels.requestFailed]);
307
+ useEffect2(refreshConversations, [refreshConversations]);
308
+ useEffect2(() => {
309
+ endRef.current?.scrollIntoView?.({ block: "end" });
310
+ }, [thread.length, pending]);
311
+ const newConversation = () => {
312
+ generation.current++;
313
+ setThread([]);
314
+ setConversationId(void 0);
315
+ setError(null);
316
+ setHistoryOpen(false);
317
+ setMenuOpen(false);
318
+ setPending(false);
319
+ attachments.clear();
320
+ };
321
+ const openConversation = (c) => {
322
+ const current = ++generation.current;
323
+ setHistoryOpen(false);
324
+ setMenuOpen(false);
325
+ setError(null);
326
+ setLoading(true);
327
+ api.getConversation(c.id).then(
328
+ (detail) => {
329
+ if (current !== generation.current) return;
330
+ setThread(detail.messages);
331
+ setConversationId(detail.id);
332
+ if (detail.scopeId && detail.scopeId !== scopeId) onScopeChange?.(detail.scopeId);
333
+ setLoading(false);
334
+ },
335
+ (err) => {
336
+ if (current !== generation.current) return;
337
+ setError(errorMessage(err, labels.requestFailed));
338
+ setLoading(false);
339
+ }
340
+ );
341
+ };
342
+ const canSend = !!scopeId && !pending && !attachments.uploading;
343
+ const send = (message, shownAs) => {
344
+ const ready = attachments.done;
345
+ if (!canSend || !message.trim() && ready.length === 0) return false;
346
+ const current = generation.current;
347
+ setError(null);
348
+ setHistoryOpen(false);
349
+ setPending(true);
350
+ setThread((t) => [...t, { role: "user", content: shownAs, attachments: ready }]);
351
+ attachments.clear();
352
+ api.send({
353
+ scopeId,
354
+ conversationId,
355
+ message,
356
+ attachmentIds: ready.map((a) => a.id),
357
+ screen: readScreen?.()
358
+ }).then(
359
+ (res) => {
360
+ if (current !== generation.current) return;
361
+ setConversationId(res.conversationId);
362
+ setThread((t) => [...t, { role: "assistant", content: res.answer, meta: res.meta }]);
363
+ setPending(false);
364
+ refreshConversations();
365
+ },
366
+ (err) => {
367
+ if (current !== generation.current) return;
368
+ setError(errorMessage(err, labels.requestFailed));
369
+ setPending(false);
370
+ }
371
+ );
372
+ return true;
373
+ };
374
+ const submit = () => {
375
+ const text = draft.trim();
376
+ if (send(text, text)) setDraft("");
377
+ };
378
+ const onKeyDown = (e) => {
379
+ if (e.key === "Enter" && !e.shiftKey) {
380
+ e.preventDefault();
381
+ submit();
382
+ }
383
+ };
384
+ const onPaste = (e) => {
385
+ if (!attachments.enabled) return;
386
+ const files = pastedFiles(e.clipboardData, pasteAsFileThreshold, PASTED_TEXT_NAME);
387
+ if (files.length === 0) return;
388
+ e.preventDefault();
389
+ attachments.add(files);
390
+ };
391
+ const onDragOver = (e) => {
392
+ if (!attachments.enabled || !Array.from(e.dataTransfer?.types ?? []).includes("Files")) return;
393
+ e.preventDefault();
394
+ setDragging(true);
395
+ };
396
+ const onDrop = (e) => {
397
+ if (!attachments.enabled) return;
398
+ e.preventDefault();
399
+ setDragging(false);
400
+ attachments.add(Array.from(e.dataTransfer?.files ?? []));
401
+ };
402
+ const empty = thread.length === 0 && !pending && !loading;
403
+ const panel = usePanelWidth(widthStorageKey);
404
+ return /* @__PURE__ */ jsxs2(
405
+ "aside",
406
+ {
407
+ role: "dialog",
408
+ "aria-label": labels.launcher,
409
+ className: "lalt-assistant lalt-assistant-panel",
410
+ "data-expanded": panel.expanded || void 0,
411
+ style: { ["--lalt-assistant-width"]: panel.expanded ? "min(1100px, 92vw)" : `${panel.width}px` },
412
+ onDragOver,
413
+ onDragLeave: (e) => {
414
+ if (e.currentTarget === e.target) setDragging(false);
415
+ },
416
+ onDrop,
417
+ children: [
418
+ /* @__PURE__ */ jsx2(
419
+ "div",
420
+ {
421
+ onPointerDown: panel.startResize,
422
+ role: "separator",
423
+ "aria-orientation": "vertical",
424
+ "aria-label": labels.resize,
425
+ className: "lalt-assistant-resize"
426
+ }
427
+ ),
428
+ dragging && /* @__PURE__ */ jsx2("div", { className: "lalt-assistant-dropzone", children: labels.dropHere }),
429
+ /* @__PURE__ */ jsxs2("header", { className: "lalt-assistant-header", children: [
430
+ /* @__PURE__ */ jsxs2("div", { className: "lalt-assistant-menu", children: [
431
+ /* @__PURE__ */ jsxs2("button", { type: "button", className: "lalt-assistant-title", onClick: () => setMenuOpen((v) => !v), "aria-expanded": menuOpen, children: [
432
+ /* @__PURE__ */ jsx2(Icon, { name: "bot" }),
433
+ labels.launcher,
434
+ /* @__PURE__ */ jsx2(Icon, { name: "chevronDown", size: 14, className: "lalt-assistant-muted" })
435
+ ] }),
436
+ menuOpen && /* @__PURE__ */ jsxs2("div", { role: "menu", className: "lalt-assistant-menu-content", children: [
437
+ /* @__PURE__ */ jsxs2("button", { type: "button", role: "menuitem", onClick: newConversation, children: [
438
+ /* @__PURE__ */ jsx2(Icon, { name: "plus" }),
439
+ labels.newConversation
440
+ ] }),
441
+ conversations.length > 0 && /* @__PURE__ */ jsxs2(Fragment2, { children: [
442
+ /* @__PURE__ */ jsx2("div", { className: "lalt-assistant-menu-label", children: labels.recent }),
443
+ conversations.slice(0, 5).map((c) => /* @__PURE__ */ jsxs2("button", { type: "button", role: "menuitem", onClick: () => openConversation(c), children: [
444
+ /* @__PURE__ */ jsx2("span", { className: "lalt-assistant-grow lalt-assistant-truncate", children: c.title }),
445
+ /* @__PURE__ */ jsx2("span", { className: "lalt-assistant-muted lalt-assistant-small", children: relativeTime(c.updatedAt) })
446
+ ] }, c.id))
447
+ ] }),
448
+ /* @__PURE__ */ jsx2("div", { className: "lalt-assistant-menu-separator" }),
449
+ /* @__PURE__ */ jsxs2(
450
+ "button",
451
+ {
452
+ type: "button",
453
+ role: "menuitem",
454
+ onClick: () => {
455
+ setMenuOpen(false);
456
+ setHistoryOpen(true);
457
+ },
458
+ children: [
459
+ /* @__PURE__ */ jsx2(Icon, { name: "clock" }),
460
+ labels.allHistory
461
+ ]
462
+ }
463
+ )
464
+ ] })
465
+ ] }),
466
+ /* @__PURE__ */ jsx2("div", { className: "lalt-assistant-grow" }),
467
+ scopeLocked || scopes.length <= 1 ? /* @__PURE__ */ jsx2("span", { className: "lalt-assistant-muted lalt-assistant-small lalt-assistant-truncate lalt-assistant-scope-label", children: scopeLabel }) : renderScopePicker ? renderScopePicker({
468
+ scopes,
469
+ scopeId,
470
+ label: labels.scope,
471
+ onChange: (id) => {
472
+ onScopeChange?.(id);
473
+ newConversation();
474
+ }
475
+ }) : /* @__PURE__ */ jsx2(
476
+ "select",
477
+ {
478
+ value: scopeId,
479
+ onChange: (e) => {
480
+ onScopeChange?.(e.target.value);
481
+ newConversation();
482
+ },
483
+ "aria-label": labels.scope,
484
+ className: "lalt-assistant-select",
485
+ children: scopes.map((s) => /* @__PURE__ */ jsx2("option", { value: s.id, children: s.label }, s.id))
486
+ }
487
+ ),
488
+ /* @__PURE__ */ jsx2(
489
+ "button",
490
+ {
491
+ type: "button",
492
+ onClick: () => panel.setExpanded((v) => !v),
493
+ "aria-label": panel.expanded ? labels.collapse : labels.expand,
494
+ className: "lalt-assistant-icon-button lalt-assistant-wide-only",
495
+ children: /* @__PURE__ */ jsx2(Icon, { name: panel.expanded ? "minimize" : "maximize" })
496
+ }
497
+ ),
498
+ /* @__PURE__ */ jsx2("button", { type: "button", onClick: onClose, "aria-label": labels.close, className: "lalt-assistant-icon-button", children: /* @__PURE__ */ jsx2(Icon, { name: "x" }) })
499
+ ] }),
500
+ /* @__PURE__ */ jsxs2("div", { className: "lalt-assistant-body", children: [
501
+ historyOpen ? /* @__PURE__ */ jsx2(HistoryList, { conversations, scopes, labels, onOpen: openConversation }) : empty ? /* @__PURE__ */ jsxs2("div", { children: [
502
+ /* @__PURE__ */ jsx2("h2", { className: "lalt-assistant-empty-title", children: labels.emptyTitle }),
503
+ labels.intro && /* @__PURE__ */ jsx2("p", { className: "lalt-assistant-intro", children: labels.intro }),
504
+ /* @__PURE__ */ jsx2("div", { className: "lalt-assistant-actions", children: quickActions.map((a) => /* @__PURE__ */ jsxs2(
505
+ "button",
506
+ {
507
+ type: "button",
508
+ onClick: () => send(a.prompt, a.label),
509
+ disabled: !canSend,
510
+ className: "lalt-assistant-action",
511
+ children: [
512
+ /* @__PURE__ */ jsx2("div", { className: "lalt-assistant-action-label", children: a.label }),
513
+ a.hint && /* @__PURE__ */ jsx2("div", { className: "lalt-assistant-muted lalt-assistant-small", children: a.hint })
514
+ ]
515
+ },
516
+ a.id
517
+ )) })
518
+ ] }) : /* @__PURE__ */ jsxs2("div", { className: "lalt-assistant-thread", children: [
519
+ thread.map(
520
+ (m, i) => m.role === "user" ? /* @__PURE__ */ jsxs2("div", { className: "lalt-assistant-user", children: [
521
+ m.content,
522
+ 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: [
523
+ /* @__PURE__ */ jsx2(Icon, { name: "file", size: 12 }),
524
+ a.filename
525
+ ] }, a.id)) })
526
+ ] }, i) : /* @__PURE__ */ jsxs2("div", { className: "lalt-assistant-answer", children: [
527
+ renderMarkdown ? renderMarkdown(m.content) : /* @__PURE__ */ jsx2("div", { className: "lalt-assistant-plain", children: m.content }),
528
+ renderMessageFooter?.(m)
529
+ ] }, i)
530
+ ),
531
+ (pending || loading) && /* @__PURE__ */ jsxs2("div", { className: "lalt-assistant-muted lalt-assistant-small lalt-assistant-status", children: [
532
+ /* @__PURE__ */ jsx2("span", { className: "lalt-assistant-spinner" }),
533
+ loading ? labels.loading : labels.thinking
534
+ ] })
535
+ ] }),
536
+ error && /* @__PURE__ */ jsx2("p", { role: "alert", className: "lalt-assistant-error", children: error }),
537
+ /* @__PURE__ */ jsx2("div", { ref: endRef })
538
+ ] }),
539
+ /* @__PURE__ */ jsx2("div", { className: "lalt-assistant-footer", children: /* @__PURE__ */ jsxs2("div", { className: "lalt-assistant-composer", children: [
540
+ (attachments.items.length > 0 || attachments.notice) && /* @__PURE__ */ jsxs2("div", { className: "lalt-assistant-chips", children: [
541
+ attachments.items.map((item) => /* @__PURE__ */ jsx2(AttachmentChip, { item, labels, onRemove: () => attachments.remove(item.localId) }, item.localId)),
542
+ attachments.notice && /* @__PURE__ */ jsx2("p", { className: "lalt-assistant-error", children: attachments.notice })
543
+ ] }),
544
+ /* @__PURE__ */ jsx2(
545
+ "textarea",
546
+ {
547
+ value: draft,
548
+ onChange: (e) => setDraft(e.target.value),
549
+ onKeyDown,
550
+ onPaste,
551
+ rows: 3,
552
+ placeholder: labels.placeholder,
553
+ "aria-label": "Message",
554
+ className: "lalt-assistant-textarea"
555
+ }
556
+ ),
557
+ /* @__PURE__ */ jsxs2("div", { className: "lalt-assistant-composer-row", children: [
558
+ attachments.enabled && /* @__PURE__ */ jsxs2(Fragment2, { children: [
559
+ /* @__PURE__ */ jsx2(
560
+ "button",
561
+ {
562
+ type: "button",
563
+ onClick: () => fileInput.current?.click(),
564
+ "aria-label": labels.attach,
565
+ title: labels.attach,
566
+ className: "lalt-assistant-icon-button",
567
+ children: /* @__PURE__ */ jsx2(Icon, { name: "paperclip" })
568
+ }
569
+ ),
570
+ /* @__PURE__ */ jsx2(
571
+ "input",
572
+ {
573
+ ref: fileInput,
574
+ type: "file",
575
+ multiple: true,
576
+ hidden: true,
577
+ "data-testid": "lalt-assistant-file-input",
578
+ accept: attachmentLimits ? [...attachmentLimits.imageTypes, ...attachmentLimits.fileTypes].join(",") : void 0,
579
+ onChange: (e) => {
580
+ attachments.add(Array.from(e.target.files ?? []));
581
+ e.target.value = "";
582
+ }
583
+ }
584
+ )
585
+ ] }),
586
+ /* @__PURE__ */ jsx2("div", { className: "lalt-assistant-grow" }),
587
+ /* @__PURE__ */ jsx2(
588
+ "button",
589
+ {
590
+ type: "button",
591
+ onClick: submit,
592
+ disabled: !canSend || !draft.trim() && attachments.done.length === 0,
593
+ "aria-label": labels.send,
594
+ className: "lalt-assistant-send",
595
+ children: /* @__PURE__ */ jsx2(Icon, { name: "arrowUp" })
596
+ }
597
+ )
598
+ ] })
599
+ ] }) })
600
+ ]
601
+ }
602
+ );
603
+ }
604
+ function AttachmentChip({ item, labels, onRemove }) {
605
+ return /* @__PURE__ */ jsxs2("div", { className: "lalt-assistant-chip", "data-status": item.status, title: item.error, children: [
606
+ item.previewUrl ? /* @__PURE__ */ jsx2("img", { src: item.previewUrl, alt: "", className: "lalt-assistant-chip-preview" }) : /* @__PURE__ */ jsx2(Icon, { name: "file", size: 14 }),
607
+ /* @__PURE__ */ jsxs2("span", { className: "lalt-assistant-chip-text", children: [
608
+ /* @__PURE__ */ jsx2("span", { className: "lalt-assistant-truncate", children: item.file.name }),
609
+ /* @__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) })
610
+ ] }),
611
+ item.status === "uploading" && /* @__PURE__ */ jsx2("span", { className: "lalt-assistant-spinner" }),
612
+ /* @__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 }) })
613
+ ] });
614
+ }
615
+ function HistoryList({
616
+ conversations,
617
+ scopes,
618
+ labels,
619
+ onOpen
620
+ }) {
621
+ if (conversations.length === 0) {
622
+ return /* @__PURE__ */ jsx2("p", { className: "lalt-assistant-muted lalt-assistant-history-empty", children: labels.noConversation });
623
+ }
624
+ return /* @__PURE__ */ jsx2("ul", { className: "lalt-assistant-history", children: conversations.map((c) => {
625
+ const scope = scopes.find((s) => s.id === c.scopeId)?.label;
626
+ return /* @__PURE__ */ jsx2("li", { children: /* @__PURE__ */ jsxs2("button", { type: "button", onClick: () => onOpen(c), children: [
627
+ /* @__PURE__ */ jsx2("div", { className: "lalt-assistant-truncate", children: c.title }),
628
+ /* @__PURE__ */ jsx2("div", { className: "lalt-assistant-muted lalt-assistant-small", children: [scope, relativeTime(c.updatedAt)].filter(Boolean).join(" \xB7 ") })
629
+ ] }) }, c.id);
630
+ }) });
631
+ }
632
+
633
+ // src/assistant-launcher.tsx
634
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
635
+ function AssistantLauncher(props) {
636
+ const [open, setOpen] = useState3(false);
637
+ const [showHistory, setShowHistory] = useState3(false);
638
+ const labels = { ...defaultLabels, ...props.labels };
639
+ const openPanel = (history) => {
640
+ setShowHistory(history);
641
+ setOpen(true);
642
+ };
643
+ if (open) return /* @__PURE__ */ jsx3(AssistantPanel, { ...props, initialHistory: showHistory, onClose: () => setOpen(false) });
644
+ return /* @__PURE__ */ jsxs3("div", { className: "lalt-assistant lalt-assistant-launcher", children: [
645
+ /* @__PURE__ */ jsxs3("button", { type: "button", onClick: () => openPanel(true), className: "lalt-assistant-launch lalt-assistant-launch-secondary", children: [
646
+ /* @__PURE__ */ jsx3(Icon, { name: "clock" }),
647
+ labels.history
648
+ ] }),
649
+ /* @__PURE__ */ jsxs3("button", { type: "button", onClick: () => openPanel(false), className: "lalt-assistant-launch lalt-assistant-launch-primary", children: [
650
+ /* @__PURE__ */ jsx3(Icon, { name: "bot" }),
651
+ labels.launcher
652
+ ] })
653
+ ] });
654
+ }
655
+ export {
656
+ AssistantLauncher,
657
+ AssistantPanel,
658
+ defaultLabels,
659
+ formatSize,
660
+ readMainScreen,
661
+ relativeTime
662
+ };
663
+ //# sourceMappingURL=index.js.map