@2lab.ai/ui 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +54 -2
  2. package/dist/agent-inbox.js +1061 -0
  3. package/dist/agent-inbox.js.map +1 -0
  4. package/dist/agent-queue.js +1610 -0
  5. package/dist/agent-queue.js.map +1 -0
  6. package/dist/agent-run.js +859 -0
  7. package/dist/agent-run.js.map +1 -0
  8. package/dist/ai-answer.js +687 -0
  9. package/dist/ai-answer.js.map +1 -0
  10. package/dist/components/agent-inbox/agent-inbox.d.ts +158 -0
  11. package/dist/components/agent-inbox/index.d.ts +2 -0
  12. package/dist/components/agent-inbox/styles.d.ts +6 -0
  13. package/dist/components/agent-queue/agent-queue.d.ts +41 -0
  14. package/dist/components/agent-queue/index.d.ts +6 -0
  15. package/dist/components/agent-queue/logic.d.ts +261 -0
  16. package/dist/components/agent-queue/strings.d.ts +473 -0
  17. package/dist/components/agent-queue/styles.d.ts +8 -0
  18. package/dist/components/agent-run/agent-run.d.ts +273 -0
  19. package/dist/components/agent-run/index.d.ts +2 -0
  20. package/dist/components/agent-run/styles.d.ts +6 -0
  21. package/dist/components/ai-answer/ai-answer.d.ts +92 -0
  22. package/dist/components/ai-answer/index.d.ts +2 -0
  23. package/dist/components/ai-answer/styles.d.ts +2 -0
  24. package/dist/components/context-window/context-window.d.ts +156 -0
  25. package/dist/components/context-window/index.d.ts +2 -0
  26. package/dist/components/context-window/styles.d.ts +2 -0
  27. package/dist/components/memory-panel/index.d.ts +2 -0
  28. package/dist/components/memory-panel/memory-panel.d.ts +101 -0
  29. package/dist/components/memory-panel/styles.d.ts +2 -0
  30. package/dist/components/model-picker/index.d.ts +2 -0
  31. package/dist/components/model-picker/model-picker.d.ts +136 -0
  32. package/dist/components/model-picker/morph.d.ts +9 -0
  33. package/dist/components/model-picker/styles.d.ts +6 -0
  34. package/dist/context-window.js +384 -0
  35. package/dist/context-window.js.map +1 -0
  36. package/dist/core/element.d.ts +39 -0
  37. package/dist/core/format.d.ts +14 -0
  38. package/dist/core/html.d.ts +10 -0
  39. package/dist/core/tokens.d.ts +14 -0
  40. package/dist/format-BBGWXPzk.js +51 -0
  41. package/dist/format-BBGWXPzk.js.map +1 -0
  42. package/dist/html-DZWnrdcg.js +167 -0
  43. package/dist/html-DZWnrdcg.js.map +1 -0
  44. package/dist/index.d.ts +15 -0
  45. package/dist/index.js +116 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/memory-panel.js +1255 -0
  48. package/dist/memory-panel.js.map +1 -0
  49. package/dist/model-picker.js +858 -0
  50. package/dist/model-picker.js.map +1 -0
  51. package/dist/tokens.css +4 -0
  52. package/package.json +75 -4
@@ -0,0 +1,687 @@
1
+ import { d as define, U as UiElement, e as esc, f as escAttr, s as safeHref, p as prefersReducedMotion } from "./html-DZWnrdcg.js";
2
+ const styles = `
3
+ .ui-root { container: ans / inline-size; }
4
+ .ans-card { position: relative; padding: 12px 14px 10px; border: 1px solid var(--border); border-radius: 12px; background: var(--surface); color: var(--text); box-shadow: var(--ib-shadow); overflow: hidden; }
5
+ .ans-card.is-in > * { animation: ans-in var(--dur-fade) var(--ease-out) both; }
6
+ .ans-title { margin: 0 0 10px; font-size: 15px; font-weight: 600; line-height: 1.35; letter-spacing: -0.01em; color: var(--text); overflow-wrap: anywhere; }
7
+
8
+ /* Link cards (D6): a sideways row, scrollbar hidden, both ends fade. */
9
+ .ans-srcs { display: flex; gap: 8px; margin: -4px -14px 8px; padding: 4px 14px; list-style: none; overflow-x: auto; scrollbar-width: none;
10
+ -webkit-mask: linear-gradient(90deg, transparent, black 12px, black calc(100% - 12px), transparent);
11
+ mask: linear-gradient(90deg, transparent, black 12px, black calc(100% - 12px), transparent); }
12
+ .ans-srcs::-webkit-scrollbar { display: none; }
13
+ .ans-srcs > li { flex: none; display: flex; }
14
+ .ans-src { display: flex; flex-direction: column; gap: 4px; width: 148px; min-height: 64px; padding: 8px 10px; border-radius: 10px; background: var(--surface-raised); color: inherit; text-decoration: none; transition: background-color var(--dur-press) ease, box-shadow var(--dur-press) ease; }
15
+ .ans-card.is-in .ans-src { animation: ans-pop var(--dur-move) var(--ease-out) both; animation-delay: calc(var(--ans-i, 0) * 60ms); }
16
+ .ans-src:hover { background: var(--ib-pill); }
17
+ .ans-src.is-lit { box-shadow: inset 0 0 0 1.5px var(--text-tertiary); }
18
+ .ans-src-site { display: flex; align-items: center; gap: 6px; min-width: 0; font-size: 11px; color: var(--text-secondary); }
19
+ .ans-src-name { flex: 1; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
20
+ .ans-src-num { flex: none; font-variant-numeric: tabular-nums; color: var(--text-tertiary); }
21
+ .ans-src-title { display: -webkit-box; overflow: hidden; -webkit-line-clamp: 2; -webkit-box-orient: vertical; font-size: 12px; line-height: 1.4; color: var(--text); word-break: break-word; }
22
+
23
+ /* Label (D9) and body (D2). */
24
+ .ans-label { margin: 0 0 6px; font-size: 12px; font-weight: 600; color: var(--text-secondary); }
25
+ .ans-body { font-size: 13.5px; line-height: 1.6; }
26
+ .ans-body > :last-child { margin-bottom: 0; }
27
+
28
+ /* The document (dashboard .md-doc). */
29
+ .md-doc { color: var(--text); word-break: break-word; }
30
+ .md-doc h1, .md-doc h2, .md-doc h3, .md-doc h4, .md-doc h5, .md-doc h6 { margin: 1.3em 0 0.5em; line-height: 1.3; letter-spacing: -0.01em; }
31
+ .md-doc h1, .md-doc h2 { font-size: 1.15em; }
32
+ .md-doc h3, .md-doc h4, .md-doc h5, .md-doc h6 { font-size: 1em; }
33
+ .md-doc > :first-child { margin-top: 0; }
34
+ .md-doc p, .md-doc ul, .md-doc ol, .md-doc blockquote, .md-doc pre, .md-doc .md-table { margin: 0 0 0.9em; }
35
+ .md-doc ul, .md-doc ol { padding-left: 1.4em; }
36
+ .md-doc li + li { margin-top: 0.2em; }
37
+ .md-doc li > ul, .md-doc li > ol { margin: 0.2em 0 0; }
38
+ .md-doc code { padding: 0.1em 0.35em; border-radius: 4px; background: var(--ib-pill); font: 0.88em var(--mono); }
39
+ .md-doc pre { padding: 12px 14px; overflow: auto; border: 1px solid var(--border); border-radius: 8px; background: var(--ib-col); }
40
+ .md-doc pre code { padding: 0; background: none; font-size: 12.5px; line-height: 1.55; }
41
+ .md-doc blockquote { padding: 2px 0 2px 12px; border-left: 3px solid var(--border-focus); color: var(--text-secondary); }
42
+ .md-doc blockquote > :last-child { margin-bottom: 0; }
43
+ .md-doc hr { margin: 1.4em 0; border: 0; border-top: 1px solid var(--border); }
44
+ .md-doc a { color: var(--accent); }
45
+ .md-doc .md-table { overflow-x: auto; }
46
+ .md-doc table { border-collapse: collapse; font-size: 0.93em; }
47
+ .md-doc th, .md-doc td { padding: 6px 10px; border: 1px solid var(--border); text-align: left; vertical-align: top; }
48
+ .md-doc th { background: var(--ib-col); font-weight: 600; }
49
+
50
+ /* Link chips (D7) — lit = inverted (D8). */
51
+ .ans-body .ans-cite { display: inline-block; max-width: 16ch; height: 18px; margin: 0 1px 0 3px; padding: 0 6px; overflow: hidden; border-radius: 6px; background: var(--surface-raised); font-size: 11px; font-weight: 500; line-height: 18px; color: var(--text-secondary); text-decoration: none; white-space: nowrap; text-overflow: ellipsis; vertical-align: middle; transition: background-color var(--dur-press) ease, color var(--dur-press) ease; }
52
+ .ans-body .ans-cite:hover { background: var(--ib-pill); color: var(--text); }
53
+ .ans-body .ans-cite.is-lit { background: var(--text); color: var(--surface); }
54
+ .ans-nb { white-space: nowrap; }
55
+ .ans-cite:focus-visible, .ans-src:focus-visible { outline: 2px solid var(--border-focus); outline-offset: 2px; }
56
+
57
+ /* Footer (D10): Copy · status · response time. */
58
+ .ans-foot { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--border); }
59
+ .ans-btn { display: inline-flex; flex: none; align-items: center; justify-content: center; gap: 5px; min-height: 32px; padding: 5px 12px; border: 1px solid var(--ib-card-border); border-radius: 8px; background: var(--ib-card); font-size: 13px; font-weight: 500; color: var(--ib-text); white-space: nowrap; transition: background-color 150ms ease, border-color 150ms ease, color 150ms ease; }
60
+ .ans-btn svg { width: 13px; height: 13px; }
61
+ .ans-copy[data-copied] { color: var(--green); }
62
+ .ans-status { flex: 1 1 0; min-width: 0; font-size: 12px; color: var(--text-tertiary); }
63
+ .ans-end { display: inline-flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 6px 10px; margin-left: auto; min-width: 0; }
64
+ .ans-took { font-size: 11.5px; color: var(--text-tertiary); font-variant-numeric: tabular-nums; white-space: nowrap; }
65
+ @media (hover: hover) and (pointer: fine) { .ans-btn:hover { border-color: var(--ib-muted); } }
66
+
67
+ /* Writing (D12): a sweep over the words + a pulsing run-colour dot. */
68
+ .ans-foot.is-writing { min-height: 41px; }
69
+ .ans-writing { display: flex; align-items: center; gap: 8px; font-size: 12px; }
70
+ .ans-shimmer { color: transparent; background: linear-gradient(90deg, var(--text-tertiary) 38%, var(--text) 50%, var(--text-tertiary) 62%) 100% 0 / 300% 100% no-repeat; -webkit-background-clip: text; background-clip: text; animation: ans-sweep 1.6s linear infinite; }
71
+ .ans-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--st-run); animation: ans-pulse 1s ease-in-out infinite; }
72
+
73
+ /* Loading · empty · failed (D13). */
74
+ .ans-state { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; margin: 0; font-size: 12px; color: var(--text-tertiary); }
75
+ .ans-error { font-size: 12px; color: var(--st-fail-text); overflow-wrap: anywhere; }
76
+
77
+ @keyframes ans-in { from { opacity: 0; filter: blur(2px); transform: translateY(3px); } to { opacity: 1; filter: none; transform: none; } }
78
+ @keyframes ans-pop { from { opacity: 0; transform: translateY(6px) scale(0.96); } to { opacity: 1; transform: none; } }
79
+ @keyframes ans-sweep { from { background-position: 100% 0; } to { background-position: 0 0; } }
80
+ @keyframes ans-pulse { 50% { transform: scale(0.6); opacity: 0.45; } }
81
+
82
+ @container ans (min-width: 520px) {
83
+ .ans-card { padding: 16px 18px 12px; }
84
+ .ans-title { font-size: 16.5px; }
85
+ .ans-srcs { margin: -4px -18px 10px; padding: 4px 18px; }
86
+ .ans-src { width: 172px; }
87
+ .ans-body { font-size: 14px; }
88
+ }
89
+
90
+ /* Touch (D16): the chip's hit area grows, chips are never cut, buttons are 44px. */
91
+ @media (pointer: coarse) {
92
+ .ans-body .ans-cite { position: relative; overflow: visible; max-width: none; }
93
+ .ans-cite::after { content: ''; position: absolute; inset: -10px -4px; }
94
+ .ans-btn { min-height: 44px; }
95
+ }
96
+ /* The global rule only shortens durations, so infinite loops would flicker: stop them. */
97
+ @media (prefers-reduced-motion: reduce) {
98
+ .ans-shimmer, .ans-dot, .ans-src, .ans-card.is-in > * { animation: none !important; }
99
+ .ans-shimmer { background: none; color: var(--text-tertiary); }
100
+ .ans-src, .ans-body .ans-cite { transition: none; }
101
+ }
102
+ `;
103
+ const COPIED_MS = 1500;
104
+ const GLIDE_MS = 600;
105
+ const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
106
+ const STRINGS = {
107
+ en: {
108
+ label: (n) => n ? `Answer · ${plural(n, "link", "links")}` : "Answer",
109
+ list: (n) => `${plural(n, "link", "links")} in this answer`,
110
+ cite: (n, title, name) => `Link ${n}: ${title} (${name})`,
111
+ issue: "Issue",
112
+ commit: "Commit",
113
+ linksHead: "Links:",
114
+ copy: "Copy",
115
+ copied: "Copied",
116
+ copiedSay: (n) => n ? `Copied the answer and ${plural(n, "link", "links")}` : "Copied the answer",
117
+ selected: "Selected — press ⌘C / Ctrl+C to copy",
118
+ writing: "Writing the answer",
119
+ loading: "Loading…",
120
+ empty: "No answer text was recorded",
121
+ failed: "Couldn’t get the answer",
122
+ retry: "Try again",
123
+ under: "Responded in under 1s",
124
+ secs: (s) => `Responded in ${s}s`,
125
+ mins: (m, s) => `Responded in ${m}m ${s}s`,
126
+ hours: (h, m) => `Responded in ${h}h ${m}m`
127
+ },
128
+ ko: {
129
+ label: (n) => n ? `답변 · 링크 ${n}` : "답변",
130
+ list: (n) => `답변에 포함된 링크 ${n}개`,
131
+ cite: (n, title, name) => `링크 ${n}: ${title} (${name})`,
132
+ issue: "이슈",
133
+ commit: "커밋",
134
+ linksHead: "링크:",
135
+ copy: "복사",
136
+ copied: "복사됨",
137
+ copiedSay: (n) => n ? `답변과 링크 ${n}개를 복사했습니다` : "답변을 복사했습니다",
138
+ selected: "선택했습니다 — ⌘C / Ctrl+C로 복사",
139
+ writing: "응답 작성 중",
140
+ loading: "불러오는 중…",
141
+ empty: "기록된 답변 내용이 없습니다",
142
+ failed: "답변을 불러오지 못했습니다",
143
+ retry: "다시 시도",
144
+ under: "응답 1초 미만",
145
+ secs: (s) => `응답 ${s}초`,
146
+ mins: (m, s) => `응답 ${m}분 ${s}초`,
147
+ hours: (h, m) => `응답 ${h}시간 ${m}분`
148
+ }
149
+ };
150
+ const strings = (lang) => lang === "ko" ? STRINGS.ko : STRINGS.en;
151
+ const HTTP = /^https?:\/\//i;
152
+ const CODE = "";
153
+ const LINK = "";
154
+ function unesc(s) {
155
+ return s.replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
156
+ }
157
+ const textOf = (html) => unesc(html.replace(/<[^>]*>/g, ""));
158
+ function answerLinkKey(href) {
159
+ return String(href ?? "").replace(/\/+$/, "");
160
+ }
161
+ function decode(s) {
162
+ try {
163
+ return decodeURIComponent(s);
164
+ } catch {
165
+ return s;
166
+ }
167
+ }
168
+ const encodeParens = (u) => u.replace(/\(/g, "%28").replace(/\)/g, "%29");
169
+ const decodeParens = (u) => u.replace(/%28/gi, "(").replace(/%29/gi, ")");
170
+ function linkifyText(s) {
171
+ return s.replace(
172
+ /<(https?:\/\/[^\s<>|]+)(?:\|([^<>]*))?>|https?:\/\/[^\s<>[\]"'`]+/gi,
173
+ (m, url, label) => {
174
+ if (url) {
175
+ const l = String(label ?? "").replace(/[[\]]/g, "").trim();
176
+ return `[${l || url}](${encodeParens(url)})`;
177
+ }
178
+ let u = m;
179
+ let tail = "";
180
+ for (let guard = 0; guard < 8; guard++) {
181
+ const p = u.match(/[.,;:!?*]+$/);
182
+ if (p) {
183
+ tail = p[0] + tail;
184
+ u = u.slice(0, u.length - p[0].length);
185
+ continue;
186
+ }
187
+ if (u.endsWith(")") && u.split("(").length < u.split(")").length) {
188
+ tail = `)${tail}`;
189
+ u = u.slice(0, -1);
190
+ continue;
191
+ }
192
+ break;
193
+ }
194
+ return `[${u}](${encodeParens(u)})${tail}`;
195
+ }
196
+ );
197
+ }
198
+ function answerLinkify(raw) {
199
+ let fence = "";
200
+ let fenceQuoted = false;
201
+ return String(raw ?? "").replace(/\r\n?/g, "\n").split("\n").map((line) => {
202
+ const quoted = /^\s{0,3}>/.test(line);
203
+ const body = line.replace(/^(\s{0,3}>\s?)+/, "");
204
+ if (fence && fenceQuoted && !quoted) fence = "";
205
+ if (fence) {
206
+ if (body.trim().startsWith(fence)) fence = "";
207
+ return line;
208
+ }
209
+ const f = body.match(/^\s{0,3}(`{3,}|~{3,})/);
210
+ if (f) {
211
+ fence = f[1];
212
+ fenceQuoted = quoted;
213
+ return line;
214
+ }
215
+ return line.split(/(!?\[[^\]]+\]\((?:[^()\s]|\([^()\s]*\))+\)|`[^`]+`)/).map((part, i) => {
216
+ if (!(i % 2)) return linkifyText(part);
217
+ if (part.startsWith("`")) return part;
218
+ const at = part.indexOf("](");
219
+ return `${part.slice(0, at + 2)}${encodeParens(part.slice(at + 2, -1))})`;
220
+ }).join("");
221
+ }).join("\n");
222
+ }
223
+ const plainLink = (href, label) => `<a href="${safeHref(href)}" target="_blank" rel="noopener noreferrer">${label}</a>`;
224
+ function marks(t) {
225
+ return t.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/(^|[^*\w])\*([^*\s][^*]*?)\*(?!\w)/g, "$1<em>$2</em>").replace(/~~([^~]+)~~/g, "<del>$1</del>");
226
+ }
227
+ function mdInline(s, link) {
228
+ const codes = [];
229
+ const rawCodes = [];
230
+ const links = [];
231
+ const restoreCodes = (x) => x.replace(/\uE000(\d+)\uE000/g, (_m, i) => codes[Number(i)] ?? "");
232
+ const restoreRaw = (x) => x.replace(/\uE000(\d+)\uE000/g, (_m, i) => `\`${rawCodes[Number(i)] ?? ""}\``);
233
+ let t = String(s ?? "").replace(/[\uE000\uE001]/g, "").replace(/`([^`]+)`/g, (_m, c) => {
234
+ codes.push(`<code>${escAttr(c)}</code>`);
235
+ rawCodes.push(c);
236
+ return `${CODE}${codes.length - 1}${CODE}`;
237
+ });
238
+ t = esc(t);
239
+ t = t.replace(/!?\[([^\]]+)\]\(([^)\s]+)\)/g, (_m, label, href) => {
240
+ const url = restoreRaw(unesc(href));
241
+ if (!HTTP.test(url)) return label;
242
+ links.push(link(url, restoreCodes(marks(label))));
243
+ return `${LINK}${links.length - 1}${LINK}`;
244
+ });
245
+ t = marks(t);
246
+ t = t.replace(/\uE001(\d+)\uE001/g, (_m, i) => links[Number(i)] ?? "");
247
+ return restoreCodes(t);
248
+ }
249
+ function answerMarkdown(src, link = plainLink) {
250
+ const lines = String(src ?? "").replace(/\r\n?/g, "\n").split("\n");
251
+ const out = [];
252
+ let para = [];
253
+ let i = 0;
254
+ const inline = (s) => mdInline(s, link);
255
+ const flush = () => {
256
+ if (para.length) out.push(`<p>${para.map(inline).join("<br>")}</p>`);
257
+ para = [];
258
+ };
259
+ const cells = (l) => l.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
260
+ const listRe = /^(\s*)([-*+]|\d{1,3}[.)])\s+(.*)$/;
261
+ while (i < lines.length) {
262
+ const line = lines[i];
263
+ const fence = line.match(/^\s{0,3}(`{3,}|~{3,})\s*([\w+#.-]*)/);
264
+ if (fence) {
265
+ flush();
266
+ const code = [];
267
+ i++;
268
+ while (i < lines.length && !lines[i].trim().startsWith(fence[1])) {
269
+ code.push(lines[i]);
270
+ i++;
271
+ }
272
+ i++;
273
+ out.push(
274
+ `<pre class="md-code"><code${fence[2] ? ` data-lang="${escAttr(fence[2])}"` : ""}>${esc(code.join("\n"))}</code></pre>`
275
+ );
276
+ continue;
277
+ }
278
+ if (!line.trim()) {
279
+ flush();
280
+ i++;
281
+ continue;
282
+ }
283
+ const h = line.match(/^\s{0,3}(#{1,6})\s+(.*?)\s*#*\s*$/);
284
+ if (h) {
285
+ flush();
286
+ out.push(`<h${h[1].length}>${inline(h[2])}</h${h[1].length}>`);
287
+ i++;
288
+ continue;
289
+ }
290
+ if (/^\s{0,3}([-*_])(\s*\1){2,}\s*$/.test(line)) {
291
+ flush();
292
+ out.push("<hr>");
293
+ i++;
294
+ continue;
295
+ }
296
+ if (/^\s{0,3}>/.test(line)) {
297
+ flush();
298
+ const quote = [];
299
+ while (i < lines.length && /^\s{0,3}>/.test(lines[i])) {
300
+ quote.push(lines[i].replace(/^\s{0,3}>\s?/, ""));
301
+ i++;
302
+ }
303
+ out.push(`<blockquote>${answerMarkdown(quote.join("\n"), link)}</blockquote>`);
304
+ continue;
305
+ }
306
+ if (line.includes("|") && i + 1 < lines.length && /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?\s*$/.test(lines[i + 1])) {
307
+ flush();
308
+ const head = cells(line);
309
+ const rows = [];
310
+ i += 2;
311
+ while (i < lines.length && lines[i].includes("|") && lines[i].trim()) {
312
+ rows.push(cells(lines[i]));
313
+ i++;
314
+ }
315
+ out.push(
316
+ `<div class="md-table"><table><thead><tr>${head.map((c) => `<th>${inline(c)}</th>`).join("")}</tr></thead><tbody>${rows.map((r) => `<tr>${head.map((_c, k) => `<td>${inline(r[k] ?? "")}</td>`).join("")}</tr>`).join("")}</tbody></table></div>`
317
+ );
318
+ continue;
319
+ }
320
+ if (listRe.test(line)) {
321
+ flush();
322
+ const items = [];
323
+ while (i < lines.length) {
324
+ const m = lines[i].match(listRe);
325
+ if (m) {
326
+ items.push({ indent: m[1].replace(/\t/g, " ").length, ordered: /\d/.test(m[2]), text: m[3] });
327
+ i++;
328
+ continue;
329
+ }
330
+ if (items.length && /^\s{2,}\S/.test(lines[i])) {
331
+ items[items.length - 1].text += ` ${lines[i].trim()}`;
332
+ i++;
333
+ continue;
334
+ }
335
+ break;
336
+ }
337
+ let html = "";
338
+ const stack = [];
339
+ for (const it of items) {
340
+ while (stack.length > 1 && it.indent < stack[stack.length - 1].indent) html += `</li></${stack.pop().tag}>`;
341
+ const top = stack[stack.length - 1];
342
+ if (!top || it.indent > top.indent) {
343
+ const tag = it.ordered ? "ol" : "ul";
344
+ stack.push({ indent: it.indent, tag });
345
+ html += `<${tag}><li>`;
346
+ } else {
347
+ html += "</li><li>";
348
+ }
349
+ html += inline(it.text);
350
+ }
351
+ while (stack.length) html += `</li></${stack.pop().tag}>`;
352
+ out.push(html);
353
+ continue;
354
+ }
355
+ para.push(line.trim());
356
+ i++;
357
+ }
358
+ flush();
359
+ return out.join("\n");
360
+ }
361
+ function answerLinkInfo(href, label = "", lang = "en", titles) {
362
+ const t = strings(lang);
363
+ const url = String(href ?? "");
364
+ const m = url.match(/^https?:\/\/([^/?#]+)([^?#]*)(\?[^#]*)?/i);
365
+ const host = m ? m[1].toLowerCase().replace(/^www\./, "") : url;
366
+ const path = m ? m[2] : "";
367
+ let name = host;
368
+ let chip = host;
369
+ let title = "";
370
+ if (host === "github.com") {
371
+ name = "GitHub";
372
+ chip = "GitHub";
373
+ const seg = path.split("/").filter(Boolean);
374
+ const repo = seg.slice(0, 2).join("/");
375
+ const num = seg.length >= 4 && /^\d+$/.test(seg[3]) ? seg[3] : "";
376
+ if (num && seg[2] === "pull") {
377
+ title = `PR #${num} · ${repo}`;
378
+ chip = `#${num}`;
379
+ } else if (num && seg[2] === "issues") {
380
+ title = `${t.issue} #${num} · ${repo}`;
381
+ chip = `#${num}`;
382
+ } else if (seg.length >= 4 && seg[2] === "commit" && /^[0-9a-f]{7,40}$/i.test(seg[3])) {
383
+ chip = seg[3].slice(0, 7);
384
+ title = `${t.commit} ${chip} · ${repo}`;
385
+ } else title = repo ? repo + (seg.length > 2 ? ` · ${decode(seg.slice(2).join("/"))}` : "") : "github.com";
386
+ } else {
387
+ title = decode((path + (m?.[3] ?? "")).replace(/\/+$/, "")) || host;
388
+ }
389
+ if (label) title = label;
390
+ if (titles) {
391
+ const key = answerLinkKey(url);
392
+ for (const [u, known] of Object.entries(titles)) if (known && answerLinkKey(u) === key) title = String(known);
393
+ }
394
+ return { href: url, name, title, chip };
395
+ }
396
+ function citeHtml(link, k, t) {
397
+ return `<a class="ans-cite" data-n="${link.n}" data-key="cite-${k}" href="${safeHref(link.href)}" target="_blank" rel="noopener noreferrer" aria-label="${escAttr(
398
+ t.cite(link.n, link.title, link.name)
399
+ )}">${esc(link.chip)}</a>`;
400
+ }
401
+ function renderAnswer(markdown, opts = {}) {
402
+ const t = strings(opts.lang);
403
+ const md = answerLinkify(markdown);
404
+ const byKey = /* @__PURE__ */ new Map();
405
+ const found = [];
406
+ answerMarkdown(md, (url, labelHtml) => {
407
+ const href = decodeParens(url);
408
+ const text = textOf(labelHtml);
409
+ const key = answerLinkKey(href);
410
+ const label = text && text !== href ? text : "";
411
+ const seen = byKey.get(key);
412
+ if (!seen) {
413
+ const f = { href, label, n: found.length + 1 };
414
+ byKey.set(key, f);
415
+ found.push(f);
416
+ } else if (!seen.label && label) seen.label = label;
417
+ return "";
418
+ });
419
+ const links = found.map((f) => ({ ...answerLinkInfo(f.href, f.label, opts.lang, opts.titles), n: f.n }));
420
+ let k = 0;
421
+ const html = answerMarkdown(md, (url, labelHtml) => {
422
+ const href = decodeParens(url);
423
+ const link = links[(byKey.get(answerLinkKey(href))?.n ?? 1) - 1];
424
+ return (textOf(labelHtml) === href ? "" : labelHtml) + citeHtml(link, k++, t);
425
+ });
426
+ return {
427
+ html: html.replace(
428
+ /(<a class="ans-cite"[^>]*>[^<]*<\/a>)([^\s<]{1,12})(?![^\s<])/g,
429
+ '<span class="ans-nb">$1$2</span>'
430
+ ),
431
+ links
432
+ };
433
+ }
434
+ function answerCopyText(markdown, links, lang = "en") {
435
+ const text = String(markdown ?? "").trim();
436
+ if (!links.length) return text;
437
+ return `${text}
438
+
439
+ ${strings(lang).linksHead}
440
+ ${links.map((l) => `[${l.n}] ${l.title} – ${l.href}`).join("\n")}`;
441
+ }
442
+ function answerTookLabel(ms, lang = "en") {
443
+ const t = strings(lang);
444
+ const v = Number(ms);
445
+ if (!Number.isFinite(v) || v <= 0) return "";
446
+ if (v < 1e3) return t.under;
447
+ const sec = Math.floor(v / 1e3);
448
+ if (sec < 60) return t.secs(sec);
449
+ const min = Math.floor(sec / 60);
450
+ if (min < 60) return t.mins(min, sec % 60);
451
+ return t.hours(Math.floor(min / 60), min % 60);
452
+ }
453
+ const CHECK_SVG = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg>';
454
+ class UiAiAnswer extends UiElement {
455
+ static styles = [styles];
456
+ static get observedAttributes() {
457
+ return ["lang"];
458
+ }
459
+ #links = [];
460
+ #lit = 0;
461
+ #copied = false;
462
+ #status = "";
463
+ #timer = 0;
464
+ #shownMd = "";
465
+ #glide = null;
466
+ constructor() {
467
+ super();
468
+ this.root.addEventListener("mouseover", (e) => this.#hover(e, true));
469
+ this.root.addEventListener("mouseout", (e) => this.#hover(e, false));
470
+ this.root.addEventListener("focusin", (e) => this.#hover(e, true));
471
+ this.root.addEventListener("focusout", (e) => this.#hover(e, false));
472
+ this.root.addEventListener("click", (e) => {
473
+ const t = e.target;
474
+ const a = t?.closest?.("a[data-n]");
475
+ if (a) {
476
+ this.#follow(e, a);
477
+ return;
478
+ }
479
+ if (t?.closest?.(".ans-copy")) this.#copy();
480
+ else if (t?.closest?.(".ans-retry")) this.emit("ui-retry", {});
481
+ });
482
+ }
483
+ attributeChangedCallback() {
484
+ this.requestRender();
485
+ }
486
+ disconnectedCallback() {
487
+ clearTimeout(this.#timer);
488
+ this.#copied = false;
489
+ this.#status = "";
490
+ }
491
+ /** The links of the last render, in card order. */
492
+ get links() {
493
+ return this.#links.slice();
494
+ }
495
+ get #lang() {
496
+ return this.getAttribute("lang") === "ko" ? "ko" : "en";
497
+ }
498
+ update() {
499
+ const left = this.$(".ans-srcs")?.scrollLeft ?? 0;
500
+ super.update();
501
+ const row = this.$(".ans-srcs");
502
+ if (row && left) row.scrollLeft = left;
503
+ }
504
+ render() {
505
+ const d = this.data;
506
+ if (!d) {
507
+ this.#links = [];
508
+ return "";
509
+ }
510
+ const lang = this.#lang;
511
+ const t = strings(lang);
512
+ const md = String(d.markdown ?? "");
513
+ const has = md.trim() !== "";
514
+ const streaming = !!d.streaming;
515
+ const error = d.error ? String(d.error) : "";
516
+ const title = d.title ? `<h3 class="ans-title">${esc(d.title)}</h3>` : "";
517
+ const retry = error && d.canRetry ? `<button type="button" class="ans-btn ans-retry" data-key="retry">${esc(t.retry)}</button>` : "";
518
+ const failed = `<span class="ans-error">${esc(t.failed)}${error ? ` (${esc(error)})` : ""}</span>`;
519
+ const prev = this.#shownMd;
520
+ this.#shownMd = md;
521
+ const enter = has && !streaming && !(prev && md.startsWith(prev));
522
+ if (!has && !streaming) {
523
+ this.#links = [];
524
+ const state = error ? `<div class="ans-state">${failed}${retry}</div>` : d.loading ? `<p class="ans-state ans-loading">${esc(t.loading)}</p>` : `<p class="ans-state ans-empty">${esc(t.empty)}</p>`;
525
+ return `<section class="ans-card" aria-label="${escAttr(t.label(0))}"${d.loading && !error ? ' aria-busy="true"' : ""}>${title}${state}</section>`;
526
+ }
527
+ const a = has ? renderAnswer(md, { lang, titles: d.linkTitles }) : { html: "", links: [] };
528
+ this.#links = a.links;
529
+ const n = a.links.length;
530
+ const srcs = n ? `<ol class="ans-srcs" aria-label="${escAttr(t.list(n))}">${a.links.map(
531
+ (l, i) => `<li style="--ans-i:${Math.min(i, 6)}"><a class="ans-src" data-n="${l.n}" data-key="src-${l.n}" href="${safeHref(
532
+ l.href
533
+ )}" target="_blank" rel="noopener noreferrer" title="${escAttr(l.href)}"><span class="ans-src-site"><span class="ans-src-name">${esc(
534
+ l.name
535
+ )}</span><span class="ans-src-num">${l.n}</span></span><span class="ans-src-title">${esc(l.title)}</span></a></li>`
536
+ ).join("")}</ol>` : "";
537
+ const body = has ? `<div class="ans-body md-doc">${a.html}</div>` : "";
538
+ let foot;
539
+ if (streaming) {
540
+ foot = `<div class="ans-foot is-writing"><span class="ans-writing"><span class="ans-shimmer">${esc(
541
+ t.writing
542
+ )}</span><i class="ans-dot" aria-hidden="true"></i></span></div>`;
543
+ } else {
544
+ const took = answerTookLabel(Number(d.responseMs), lang);
545
+ const right = error ? `${failed}${retry}` : took ? `<span class="ans-took">${esc(took)}</span>` : "";
546
+ foot = `<div class="ans-foot"><button type="button" class="ans-btn ans-copy" data-key="copy"${this.#copied ? " data-copied" : ""}>${this.#copyLabel(t)}</button><span class="ans-status" role="status" aria-live="polite">${esc(
547
+ this.#status
548
+ )}</span>${right ? `<span class="ans-end">${right}</span>` : ""}</div>`;
549
+ }
550
+ return `<section class="ans-card${enter ? " is-in" : ""}" aria-labelledby="ans-label"${streaming ? ' aria-busy="true"' : ""}>${title}${srcs}<div class="ans-label" id="ans-label">${esc(
551
+ t.label(n)
552
+ )}</div>${body}${foot}</section>`;
553
+ }
554
+ #copyLabel(t) {
555
+ return this.#copied ? `${CHECK_SVG}<span>${esc(t.copied)}</span>` : `<span>${esc(t.copy)}</span>`;
556
+ }
557
+ updated() {
558
+ if (this.#lit && !this.#links.some((l) => l.n === this.#lit)) this.#lit = 0;
559
+ this.#paintLit();
560
+ }
561
+ /** Hover / focus on a chip or card lights its number (D8); leaving clears. */
562
+ #hover(e, on) {
563
+ const el = e.target?.closest?.("[data-n]");
564
+ if (!el) return;
565
+ const to = e.relatedTarget;
566
+ if (!on && to?.closest?.("[data-n]") === el) return;
567
+ const n = on ? Number(el.getAttribute("data-n")) : 0;
568
+ if (n === this.#lit) return;
569
+ this.#lit = n;
570
+ this.#paintLit();
571
+ if (n) this.#reveal(n);
572
+ }
573
+ #paintLit() {
574
+ const key = String(this.#lit);
575
+ for (const el of this.$$("[data-n]"))
576
+ el.classList.toggle("is-lit", !!this.#lit && el.getAttribute("data-n") === key);
577
+ }
578
+ /** A lit card outside the row slides the row sideways — never the page (no scrollIntoView). */
579
+ #reveal(n) {
580
+ const row = this.$(".ans-srcs");
581
+ const src = row?.querySelector(`.ans-src[data-n="${n}"]`);
582
+ if (!row || !src || typeof row.scrollTo !== "function") return;
583
+ const now = Date.now();
584
+ const at = this.#glide && now < this.#glide.until ? this.#glide.left : row.scrollLeft;
585
+ const shift = row.scrollLeft - at;
586
+ const rr = row.getBoundingClientRect();
587
+ const sr = src.getBoundingClientRect();
588
+ const l = sr.left + shift;
589
+ const r = sr.right + shift;
590
+ const target = r > rr.right ? at + (r - rr.right) + 16 : l < rr.left ? at - (rr.left - l) - 16 : null;
591
+ if (target === null) return;
592
+ const left = Math.max(0, Math.min(target, row.scrollWidth - row.clientWidth));
593
+ const smooth = !prefersReducedMotion();
594
+ this.#glide = smooth ? { left, until: now + GLIDE_MS } : null;
595
+ row.scrollTo({ left, behavior: smooth ? "smooth" : "auto" });
596
+ }
597
+ #follow(e, a) {
598
+ const n = Number(a.getAttribute("data-n"));
599
+ const link = this.#links[n - 1];
600
+ if (!link) return;
601
+ const from = a.classList.contains("ans-cite") ? "chip" : "card";
602
+ if (!this.emit("ui-link", { url: link.href, index: n - 1, from })) e.preventDefault();
603
+ }
604
+ #copy() {
605
+ const d = this.data;
606
+ const md = String(d?.markdown ?? "");
607
+ if (!d || d.streaming || !md.trim()) return;
608
+ const text = answerCopyText(md, this.#links, this.#lang);
609
+ if (!this.emit("ui-copy", { text })) return;
610
+ const done = () => this.#flash(true, strings(this.#lang).copiedSay(this.#links.length));
611
+ const fallback = () => this.#viaTextarea(text) ? done() : this.#selectBody();
612
+ try {
613
+ const clip = typeof navigator !== "undefined" ? navigator.clipboard : void 0;
614
+ if (clip && typeof clip.writeText === "function") clip.writeText(text).then(done, fallback);
615
+ else fallback();
616
+ } catch {
617
+ fallback();
618
+ }
619
+ }
620
+ /** Without the Clipboard API (plain http): copy the raw text from a hidden textarea. */
621
+ #viaTextarea(text) {
622
+ if (typeof document.execCommand !== "function") return false;
623
+ try {
624
+ const ta = document.createElement("textarea");
625
+ ta.value = text;
626
+ ta.setAttribute("readonly", "");
627
+ ta.setAttribute("aria-hidden", "true");
628
+ ta.style.cssText = "position:fixed;top:0;left:0;opacity:0;pointer-events:none";
629
+ this.root.append(ta);
630
+ ta.select();
631
+ const ok = document.execCommand("copy");
632
+ ta.remove();
633
+ this.$(".ans-copy")?.focus({ preventScroll: true });
634
+ return !!ok;
635
+ } catch {
636
+ return false;
637
+ }
638
+ }
639
+ /** Last resort: select the rendered answer so ⌘C / Ctrl+C copies it. */
640
+ #selectBody() {
641
+ const body = this.$(".ans-body");
642
+ try {
643
+ const sel = typeof getSelection === "function" ? getSelection() : null;
644
+ if (body && sel) {
645
+ const range = document.createRange();
646
+ range.selectNodeContents(body);
647
+ sel.removeAllRanges();
648
+ sel.addRange(range);
649
+ }
650
+ } catch {
651
+ }
652
+ this.#flash(false, strings(this.#lang).selected);
653
+ }
654
+ /** Copy feedback lives in place (no re-render) and clears after 1.5 s. */
655
+ #flash(copied, message) {
656
+ this.#copied = copied;
657
+ this.#status = message;
658
+ this.#paintCopy();
659
+ clearTimeout(this.#timer);
660
+ this.#timer = window.setTimeout(() => {
661
+ this.#copied = false;
662
+ this.#status = "";
663
+ this.#paintCopy();
664
+ }, COPIED_MS);
665
+ }
666
+ #paintCopy() {
667
+ const b = this.$(".ans-copy");
668
+ if (b) {
669
+ b.innerHTML = this.#copyLabel(strings(this.#lang));
670
+ b.toggleAttribute("data-copied", this.#copied);
671
+ }
672
+ const s = this.$(".ans-status");
673
+ if (s) s.textContent = this.#status;
674
+ }
675
+ }
676
+ define("ui-ai-answer", UiAiAnswer);
677
+ export {
678
+ UiAiAnswer,
679
+ answerCopyText,
680
+ answerLinkInfo,
681
+ answerLinkKey,
682
+ answerLinkify,
683
+ answerMarkdown,
684
+ answerTookLabel,
685
+ renderAnswer
686
+ };
687
+ //# sourceMappingURL=ai-answer.js.map