prompt-area 0.1.0 → 0.3.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 (39) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +11 -3
  3. package/dist/action-bar/index.js +1 -4
  4. package/dist/chat-prompt-layout/index.js +1 -4
  5. package/dist/chunk-23Y7B365.js +2 -0
  6. package/dist/chunk-2DBBB352.js +3 -0
  7. package/dist/chunk-6VISE4VA.js +2 -0
  8. package/dist/chunk-AUCRB7HL.js +2 -0
  9. package/dist/chunk-CRC4ST6U.js +3 -0
  10. package/dist/chunk-VULUMPYE.js +2 -0
  11. package/dist/chunk-WBAKQRYT.js +20 -0
  12. package/dist/compact-prompt-area/index.js +1 -5
  13. package/dist/helpers/index.js +7 -291
  14. package/dist/index.js +1 -9
  15. package/dist/prompt-area/index.js +1 -5
  16. package/dist/status-bar/index.js +1 -4
  17. package/package.json +24 -12
  18. package/LICENSE +0 -21
  19. package/dist/action-bar/index.js.map +0 -1
  20. package/dist/chat-prompt-layout/index.js.map +0 -1
  21. package/dist/chunk-ANZZEZP2.js +0 -38
  22. package/dist/chunk-ANZZEZP2.js.map +0 -1
  23. package/dist/chunk-BPJO4DGM.js +0 -198
  24. package/dist/chunk-BPJO4DGM.js.map +0 -1
  25. package/dist/chunk-BWVBDP7C.js +0 -38
  26. package/dist/chunk-BWVBDP7C.js.map +0 -1
  27. package/dist/chunk-E7HUXORB.js +0 -2692
  28. package/dist/chunk-E7HUXORB.js.map +0 -1
  29. package/dist/chunk-NF2LHZIE.js +0 -12
  30. package/dist/chunk-NF2LHZIE.js.map +0 -1
  31. package/dist/chunk-UBBCAMJA.js +0 -116
  32. package/dist/chunk-UBBCAMJA.js.map +0 -1
  33. package/dist/chunk-XDKRP7UE.js +0 -125
  34. package/dist/chunk-XDKRP7UE.js.map +0 -1
  35. package/dist/compact-prompt-area/index.js.map +0 -1
  36. package/dist/helpers/index.js.map +0 -1
  37. package/dist/index.js.map +0 -1
  38. package/dist/prompt-area/index.js.map +0 -1
  39. package/dist/status-bar/index.js.map +0 -1
@@ -1,2692 +0,0 @@
1
- 'use client';
2
- import { cn } from './chunk-NF2LHZIE.js';
3
- import { useRef, useState, useCallback, useEffect, useMemo, useImperativeHandle } from 'react';
4
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
-
6
- // src/prompt-area/prompt-area-engine.ts
7
- function segmentsToPlainText(segments) {
8
- return segments.map((seg) => {
9
- if (seg.type === "text") return seg.text;
10
- return `${seg.trigger}${seg.displayText}`;
11
- }).join("");
12
- }
13
- function plainTextToSegments(text) {
14
- if (!text) return [];
15
- return [{ type: "text", text }];
16
- }
17
- function isValidTriggerPosition(text, charIndex, position) {
18
- if (charIndex === 0) return true;
19
- const prevChar = text[charIndex - 1];
20
- if (position === "start") {
21
- return prevChar === "\n";
22
- }
23
- return prevChar === " " || prevChar === "\n" || prevChar === " ";
24
- }
25
- function detectActiveTrigger(text, cursorPos, triggers) {
26
- if (!text || cursorPos === 0 || triggers.length === 0) return null;
27
- for (let i = cursorPos - 1; i >= 0; i--) {
28
- const char = text[i];
29
- if (char === " " || char === "\n" || char === " ") {
30
- if (i + 1 < cursorPos) {
31
- const nextChar = text[i + 1];
32
- const matchingTrigger2 = triggers.find((t) => t.char === nextChar);
33
- if (matchingTrigger2 && isValidTriggerPosition(text, i + 1, matchingTrigger2.position)) {
34
- return {
35
- config: matchingTrigger2,
36
- startOffset: i + 1,
37
- query: text.slice(i + 2, cursorPos)
38
- };
39
- }
40
- }
41
- return null;
42
- }
43
- const matchingTrigger = triggers.find((t) => t.char === char);
44
- if (matchingTrigger && isValidTriggerPosition(text, i, matchingTrigger.position)) {
45
- return {
46
- config: matchingTrigger,
47
- startOffset: i,
48
- query: text.slice(i + 1, cursorPos)
49
- };
50
- }
51
- }
52
- return null;
53
- }
54
- function resolveChip(segments, activeTrigger, chip) {
55
- const triggerStart = activeTrigger.startOffset;
56
- const triggerEnd = triggerStart + 1 + activeTrigger.query.length;
57
- const newSegments = [];
58
- let offset = 0;
59
- for (const seg of segments) {
60
- if (seg.type === "chip") {
61
- const chipText = `${seg.trigger}${seg.displayText}`;
62
- const chipStart = offset;
63
- const chipEnd = offset + chipText.length;
64
- if (chipEnd <= triggerStart || chipStart >= triggerEnd) {
65
- newSegments.push(seg);
66
- }
67
- offset = chipEnd;
68
- } else {
69
- const textStart = offset;
70
- const textEnd = offset + seg.text.length;
71
- if (textEnd <= triggerStart) {
72
- newSegments.push(seg);
73
- } else if (textStart >= triggerEnd) {
74
- newSegments.push(seg);
75
- } else {
76
- const beforeText = seg.text.slice(0, Math.max(0, triggerStart - textStart));
77
- const afterText = seg.text.slice(Math.min(seg.text.length, triggerEnd - textStart));
78
- if (beforeText) {
79
- newSegments.push({ type: "text", text: beforeText });
80
- }
81
- const newChip = {
82
- type: "chip",
83
- trigger: activeTrigger.config.char,
84
- value: chip.value,
85
- displayText: chip.displayText,
86
- ...chip.data !== void 0 ? { data: chip.data } : {},
87
- ...chip.autoResolved ? { autoResolved: true } : {}
88
- };
89
- newSegments.push(newChip);
90
- if (afterText) {
91
- newSegments.push({ type: "text", text: " " + afterText.replace(/^\s/, "") });
92
- } else {
93
- newSegments.push({ type: "text", text: " " });
94
- }
95
- }
96
- offset = textEnd;
97
- }
98
- }
99
- const merged = mergeAdjacentTextSegments(newSegments);
100
- let lastChipEndOffset = -1;
101
- let runningOffset = 0;
102
- for (const seg of merged) {
103
- if (seg.type === "text") {
104
- runningOffset += seg.text.length;
105
- } else {
106
- runningOffset += seg.trigger.length + seg.displayText.length;
107
- if (seg.value === chip.value && seg.displayText === chip.displayText && seg.trigger === activeTrigger.config.char) {
108
- lastChipEndOffset = runningOffset;
109
- }
110
- }
111
- }
112
- const cursorOffset = lastChipEndOffset === -1 ? runningOffset : lastChipEndOffset + 1;
113
- return { segments: merged, cursorOffset };
114
- }
115
- function removeChipAtIndex(segments, index) {
116
- if (index < 0 || index >= segments.length) return segments;
117
- if (segments[index].type !== "chip") return segments;
118
- const result = [...segments.slice(0, index), ...segments.slice(index + 1)];
119
- return mergeAdjacentTextSegments(result);
120
- }
121
- function revertChipAtIndex(segments, index) {
122
- if (index < 0 || index >= segments.length) return null;
123
- const seg = segments[index];
124
- if (seg.type !== "chip" || !seg.autoResolved) return null;
125
- const revertedText = `${seg.trigger}${seg.displayText}`;
126
- const result = [
127
- ...segments.slice(0, index),
128
- { type: "text", text: revertedText },
129
- ...segments.slice(index + 1)
130
- ];
131
- return { segments: mergeAdjacentTextSegments(result), revertedText };
132
- }
133
- function resolveTriggersInSegments(segments, triggers) {
134
- const autoResolveTriggers = triggers.filter((t) => t.resolveOnSpace);
135
- if (autoResolveTriggers.length === 0) return segments;
136
- const triggerChars = new Set(autoResolveTriggers.map((t) => t.char));
137
- const result = [];
138
- for (const seg of segments) {
139
- if (seg.type === "chip") {
140
- result.push(seg);
141
- continue;
142
- }
143
- const parts = splitTextByTriggerPatterns(seg.text, autoResolveTriggers, triggerChars);
144
- result.push(...parts);
145
- }
146
- return mergeAdjacentTextSegments(result);
147
- }
148
- function splitTextByTriggerPatterns(text, triggers, triggerChars) {
149
- if (!text) return [];
150
- const segments = [];
151
- let i = 0;
152
- while (i < text.length) {
153
- const char = text[i];
154
- if (triggerChars.has(char)) {
155
- const isAtBoundary = i === 0 || text[i - 1] === " " || text[i - 1] === "\n" || text[i - 1] === " ";
156
- if (isAtBoundary) {
157
- const trigger = triggers.find((t) => t.char === char);
158
- if (trigger && isValidTriggerPosition(text, i, trigger.position)) {
159
- let end = i + 1;
160
- while (end < text.length && text[end] !== " " && text[end] !== "\n" && text[end] !== " ") {
161
- end++;
162
- }
163
- const query = text.slice(i + 1, end);
164
- if (query.length > 0) {
165
- const displayText = trigger.onSelect?.({ value: query, label: query }) || query;
166
- segments.push({
167
- type: "chip",
168
- trigger: char,
169
- value: query,
170
- displayText,
171
- autoResolved: true
172
- });
173
- i = end;
174
- continue;
175
- }
176
- }
177
- }
178
- }
179
- const start = i;
180
- i++;
181
- while (i < text.length && !(triggerChars.has(text[i]) && (text[i - 1] === " " || text[i - 1] === "\n" || text[i - 1] === " "))) {
182
- i++;
183
- }
184
- segments.push({ type: "text", text: text.slice(start, i) });
185
- }
186
- return segments;
187
- }
188
- function replaceTextRange(segments, start, end, replacement) {
189
- const newSegments = [];
190
- let offset = 0;
191
- let inserted = false;
192
- for (const seg of segments) {
193
- if (seg.type === "chip") {
194
- const chipText = `${seg.trigger}${seg.displayText}`;
195
- const chipStart = offset;
196
- const chipEnd = offset + chipText.length;
197
- if (!inserted && start === end && chipStart === start) {
198
- newSegments.push({ type: "text", text: replacement });
199
- inserted = true;
200
- }
201
- if (chipEnd <= start || chipStart >= end) {
202
- newSegments.push(seg);
203
- }
204
- offset = chipEnd;
205
- } else {
206
- const textStart = offset;
207
- const textEnd = offset + seg.text.length;
208
- const isBefore = start === end ? textEnd < start : textEnd <= start;
209
- const isAfter = start === end ? textStart > end : textStart >= end;
210
- if (isBefore) {
211
- newSegments.push(seg);
212
- } else if (isAfter) {
213
- newSegments.push(seg);
214
- } else {
215
- const beforeText = seg.text.slice(0, Math.max(0, start - textStart));
216
- const afterText = seg.text.slice(Math.min(seg.text.length, end - textStart));
217
- if (beforeText) {
218
- newSegments.push({ type: "text", text: beforeText });
219
- }
220
- if (!inserted && textStart <= start) {
221
- newSegments.push({ type: "text", text: replacement });
222
- inserted = true;
223
- }
224
- if (afterText) {
225
- newSegments.push({ type: "text", text: afterText });
226
- }
227
- }
228
- offset = textEnd;
229
- }
230
- }
231
- if (!inserted && replacement) {
232
- newSegments.push({ type: "text", text: replacement });
233
- }
234
- return mergeAdjacentTextSegments(newSegments);
235
- }
236
- function toggleMarkdownWrap(segments, selectionStart, selectionEnd, marker) {
237
- if (selectionStart === selectionEnd) return null;
238
- const plainText = segmentsToPlainText(segments);
239
- const markerLen = marker.length;
240
- const hasOpeningMarker = selectionStart >= markerLen && plainText.slice(selectionStart - markerLen, selectionStart) === marker;
241
- const hasClosingMarker = selectionEnd + markerLen <= plainText.length && plainText.slice(selectionEnd, selectionEnd + markerLen) === marker;
242
- let isWrapped = hasOpeningMarker && hasClosingMarker;
243
- if (isWrapped && markerLen === 1) {
244
- const charBeforeOpening = selectionStart > markerLen ? plainText[selectionStart - markerLen - 1] : "";
245
- const charAfterClosing = selectionEnd + markerLen < plainText.length ? plainText[selectionEnd + markerLen] : "";
246
- if (charBeforeOpening === marker || charAfterClosing === marker) {
247
- isWrapped = false;
248
- }
249
- }
250
- if (isWrapped) {
251
- const afterClosing2 = replaceTextRange(segments, selectionEnd, selectionEnd + markerLen, "");
252
- const afterOpening2 = replaceTextRange(
253
- afterClosing2,
254
- selectionStart - markerLen,
255
- selectionStart,
256
- ""
257
- );
258
- return {
259
- segments: afterOpening2,
260
- selectionStart: selectionStart - markerLen,
261
- selectionEnd: selectionEnd - markerLen
262
- };
263
- }
264
- const afterClosing = replaceTextRange(segments, selectionEnd, selectionEnd, marker);
265
- const afterOpening = replaceTextRange(afterClosing, selectionStart, selectionStart, marker);
266
- return {
267
- segments: afterOpening,
268
- selectionStart: selectionStart + markerLen,
269
- selectionEnd: selectionEnd + markerLen
270
- };
271
- }
272
- function parseInlineMarkdown(text) {
273
- if (!text) return [];
274
- const tokens = [];
275
- const pattern = /(\*{3}(.+?)\*{3})|(\*{2}(.+?)\*{2})|(\*(.+?)\*)|(https?:\/\/[^\s),]+)/g;
276
- let lastIndex = 0;
277
- let match;
278
- while ((match = pattern.exec(text)) !== null) {
279
- if (match.index > lastIndex) {
280
- tokens.push({ type: "plain", text: text.slice(lastIndex, match.index) });
281
- }
282
- if (match[1] && match[2]) {
283
- tokens.push({ type: "bold-italic", text: match[2] });
284
- } else if (match[3] && match[4]) {
285
- tokens.push({ type: "bold", text: match[4] });
286
- } else if (match[5] && match[6]) {
287
- tokens.push({ type: "italic", text: match[6] });
288
- } else if (match[7]) {
289
- tokens.push({ type: "url", text: match[7] });
290
- }
291
- lastIndex = match.index + match[0].length;
292
- }
293
- if (lastIndex < text.length) {
294
- tokens.push({ type: "plain", text: text.slice(lastIndex) });
295
- }
296
- return tokens;
297
- }
298
- function segmentsEqual(a, b) {
299
- if (a === b) return true;
300
- if (a.length !== b.length) return false;
301
- for (let i = 0; i < a.length; i++) {
302
- const sa = a[i];
303
- const sb = b[i];
304
- if (sa.type !== sb.type) return false;
305
- if (sa.type === "text") {
306
- if (sb.type !== "text" || sa.text !== sb.text) return false;
307
- } else {
308
- if (sb.type !== "chip" || sa.trigger !== sb.trigger || sa.value !== sb.value || sa.displayText !== sb.displayText || sa.autoResolved !== sb.autoResolved)
309
- return false;
310
- }
311
- }
312
- return true;
313
- }
314
- function mergeAdjacentTextSegments(segments) {
315
- const result = [];
316
- for (const seg of segments) {
317
- if (seg.type === "text" && seg.text === "") continue;
318
- const last = result[result.length - 1];
319
- if (seg.type === "text" && last?.type === "text") {
320
- result[result.length - 1] = { type: "text", text: last.text + seg.text };
321
- } else {
322
- result.push(seg);
323
- }
324
- }
325
- return result;
326
- }
327
-
328
- // src/prompt-area/prompt-area-list-ops.ts
329
- function getListContext(text, cursorPos) {
330
- const lineStart = text.lastIndexOf("\n", cursorPos - 1) + 1;
331
- const lineEnd = text.indexOf("\n", cursorPos);
332
- const line = text.slice(lineStart, lineEnd === -1 ? text.length : lineEnd);
333
- const bulletMatch = line.match(/^(\s*)([•\-*]) /);
334
- if (bulletMatch) {
335
- const indentStr = bulletMatch[1];
336
- return {
337
- lineStart,
338
- prefix: bulletMatch[0],
339
- indent: Math.floor(indentStr.length / 2),
340
- listType: "bullet",
341
- contentStart: lineStart + bulletMatch[0].length
342
- };
343
- }
344
- const numberMatch = line.match(/^(\s*)(\d+)\. /);
345
- if (numberMatch) {
346
- const indentStr = numberMatch[1];
347
- return {
348
- lineStart,
349
- prefix: numberMatch[0],
350
- indent: Math.floor(indentStr.length / 2),
351
- listType: "numbered",
352
- number: parseInt(numberMatch[2], 10),
353
- contentStart: lineStart + numberMatch[0].length
354
- };
355
- }
356
- return null;
357
- }
358
- function autoFormatListPrefix(segments, cursorPos) {
359
- const plainText = segmentsToPlainText(segments);
360
- const lineStart = plainText.lastIndexOf("\n", cursorPos - 1) + 1;
361
- const lineText = plainText.slice(lineStart, cursorPos);
362
- const match = lineText.match(/^(\s*)[-*] $/);
363
- if (!match) return null;
364
- const indent = match[1];
365
- const replacement = `${indent}\u2022 `;
366
- const rangeStart = lineStart;
367
- const rangeEnd = lineStart + lineText.length;
368
- const newSegments = replaceTextRange(segments, rangeStart, rangeEnd, replacement);
369
- return {
370
- segments: newSegments,
371
- cursorOffset: lineStart + replacement.length
372
- };
373
- }
374
- function insertListContinuation(segments, cursorPos) {
375
- const plainText = segmentsToPlainText(segments);
376
- const ctx = getListContext(plainText, cursorPos);
377
- if (!ctx) return null;
378
- const lineEnd = plainText.indexOf("\n", cursorPos);
379
- const lineContent = plainText.slice(ctx.contentStart, lineEnd === -1 ? plainText.length : lineEnd);
380
- if (lineContent.trim() === "") {
381
- const newSegments2 = replaceTextRange(
382
- segments,
383
- ctx.lineStart,
384
- ctx.lineStart + ctx.prefix.length,
385
- ""
386
- );
387
- return {
388
- segments: newSegments2,
389
- cursorOffset: ctx.lineStart
390
- };
391
- }
392
- const indent = " ".repeat(ctx.indent);
393
- let nextPrefix;
394
- if (ctx.listType === "bullet") {
395
- nextPrefix = `${indent}\u2022 `;
396
- } else {
397
- const nextNum = (ctx.number ?? 1) + 1;
398
- nextPrefix = `${indent}${nextNum}. `;
399
- }
400
- const insertion = `
401
- ${nextPrefix}`;
402
- const newSegments = replaceTextRange(segments, cursorPos, cursorPos, insertion);
403
- return {
404
- segments: newSegments,
405
- cursorOffset: cursorPos + insertion.length
406
- };
407
- }
408
- function indentListItem(segments, cursorPos) {
409
- const plainText = segmentsToPlainText(segments);
410
- const ctx = getListContext(plainText, cursorPos);
411
- if (!ctx) return null;
412
- const newSegments = replaceTextRange(segments, ctx.lineStart, ctx.lineStart, " ");
413
- return {
414
- segments: newSegments,
415
- cursorOffset: cursorPos + 2
416
- };
417
- }
418
- function outdentListItem(segments, cursorPos) {
419
- const plainText = segmentsToPlainText(segments);
420
- const ctx = getListContext(plainText, cursorPos);
421
- if (!ctx || ctx.indent === 0) return null;
422
- const newSegments = replaceTextRange(segments, ctx.lineStart, ctx.lineStart + 2, "");
423
- return {
424
- segments: newSegments,
425
- cursorOffset: Math.max(ctx.lineStart, cursorPos - 2)
426
- };
427
- }
428
- function removeListPrefix(segments, cursorPos) {
429
- const plainText = segmentsToPlainText(segments);
430
- const ctx = getListContext(plainText, cursorPos);
431
- if (!ctx) return null;
432
- if (cursorPos > ctx.contentStart) return null;
433
- const newSegments = replaceTextRange(
434
- segments,
435
- ctx.lineStart,
436
- ctx.contentStart,
437
- " ".repeat(ctx.indent)
438
- );
439
- return {
440
- segments: newSegments,
441
- cursorOffset: ctx.lineStart + ctx.indent * 2
442
- };
443
- }
444
- function normalizeListPrefixes(segments, markdownEnabled) {
445
- let changed = false;
446
- const result = segments.map((seg) => {
447
- if (seg.type !== "text") return seg;
448
- const newText = markdownEnabled ? seg.text.replace(/(^|\n)(\s*)- /g, "$1$2\u2022 ") : seg.text.replace(/(^|\n)(\s*)• /g, "$1$2- ");
449
- if (newText === seg.text) return seg;
450
- changed = true;
451
- return { ...seg, text: newText };
452
- });
453
- return changed ? result : segments;
454
- }
455
-
456
- // src/prompt-area/dom-helpers.ts
457
- function isHTMLElement(node) {
458
- return node instanceof HTMLElement;
459
- }
460
- function isChipElement(node) {
461
- return node instanceof HTMLElement && node.dataset.chipTrigger !== void 0;
462
- }
463
- function isBRElement(node) {
464
- return node instanceof HTMLBRElement;
465
- }
466
- function isTextNode(node) {
467
- return node instanceof Text;
468
- }
469
- function getChipAutoResolved(node) {
470
- return isChipElement(node) && node.dataset.chipAutoResolved === "true";
471
- }
472
- function isLinkElement(node) {
473
- return node instanceof HTMLAnchorElement && node.dataset.url === "true";
474
- }
475
- function safeJsonParse(json) {
476
- try {
477
- const parsed = JSON.parse(json);
478
- return parsed;
479
- } catch {
480
- return void 0;
481
- }
482
- }
483
- function safeJsonStringify(value) {
484
- try {
485
- return JSON.stringify(value);
486
- } catch {
487
- return void 0;
488
- }
489
- }
490
- function getChipTrigger(node) {
491
- if (!isChipElement(node)) return void 0;
492
- return node.dataset.chipTrigger;
493
- }
494
- function getChipValue(node) {
495
- if (!isChipElement(node)) return void 0;
496
- return node.dataset.chipValue;
497
- }
498
- function getChipDisplay(node) {
499
- if (!isChipElement(node)) return void 0;
500
- return node.dataset.chipDisplay ?? node.textContent ?? void 0;
501
- }
502
- function getChipData(node) {
503
- if (!isChipElement(node)) return void 0;
504
- const raw = node.dataset.chipData;
505
- if (!raw) return void 0;
506
- return safeJsonParse(raw);
507
- }
508
- function indexOfChildNode(parent, child) {
509
- const children = parent.childNodes;
510
- for (let i = 0; i < children.length; i++) {
511
- if (children[i] === child) return i;
512
- }
513
- return -1;
514
- }
515
- function getDirectChildContaining(ancestor, descendant) {
516
- let node = descendant;
517
- while (node !== null) {
518
- if (node.parentNode === ancestor) return node;
519
- node = node.parentNode;
520
- }
521
- return null;
522
- }
523
- function unwrapBlockElement(parent, block) {
524
- const fragment = document.createDocumentFragment();
525
- while (block.firstChild) {
526
- fragment.appendChild(block.firstChild);
527
- }
528
- fragment.appendChild(document.createElement("br"));
529
- parent.replaceChild(fragment, block);
530
- }
531
- function normalizeEditorDOM(editor) {
532
- let changed = false;
533
- const blockTags = /* @__PURE__ */ new Set(["DIV", "P", "SECTION", "ARTICLE", "BLOCKQUOTE"]);
534
- for (let i = editor.childNodes.length - 1; i >= 0; i--) {
535
- const child = editor.childNodes[i];
536
- if (!(child instanceof HTMLElement)) continue;
537
- if (child.dataset.chipTrigger !== void 0) continue;
538
- if (child instanceof HTMLBRElement) continue;
539
- const tag = child.tagName;
540
- if (blockTags.has(tag)) {
541
- unwrapBlockElement(editor, child);
542
- changed = true;
543
- } else if (tag === "FONT" || tag === "B" || tag === "I" || tag === "U" || tag === "STRONG" || tag === "EM" || tag === "A" || tag === "SPAN") {
544
- const text = child.textContent ?? "";
545
- if (text) {
546
- editor.replaceChild(document.createTextNode(text), child);
547
- } else {
548
- editor.removeChild(child);
549
- }
550
- changed = true;
551
- }
552
- }
553
- editor.normalize();
554
- return changed;
555
- }
556
- var URL_PATTERN = /https?:\/\/[^\s),]+/g;
557
- function decorateURLsInEditor(editor) {
558
- let decorated = false;
559
- const textNodes = [];
560
- for (let i = 0; i < editor.childNodes.length; i++) {
561
- const node = editor.childNodes[i];
562
- if (isTextNode(node) && node.textContent) {
563
- textNodes.push(node);
564
- }
565
- }
566
- for (const textNode of textNodes) {
567
- const text = textNode.textContent ?? "";
568
- URL_PATTERN.lastIndex = 0;
569
- const matches = [];
570
- let match;
571
- while ((match = URL_PATTERN.exec(text)) !== null) {
572
- let url = match[0];
573
- while (url.length > 0 && /[.;:!?]$/.test(url)) {
574
- url = url.slice(0, -1);
575
- }
576
- if (url.length > 0) {
577
- matches.push({ url, index: match.index });
578
- }
579
- }
580
- if (matches.length === 0) continue;
581
- const parent = textNode.parentNode;
582
- if (!parent) continue;
583
- const safeMatches = [];
584
- for (const { url, index } of matches) {
585
- try {
586
- const parsed = new URL(url);
587
- if (parsed.protocol === "http:" || parsed.protocol === "https:") {
588
- safeMatches.push({ url, href: parsed.href, index });
589
- }
590
- } catch {
591
- }
592
- }
593
- if (safeMatches.length === 0) continue;
594
- decorated = true;
595
- const fragment = document.createDocumentFragment();
596
- let lastIndex = 0;
597
- for (const { url, href, index } of safeMatches) {
598
- if (index > lastIndex) {
599
- fragment.appendChild(document.createTextNode(text.slice(lastIndex, index)));
600
- }
601
- const anchor = document.createElement("a");
602
- anchor.href = href;
603
- anchor.target = "_blank";
604
- anchor.rel = "noopener noreferrer";
605
- anchor.dataset.url = "true";
606
- anchor.className = "text-primary hover:text-primary/80 underline cursor-pointer";
607
- anchor.textContent = url;
608
- fragment.appendChild(anchor);
609
- lastIndex = index + url.length;
610
- }
611
- if (lastIndex < text.length) {
612
- fragment.appendChild(document.createTextNode(text.slice(lastIndex)));
613
- }
614
- parent.replaceChild(fragment, textNode);
615
- }
616
- return decorated;
617
- }
618
- var MARKDOWN_INLINE_PATTERN = /(\*{3})(.+?)\*{3}|(\*{2})(.+?)\*{2}|(\*)(.+?)\*/g;
619
- function decorateMarkdownInEditor(editor) {
620
- let decorated = false;
621
- const textNodes = [];
622
- for (let i = 0; i < editor.childNodes.length; i++) {
623
- const node = editor.childNodes[i];
624
- if (isTextNode(node) && node.textContent) {
625
- textNodes.push(node);
626
- }
627
- }
628
- for (const textNode of textNodes) {
629
- const text = textNode.textContent ?? "";
630
- MARKDOWN_INLINE_PATTERN.lastIndex = 0;
631
- const matches = [];
632
- let match;
633
- while ((match = MARKDOWN_INLINE_PATTERN.exec(text)) !== null) {
634
- if (match[1] && match[2]) {
635
- matches.push({
636
- fullMatch: match[0],
637
- marker: match[1],
638
- content: match[2],
639
- index: match.index,
640
- className: "font-bold italic"
641
- });
642
- } else if (match[3] && match[4]) {
643
- matches.push({
644
- fullMatch: match[0],
645
- marker: match[3],
646
- content: match[4],
647
- index: match.index,
648
- className: "font-bold"
649
- });
650
- } else if (match[5] && match[6]) {
651
- matches.push({
652
- fullMatch: match[0],
653
- marker: match[5],
654
- content: match[6],
655
- index: match.index,
656
- className: "italic"
657
- });
658
- }
659
- }
660
- if (matches.length === 0) continue;
661
- decorated = true;
662
- const parent = textNode.parentNode;
663
- if (!parent) continue;
664
- const fragment = document.createDocumentFragment();
665
- let lastIndex = 0;
666
- for (const { fullMatch, marker, content, index, className } of matches) {
667
- if (index > lastIndex) {
668
- fragment.appendChild(document.createTextNode(text.slice(lastIndex, index)));
669
- }
670
- const span = document.createElement("span");
671
- span.dataset.md = "true";
672
- const openMarker = document.createElement("span");
673
- openMarker.className = "prompt-area-md-marker";
674
- openMarker.textContent = marker;
675
- const styledContent = document.createElement("span");
676
- styledContent.className = className;
677
- styledContent.textContent = content;
678
- const closeMarker = document.createElement("span");
679
- closeMarker.className = "prompt-area-md-marker";
680
- closeMarker.textContent = marker;
681
- span.appendChild(openMarker);
682
- span.appendChild(styledContent);
683
- span.appendChild(closeMarker);
684
- fragment.appendChild(span);
685
- lastIndex = index + fullMatch.length;
686
- }
687
- if (lastIndex < text.length) {
688
- fragment.appendChild(document.createTextNode(text.slice(lastIndex)));
689
- }
690
- parent.replaceChild(fragment, textNode);
691
- }
692
- return decorated;
693
- }
694
- function getSelectionRange() {
695
- const sel = window.getSelection();
696
- if (!sel || sel.rangeCount === 0) return null;
697
- return sel.getRangeAt(0);
698
- }
699
-
700
- // src/prompt-area/cursor-helpers.ts
701
- function saveCursorPosition(editor) {
702
- const range = getSelectionRange();
703
- if (!range) return null;
704
- if (!editor.contains(range.startContainer)) return null;
705
- const node = range.startContainer;
706
- if (node === editor) {
707
- return { nodeIndex: range.startOffset, offset: 0 };
708
- }
709
- const directChild = getDirectChildContaining(editor, node);
710
- if (!directChild) return null;
711
- const nodeIndex = indexOfChildNode(editor, directChild);
712
- return { nodeIndex, offset: range.startOffset };
713
- }
714
- function restoreCursorPosition(editor, saved) {
715
- const sel = window.getSelection();
716
- if (!sel) return;
717
- const childNodes = editor.childNodes;
718
- if (childNodes.length === 0) return;
719
- const range = document.createRange();
720
- if (saved.nodeIndex >= childNodes.length) {
721
- const lastChild = childNodes[childNodes.length - 1];
722
- if (lastChild.nodeType === Node.TEXT_NODE) {
723
- range.setStart(lastChild, (lastChild.textContent ?? "").length);
724
- } else {
725
- range.setStartAfter(lastChild);
726
- }
727
- } else {
728
- const targetNode = childNodes[saved.nodeIndex];
729
- if (targetNode.nodeType === Node.TEXT_NODE) {
730
- const maxOffset = (targetNode.textContent ?? "").length;
731
- range.setStart(targetNode, Math.min(saved.offset, maxOffset));
732
- } else {
733
- range.setStartAfter(targetNode);
734
- }
735
- }
736
- range.collapse(true);
737
- sel.removeAllRanges();
738
- sel.addRange(range);
739
- }
740
- function getCursorOffset(editor) {
741
- const range = getSelectionRange();
742
- if (!range) return null;
743
- if (!editor.contains(range.startContainer)) return null;
744
- const preRange = document.createRange();
745
- preRange.selectNodeContents(editor);
746
- preRange.setEnd(range.startContainer, range.startOffset);
747
- return getTextLengthInRange(preRange);
748
- }
749
- function createRangeAtOffset(editor, targetOffset) {
750
- const pos = findDOMPosition(editor, targetOffset);
751
- if (!pos) return null;
752
- const range = document.createRange();
753
- range.setStart(pos.node, pos.offset);
754
- range.collapse(true);
755
- return range;
756
- }
757
- function setCursorAtOffset(editor, targetOffset) {
758
- const sel = window.getSelection();
759
- if (!sel) return;
760
- const pos = findDOMPosition(editor, targetOffset);
761
- if (pos) {
762
- const range2 = document.createRange();
763
- range2.setStart(pos.node, pos.offset);
764
- range2.collapse(true);
765
- sel.removeAllRanges();
766
- sel.addRange(range2);
767
- return;
768
- }
769
- const range = document.createRange();
770
- range.selectNodeContents(editor);
771
- range.collapse(false);
772
- sel.removeAllRanges();
773
- sel.addRange(range);
774
- }
775
- function getTextLengthInRange(range) {
776
- const fragment = range.cloneContents();
777
- let length = 0;
778
- const walk = (node) => {
779
- if (node.nodeType === Node.TEXT_NODE) {
780
- length += (node.textContent ?? "").length;
781
- } else if (isChipElement(node)) {
782
- const trigger = node.dataset.chipTrigger ?? "";
783
- const display = node.dataset.chipDisplay ?? node.textContent ?? "";
784
- length += trigger.length + display.length;
785
- } else if (isHTMLElement(node) && node.tagName === "BR") {
786
- if (node.dataset.sentinel) return;
787
- length += 1;
788
- } else if (isHTMLElement(node)) {
789
- node.childNodes.forEach(walk);
790
- }
791
- };
792
- fragment.childNodes.forEach(walk);
793
- return length;
794
- }
795
- function getSelectionOffsets(editor) {
796
- const range = getSelectionRange();
797
- if (!range) return null;
798
- if (!editor.contains(range.startContainer)) return null;
799
- const startRange = document.createRange();
800
- startRange.selectNodeContents(editor);
801
- startRange.setEnd(range.startContainer, range.startOffset);
802
- const start = getTextLengthInRange(startRange);
803
- if (range.collapsed) return { start, end: start };
804
- const endRange = document.createRange();
805
- endRange.selectNodeContents(editor);
806
- endRange.setEnd(range.endContainer, range.endOffset);
807
- const end = getTextLengthInRange(endRange);
808
- return { start, end };
809
- }
810
- function setSelectionAtOffsets(editor, startOffset, endOffset) {
811
- const sel = window.getSelection();
812
- if (!sel) return;
813
- if (startOffset === endOffset) {
814
- setCursorAtOffset(editor, startOffset);
815
- return;
816
- }
817
- const startPos = findDOMPosition(editor, startOffset);
818
- const endPos = findDOMPosition(editor, endOffset);
819
- if (!startPos || !endPos) return;
820
- const range = document.createRange();
821
- range.setStart(startPos.node, startPos.offset);
822
- range.setEnd(endPos.node, endPos.offset);
823
- sel.removeAllRanges();
824
- sel.addRange(range);
825
- }
826
- function findDOMPosition(container, targetOffset) {
827
- let remaining = targetOffset;
828
- for (let i = 0; i < container.childNodes.length; i++) {
829
- const child = container.childNodes[i];
830
- if (child.nodeType === Node.TEXT_NODE) {
831
- const len = (child.textContent ?? "").length;
832
- if (remaining <= len) {
833
- return { node: child, offset: remaining };
834
- }
835
- remaining -= len;
836
- } else if (isChipElement(child)) {
837
- const trigger = child.dataset.chipTrigger ?? "";
838
- const display = child.dataset.chipDisplay ?? child.textContent ?? "";
839
- const chipLen = trigger.length + display.length;
840
- if (remaining <= chipLen) {
841
- return { node: container, offset: i + 1 };
842
- }
843
- remaining -= chipLen;
844
- } else if (isBRElement(child)) {
845
- if (child.dataset.sentinel) continue;
846
- if (remaining <= 1) {
847
- return { node: container, offset: i + 1 };
848
- }
849
- remaining -= 1;
850
- } else if (isHTMLElement(child)) {
851
- const textLen = (child.textContent ?? "").length;
852
- if (remaining <= textLen) {
853
- const result = findDOMPosition(child, remaining);
854
- if (result) return result;
855
- }
856
- remaining -= textLen;
857
- }
858
- }
859
- return { node: container, offset: container.childNodes.length };
860
- }
861
-
862
- // src/prompt-area/clipboard-helpers.ts
863
- function isRecord(value) {
864
- return typeof value === "object" && value !== null && !Array.isArray(value);
865
- }
866
- function serializeFragmentToPlainText(fragment) {
867
- let text = "";
868
- const walk = (node) => {
869
- if (node.nodeType === Node.TEXT_NODE) {
870
- text += node.textContent ?? "";
871
- } else if (isChipElement(node)) {
872
- const trigger = getChipTrigger(node) ?? "";
873
- const display = getChipDisplay(node) ?? "";
874
- text += trigger + display;
875
- } else if (isHTMLElement(node) && node.tagName === "BR") {
876
- text += "\n";
877
- } else {
878
- node.childNodes.forEach(walk);
879
- }
880
- };
881
- fragment.childNodes.forEach(walk);
882
- return text;
883
- }
884
- function serializeFragmentToSegments(fragment) {
885
- const segments = [];
886
- const walk = (node) => {
887
- if (node.nodeType === Node.TEXT_NODE) {
888
- const text = node.textContent ?? "";
889
- if (text) {
890
- segments.push({ type: "text", text });
891
- }
892
- } else if (isChipElement(node)) {
893
- const trigger = getChipTrigger(node);
894
- const chipValue = getChipValue(node);
895
- const display = getChipDisplay(node);
896
- const data = getChipData(node);
897
- const autoResolved = getChipAutoResolved(node);
898
- if (trigger && chipValue !== void 0 && display) {
899
- const chip = {
900
- type: "chip",
901
- trigger,
902
- value: chipValue,
903
- displayText: display,
904
- ...data !== void 0 ? { data } : {},
905
- ...autoResolved ? { autoResolved: true } : {}
906
- };
907
- segments.push(chip);
908
- }
909
- } else if (isHTMLElement(node) && node.tagName === "BR") {
910
- segments.push({ type: "text", text: "\n" });
911
- } else {
912
- node.childNodes.forEach(walk);
913
- }
914
- };
915
- fragment.childNodes.forEach(walk);
916
- return segments;
917
- }
918
- function parseSegmentsFromClipboard(json) {
919
- try {
920
- const parsed = JSON.parse(json);
921
- if (!Array.isArray(parsed)) return null;
922
- const segments = [];
923
- for (const item of parsed) {
924
- if (!isRecord(item)) return null;
925
- if (item.type === "text" && typeof item.text === "string") {
926
- segments.push({ type: "text", text: item.text });
927
- } else if (item.type === "chip" && typeof item.trigger === "string" && typeof item.value === "string" && typeof item.displayText === "string") {
928
- const chip = {
929
- type: "chip",
930
- trigger: item.trigger,
931
- value: item.value,
932
- displayText: item.displayText,
933
- ...item.data !== void 0 ? { data: item.data } : {},
934
- ...item.autoResolved ? { autoResolved: true } : {}
935
- };
936
- segments.push(chip);
937
- } else {
938
- return null;
939
- }
940
- }
941
- return segments;
942
- } catch {
943
- return null;
944
- }
945
- }
946
- function insertSegmentsAtCursor(currentSegments, pastedSegments, editor) {
947
- const range = getSelectionRange();
948
- if (!range) return [...currentSegments, ...pastedSegments];
949
- const preRange = document.createRange();
950
- preRange.selectNodeContents(editor);
951
- preRange.setEnd(range.startContainer, range.startOffset);
952
- const cursorOffset = getTextLengthInRange(preRange);
953
- const result = [];
954
- let offset = 0;
955
- let inserted = false;
956
- const insertOnce = () => {
957
- if (!inserted) {
958
- result.push(...pastedSegments);
959
- inserted = true;
960
- }
961
- };
962
- for (const seg of currentSegments) {
963
- if (seg.type === "chip") {
964
- const chipLen = seg.trigger.length + seg.displayText.length;
965
- if (offset >= cursorOffset) insertOnce();
966
- result.push(seg);
967
- offset += chipLen;
968
- continue;
969
- }
970
- const segEnd = offset + seg.text.length;
971
- if (segEnd <= cursorOffset) {
972
- result.push(seg);
973
- } else if (offset >= cursorOffset) {
974
- insertOnce();
975
- result.push(seg);
976
- } else {
977
- const splitAt = cursorOffset - offset;
978
- const before = seg.text.slice(0, splitAt);
979
- const after = seg.text.slice(splitAt);
980
- if (before) result.push({ type: "text", text: before });
981
- insertOnce();
982
- if (after) result.push({ type: "text", text: after });
983
- }
984
- offset = segEnd;
985
- }
986
- insertOnce();
987
- return mergeAdjacentTextSegments(result);
988
- }
989
-
990
- // src/prompt-area/use-prompt-area-events.ts
991
- var MAX_UNDO_HISTORY = 100;
992
- var BLUR_DELAY_MS = 150;
993
- function usePromptAreaEvents(deps) {
994
- const {
995
- editorRef,
996
- readSegmentsFromDOM,
997
- onChange,
998
- renderSegmentsToDOM,
999
- runTriggerDetection,
1000
- dismissTrigger,
1001
- triggers,
1002
- onPaste: onPasteCallback,
1003
- onUndo,
1004
- onRedo,
1005
- onChipAdd,
1006
- onImagePaste
1007
- } = deps;
1008
- const isComposing = useRef(false);
1009
- const undoState = useRef({ undoStack: [], redoStack: [] });
1010
- const pushUndo = useCallback((segments) => {
1011
- const state = undoState.current;
1012
- state.undoStack.push(segments);
1013
- if (state.undoStack.length > MAX_UNDO_HISTORY) {
1014
- state.undoStack.shift();
1015
- }
1016
- state.redoStack = [];
1017
- }, []);
1018
- const resetUndoHistory = useCallback(() => {
1019
- undoState.current = { undoStack: [], redoStack: [] };
1020
- }, []);
1021
- const handlePaste = useCallback(
1022
- (e) => {
1023
- e.preventDefault();
1024
- const editor = editorRef.current;
1025
- if (!editor) return;
1026
- const imageFile = Array.from(e.clipboardData.files).find((f) => f.type.startsWith("image/")) ?? (() => {
1027
- const item = Array.from(e.clipboardData.items).find((i) => i.type.startsWith("image/"));
1028
- return item?.getAsFile() ?? null;
1029
- })();
1030
- if (imageFile) {
1031
- onImagePaste?.(imageFile);
1032
- return;
1033
- }
1034
- const currentSegments = readSegmentsFromDOM();
1035
- pushUndo(currentSegments);
1036
- const segmentJson = e.clipboardData.getData("text/prompt-area-segments");
1037
- if (segmentJson) {
1038
- const parsed = parseSegmentsFromClipboard(segmentJson);
1039
- if (parsed && parsed.length > 0) {
1040
- const range2 = getSelectionRange();
1041
- if (!range2) return;
1042
- range2.deleteContents();
1043
- const beforePaste = readSegmentsFromDOM();
1044
- const merged = insertSegmentsAtCursor(beforePaste, parsed, editor);
1045
- onChange(merged);
1046
- renderSegmentsToDOM(merged);
1047
- onPasteCallback?.({ segments: merged, source: "internal" });
1048
- for (const seg of parsed) {
1049
- if (seg.type === "chip") {
1050
- onChipAdd?.(seg);
1051
- }
1052
- }
1053
- runTriggerDetection();
1054
- return;
1055
- }
1056
- }
1057
- const text = e.clipboardData.getData("text/plain");
1058
- if (!text) return;
1059
- const range = getSelectionRange();
1060
- if (!range) return;
1061
- range.deleteContents();
1062
- const lines = text.split("\n");
1063
- const fragment = document.createDocumentFragment();
1064
- for (let i = 0; i < lines.length; i++) {
1065
- if (lines[i]) {
1066
- fragment.appendChild(document.createTextNode(lines[i]));
1067
- }
1068
- if (i < lines.length - 1) {
1069
- fragment.appendChild(document.createElement("br"));
1070
- }
1071
- }
1072
- range.insertNode(fragment);
1073
- range.collapse(false);
1074
- const sel = window.getSelection();
1075
- sel?.removeAllRanges();
1076
- sel?.addRange(range);
1077
- normalizeEditorDOM(editor);
1078
- const newSegments = readSegmentsFromDOM();
1079
- const resolvedSegments = resolveTriggersInSegments(newSegments, triggers);
1080
- if (resolvedSegments !== newSegments) {
1081
- onChange(resolvedSegments);
1082
- renderSegmentsToDOM(resolvedSegments);
1083
- for (const seg of resolvedSegments) {
1084
- if (seg.type === "chip" && !newSegments.some(
1085
- (s) => s.type === "chip" && s.trigger === seg.trigger && s.value === seg.value && s.displayText === seg.displayText
1086
- )) {
1087
- onChipAdd?.(seg);
1088
- }
1089
- }
1090
- } else {
1091
- onChange(newSegments);
1092
- }
1093
- onPasteCallback?.({ segments: resolvedSegments, source: "external" });
1094
- runTriggerDetection();
1095
- },
1096
- [
1097
- editorRef,
1098
- readSegmentsFromDOM,
1099
- onChange,
1100
- pushUndo,
1101
- runTriggerDetection,
1102
- renderSegmentsToDOM,
1103
- triggers,
1104
- onPasteCallback,
1105
- onChipAdd,
1106
- onImagePaste
1107
- ]
1108
- );
1109
- const handleCopy = useCallback((e) => {
1110
- e.preventDefault();
1111
- const range = getSelectionRange();
1112
- if (!range) return;
1113
- const fragment = range.cloneContents();
1114
- const plainText = serializeFragmentToPlainText(fragment);
1115
- e.clipboardData.setData("text/plain", plainText);
1116
- const fragmentSegments = serializeFragmentToSegments(fragment);
1117
- const hasChips = fragmentSegments.some((s) => s.type === "chip");
1118
- if (hasChips) {
1119
- const json = safeJsonStringify(fragmentSegments);
1120
- if (json) {
1121
- e.clipboardData.setData("text/prompt-area-segments", json);
1122
- }
1123
- }
1124
- }, []);
1125
- const handleCut = useCallback(
1126
- (e) => {
1127
- handleCopy(e);
1128
- const range = getSelectionRange();
1129
- if (!range) return;
1130
- const currentSegments = readSegmentsFromDOM();
1131
- pushUndo(currentSegments);
1132
- range.deleteContents();
1133
- const editor = editorRef.current;
1134
- if (editor) {
1135
- normalizeEditorDOM(editor);
1136
- }
1137
- const newSegments = readSegmentsFromDOM();
1138
- onChange(newSegments);
1139
- runTriggerDetection();
1140
- },
1141
- [handleCopy, editorRef, readSegmentsFromDOM, onChange, pushUndo, runTriggerDetection]
1142
- );
1143
- const handleDrop = useCallback((e) => {
1144
- e.preventDefault();
1145
- }, []);
1146
- const handleDragOver = useCallback((e) => {
1147
- e.preventDefault();
1148
- }, []);
1149
- const handleCompositionStart = useCallback(() => {
1150
- isComposing.current = true;
1151
- }, []);
1152
- const handleCompositionEnd = useCallback(() => {
1153
- isComposing.current = false;
1154
- runTriggerDetection();
1155
- }, [runTriggerDetection]);
1156
- const handleBlur = useCallback(() => {
1157
- setTimeout(() => {
1158
- const editor = editorRef.current;
1159
- if (!editor) return;
1160
- const activeEl = document.activeElement;
1161
- if (activeEl && editor.parentElement?.contains(activeEl)) return;
1162
- dismissTrigger();
1163
- }, BLUR_DELAY_MS);
1164
- }, [editorRef, dismissTrigger]);
1165
- const handleKeyDownForUndoRedo = useCallback(
1166
- (e) => {
1167
- const isMeta = e.metaKey || e.ctrlKey;
1168
- if (!isMeta || e.key !== "z") return false;
1169
- e.preventDefault();
1170
- const state = undoState.current;
1171
- if (e.shiftKey) {
1172
- if (state.redoStack.length === 0) return true;
1173
- const segments = state.redoStack.pop();
1174
- if (!segments) return true;
1175
- const current = readSegmentsFromDOM();
1176
- state.undoStack.push(current);
1177
- onChange(segments);
1178
- renderSegmentsToDOM(segments);
1179
- onRedo?.(segments);
1180
- } else {
1181
- if (state.undoStack.length === 0) return true;
1182
- const segments = state.undoStack.pop();
1183
- if (!segments) return true;
1184
- const current = readSegmentsFromDOM();
1185
- state.redoStack.push(current);
1186
- onChange(segments);
1187
- renderSegmentsToDOM(segments);
1188
- onUndo?.(segments);
1189
- }
1190
- return true;
1191
- },
1192
- [readSegmentsFromDOM, onChange, renderSegmentsToDOM, onUndo, onRedo]
1193
- );
1194
- return {
1195
- handlePaste,
1196
- handleCopy,
1197
- handleCut,
1198
- handleDrop,
1199
- handleDragOver,
1200
- handleCompositionStart,
1201
- handleCompositionEnd,
1202
- handleBlur,
1203
- handleKeyDownForUndoRedo,
1204
- pushUndo,
1205
- resetUndoHistory,
1206
- isComposing
1207
- };
1208
- }
1209
- function useTriggerSearch() {
1210
- const [suggestions, setSuggestions] = useState([]);
1211
- const [suggestionsLoading, setSuggestionsLoading] = useState(false);
1212
- const [suggestionsError, setSuggestionsError] = useState(null);
1213
- const searchVersion = useRef(0);
1214
- const abortController = useRef(null);
1215
- const debounceTimer = useRef(null);
1216
- const reset = useCallback(() => {
1217
- abortController.current?.abort();
1218
- if (debounceTimer.current) clearTimeout(debounceTimer.current);
1219
- setSuggestions([]);
1220
- setSuggestionsLoading(false);
1221
- setSuggestionsError(null);
1222
- }, []);
1223
- const search = useCallback((query, config) => {
1224
- if (!config.onSearch) return;
1225
- abortController.current?.abort();
1226
- if (debounceTimer.current) clearTimeout(debounceTimer.current);
1227
- setSuggestionsLoading(true);
1228
- setSuggestionsError(null);
1229
- searchVersion.current++;
1230
- const version = searchVersion.current;
1231
- const controller = new AbortController();
1232
- abortController.current = controller;
1233
- const { onSearch, onSearchError, searchDebounceMs } = config;
1234
- const executeSearch = () => {
1235
- const result = onSearch(query, { signal: controller.signal });
1236
- if (result instanceof Promise) {
1237
- void result.then(
1238
- (items) => {
1239
- if (controller.signal.aborted || searchVersion.current !== version) return;
1240
- setSuggestions(items);
1241
- setSuggestionsLoading(false);
1242
- },
1243
- (error) => {
1244
- if (controller.signal.aborted || searchVersion.current !== version) return;
1245
- if (error instanceof DOMException && error.name === "AbortError") return;
1246
- setSuggestionsError(error instanceof Error ? error.message : "Search failed");
1247
- setSuggestionsLoading(false);
1248
- onSearchError?.(error);
1249
- }
1250
- );
1251
- } else {
1252
- setSuggestions(result);
1253
- setSuggestionsLoading(false);
1254
- }
1255
- };
1256
- if (searchDebounceMs && searchDebounceMs > 0 && query.length > 0) {
1257
- debounceTimer.current = setTimeout(executeSearch, searchDebounceMs);
1258
- } else {
1259
- executeSearch();
1260
- }
1261
- }, []);
1262
- useEffect(() => {
1263
- return () => {
1264
- abortController.current?.abort();
1265
- if (debounceTimer.current) clearTimeout(debounceTimer.current);
1266
- };
1267
- }, []);
1268
- return {
1269
- suggestions,
1270
- suggestionsLoading,
1271
- suggestionsError,
1272
- search,
1273
- reset
1274
- };
1275
- }
1276
-
1277
- // src/prompt-area/use-prompt-area.ts
1278
- var UNDO_DEBOUNCE_MS = 300;
1279
- function usePromptArea({
1280
- value,
1281
- onChange,
1282
- triggers = [],
1283
- onSubmit,
1284
- onEscape,
1285
- onChipClick,
1286
- onChipAdd,
1287
- onChipDelete,
1288
- onLinkClick,
1289
- onPaste,
1290
- onUndo,
1291
- onRedo,
1292
- onImagePaste,
1293
- markdown: markdownEnabled = true
1294
- }) {
1295
- const editorRef = useRef(null);
1296
- const [activeTrigger, setActiveTrigger] = useState(null);
1297
- const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(0);
1298
- const [triggerRect, setTriggerRect] = useState(null);
1299
- const {
1300
- suggestions,
1301
- suggestionsLoading,
1302
- suggestionsError,
1303
- search: runSearch,
1304
- reset: resetSearch
1305
- } = useTriggerSearch();
1306
- const isSyncing = useRef(false);
1307
- const lastRenderedValue = useRef([]);
1308
- const undoTimer = useRef(null);
1309
- const undoBaseState = useRef(null);
1310
- const readSegmentsFromDOM = useCallback(() => {
1311
- const editor = editorRef.current;
1312
- if (!editor) return [];
1313
- const segments = [];
1314
- for (let i = 0; i < editor.childNodes.length; i++) {
1315
- const node = editor.childNodes[i];
1316
- if (node.nodeType === Node.TEXT_NODE) {
1317
- const text = node.textContent ?? "";
1318
- if (text) {
1319
- segments.push({ type: "text", text });
1320
- }
1321
- } else if (isChipElement(node)) {
1322
- const trigger = getChipTrigger(node);
1323
- const chipValue = getChipValue(node);
1324
- const display = getChipDisplay(node);
1325
- const data = getChipData(node);
1326
- if (trigger && chipValue !== void 0 && display) {
1327
- const autoResolved = getChipAutoResolved(node);
1328
- segments.push({
1329
- type: "chip",
1330
- trigger,
1331
- value: chipValue,
1332
- displayText: display,
1333
- ...data !== void 0 ? { data } : {},
1334
- ...autoResolved ? { autoResolved: true } : {}
1335
- });
1336
- }
1337
- } else if (isBRElement(node)) {
1338
- if (node.dataset.sentinel) continue;
1339
- segments.push({ type: "text", text: "\n" });
1340
- } else if (isHTMLElement(node)) {
1341
- const text = node.textContent ?? "";
1342
- if (text) {
1343
- segments.push({ type: "text", text });
1344
- }
1345
- }
1346
- }
1347
- return segments;
1348
- }, []);
1349
- const renderSegmentsToDOM = useCallback(
1350
- (segments) => {
1351
- const editor = editorRef.current;
1352
- if (!editor) return;
1353
- isSyncing.current = true;
1354
- const savedCursor = saveCursorPosition(editor);
1355
- while (editor.firstChild) {
1356
- editor.removeChild(editor.firstChild);
1357
- }
1358
- for (const seg of segments) {
1359
- if (seg.type === "text") {
1360
- const lines = seg.text.split("\n");
1361
- for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
1362
- if (lines[lineIdx]) {
1363
- editor.appendChild(document.createTextNode(lines[lineIdx]));
1364
- }
1365
- if (lineIdx < lines.length - 1) {
1366
- editor.appendChild(document.createElement("br"));
1367
- }
1368
- }
1369
- } else {
1370
- const chip = document.createElement("span");
1371
- chip.contentEditable = "false";
1372
- chip.dataset.chipTrigger = seg.trigger;
1373
- chip.dataset.chipValue = seg.value;
1374
- chip.dataset.chipDisplay = seg.displayText;
1375
- if (seg.data !== void 0) {
1376
- const json = safeJsonStringify(seg.data);
1377
- if (json) {
1378
- chip.dataset.chipData = json;
1379
- }
1380
- }
1381
- if (seg.autoResolved) {
1382
- chip.dataset.chipAutoResolved = "true";
1383
- }
1384
- const triggerConfig = triggers.find((t) => t.char === seg.trigger);
1385
- const chipStyle = triggerConfig?.chipStyle ?? "pill";
1386
- chip.dataset.chipStyle = chipStyle;
1387
- chip.className = cn(
1388
- "prompt-area-chip",
1389
- chipStyle === "inline" && "prompt-area-chip--inline",
1390
- triggerConfig?.chipClassName
1391
- );
1392
- chip.textContent = `${seg.trigger}${seg.displayText}`;
1393
- chip.setAttribute("role", "button");
1394
- chip.setAttribute("tabindex", "-1");
1395
- editor.appendChild(chip);
1396
- }
1397
- }
1398
- if (editor.lastChild && isBRElement(editor.lastChild)) {
1399
- const sentinel = document.createElement("br");
1400
- sentinel.dataset.sentinel = "true";
1401
- editor.appendChild(sentinel);
1402
- }
1403
- decorateURLsInEditor(editor);
1404
- if (markdownEnabled) decorateMarkdownInEditor(editor);
1405
- if (savedCursor) {
1406
- restoreCursorPosition(editor, savedCursor);
1407
- }
1408
- lastRenderedValue.current = segments;
1409
- isSyncing.current = false;
1410
- },
1411
- [triggers, markdownEnabled]
1412
- );
1413
- const runTriggerDetection = useCallback(() => {
1414
- const editor = editorRef.current;
1415
- if (!editor) return;
1416
- const segments = readSegmentsFromDOM();
1417
- const plainText = segmentsToPlainText(segments);
1418
- const cursorPos = getCursorOffset(editor);
1419
- if (cursorPos === null) return;
1420
- const detected = detectActiveTrigger(plainText, cursorPos, triggers);
1421
- if (detected) {
1422
- setActiveTrigger(detected);
1423
- setSelectedSuggestionIndex(0);
1424
- const triggerRange = createRangeAtOffset(editor, detected.startOffset);
1425
- if (triggerRange) {
1426
- const rect = triggerRange.getBoundingClientRect();
1427
- if (rect.height > 0 || rect.left > 0 || rect.top > 0) {
1428
- setTriggerRect(rect);
1429
- }
1430
- }
1431
- if (detected.config.mode === "dropdown" && detected.config.onSearch) {
1432
- runSearch(detected.query, detected.config);
1433
- }
1434
- if (detected.config.mode === "callback" && detected.config.onActivate) {
1435
- detected.config.onActivate({
1436
- text: plainText,
1437
- cursorPosition: cursorPos,
1438
- insertChip: (chip) => {
1439
- const chipResult = resolveChip(segments, detected, {
1440
- value: chip.value,
1441
- displayText: chip.displayText,
1442
- data: chip.data
1443
- });
1444
- onChange(chipResult.segments);
1445
- renderSegmentsToDOM(chipResult.segments);
1446
- onChipAdd?.({
1447
- type: "chip",
1448
- trigger: detected.config.char,
1449
- value: chip.value,
1450
- displayText: chip.displayText,
1451
- ...chip.data !== void 0 ? { data: chip.data } : {}
1452
- });
1453
- const editor2 = editorRef.current;
1454
- if (editor2) {
1455
- setCursorAtOffset(editor2, chipResult.cursorOffset);
1456
- }
1457
- }
1458
- });
1459
- }
1460
- } else {
1461
- setActiveTrigger(null);
1462
- resetSearch();
1463
- }
1464
- }, [
1465
- triggers,
1466
- readSegmentsFromDOM,
1467
- onChange,
1468
- renderSegmentsToDOM,
1469
- onChipAdd,
1470
- resetSearch,
1471
- runSearch
1472
- ]);
1473
- const dismissTrigger = useCallback(() => {
1474
- setActiveTrigger(null);
1475
- setSelectedSuggestionIndex(0);
1476
- resetSearch();
1477
- }, [resetSearch]);
1478
- const events = usePromptAreaEvents({
1479
- editorRef,
1480
- readSegmentsFromDOM,
1481
- onChange,
1482
- renderSegmentsToDOM,
1483
- runTriggerDetection,
1484
- dismissTrigger,
1485
- triggers,
1486
- onPaste,
1487
- onUndo,
1488
- onRedo,
1489
- onChipAdd,
1490
- onImagePaste
1491
- });
1492
- useEffect(() => {
1493
- if (isSyncing.current) return;
1494
- if (segmentsEqual(value, lastRenderedValue.current)) return;
1495
- if (markdownEnabled) {
1496
- const normalized = normalizeListPrefixes(value, true);
1497
- if (normalized !== value) {
1498
- onChange(normalized);
1499
- return;
1500
- }
1501
- }
1502
- renderSegmentsToDOM(value);
1503
- }, [value, renderSegmentsToDOM, markdownEnabled, onChange]);
1504
- const prevMarkdown = useRef(markdownEnabled);
1505
- useEffect(() => {
1506
- if (prevMarkdown.current === markdownEnabled) return;
1507
- prevMarkdown.current = markdownEnabled;
1508
- const converted = normalizeListPrefixes(value, markdownEnabled);
1509
- if (converted !== value) {
1510
- onChange(converted);
1511
- } else {
1512
- renderSegmentsToDOM(value);
1513
- }
1514
- }, [markdownEnabled, renderSegmentsToDOM, value, onChange]);
1515
- useEffect(() => {
1516
- return () => {
1517
- if (undoTimer.current) clearTimeout(undoTimer.current);
1518
- };
1519
- }, []);
1520
- const handleInput = useCallback(() => {
1521
- if (isSyncing.current) return;
1522
- if (events.isComposing.current) {
1523
- const segments2 = readSegmentsFromDOM();
1524
- lastRenderedValue.current = segments2;
1525
- onChange(segments2);
1526
- return;
1527
- }
1528
- const editor = editorRef.current;
1529
- const savedCursorOffset = editor ? getCursorOffset(editor) : null;
1530
- if (editor) {
1531
- normalizeEditorDOM(editor);
1532
- }
1533
- const segments = readSegmentsFromDOM();
1534
- if (markdownEnabled && editor && savedCursorOffset !== null) {
1535
- const formatted = autoFormatListPrefix(segments, savedCursorOffset);
1536
- if (formatted) {
1537
- lastRenderedValue.current = formatted.segments;
1538
- onChange(formatted.segments);
1539
- renderSegmentsToDOM(formatted.segments);
1540
- setCursorAtOffset(editor, formatted.cursorOffset);
1541
- runTriggerDetection();
1542
- return;
1543
- }
1544
- }
1545
- if (!undoBaseState.current) {
1546
- undoBaseState.current = lastRenderedValue.current;
1547
- }
1548
- lastRenderedValue.current = segments;
1549
- onChange(segments);
1550
- if (undoTimer.current) clearTimeout(undoTimer.current);
1551
- undoTimer.current = setTimeout(() => {
1552
- if (undoBaseState.current) {
1553
- events.pushUndo(undoBaseState.current);
1554
- undoBaseState.current = null;
1555
- }
1556
- undoTimer.current = null;
1557
- }, UNDO_DEBOUNCE_MS);
1558
- if (editor) {
1559
- decorateURLsInEditor(editor);
1560
- if (markdownEnabled) decorateMarkdownInEditor(editor);
1561
- if (savedCursorOffset !== null) {
1562
- setCursorAtOffset(editor, savedCursorOffset);
1563
- }
1564
- }
1565
- runTriggerDetection();
1566
- }, [
1567
- onChange,
1568
- readSegmentsFromDOM,
1569
- runTriggerDetection,
1570
- renderSegmentsToDOM,
1571
- markdownEnabled,
1572
- events
1573
- ]);
1574
- const handleClick = useCallback(
1575
- (e) => {
1576
- const target = e.target;
1577
- if (!(target instanceof Node)) return;
1578
- const editor = editorRef.current;
1579
- if (!editor) return;
1580
- let node = target;
1581
- while (node && node !== editor) {
1582
- if (isLinkElement(node)) {
1583
- if (e.metaKey || e.ctrlKey) {
1584
- e.preventDefault();
1585
- onLinkClick?.(node.href);
1586
- window.open(node.href, "_blank", "noopener,noreferrer");
1587
- return;
1588
- }
1589
- break;
1590
- }
1591
- if (isChipElement(node)) {
1592
- const chipEl = node;
1593
- const rect = chipEl.getBoundingClientRect();
1594
- const ripple = document.createElement("span");
1595
- ripple.className = "prompt-area-chip-ripple";
1596
- const size = Math.max(rect.width, rect.height);
1597
- ripple.style.width = `${size}px`;
1598
- ripple.style.height = `${size}px`;
1599
- ripple.style.left = `${e.clientX - rect.left - size / 2}px`;
1600
- ripple.style.top = `${e.clientY - rect.top - size / 2}px`;
1601
- chipEl.appendChild(ripple);
1602
- ripple.addEventListener("animationend", () => ripple.remove());
1603
- if (!onChipClick) return;
1604
- const trigger = getChipTrigger(node);
1605
- const chipValue = getChipValue(node);
1606
- const display = getChipDisplay(node);
1607
- const data = getChipData(node);
1608
- if (trigger && chipValue !== void 0 && display) {
1609
- const autoResolved = getChipAutoResolved(node);
1610
- const chip = {
1611
- type: "chip",
1612
- trigger,
1613
- value: chipValue,
1614
- displayText: display,
1615
- ...data !== void 0 ? { data } : {},
1616
- ...autoResolved ? { autoResolved: true } : {}
1617
- };
1618
- onChipClick(chip);
1619
- }
1620
- return;
1621
- }
1622
- node = node.parentNode;
1623
- }
1624
- },
1625
- [onChipClick, onLinkClick]
1626
- );
1627
- const removeChipNodeFromDOM = useCallback(
1628
- (editor, chipNode) => {
1629
- const segments = readSegmentsFromDOM();
1630
- const chipIdx = indexOfChildNode(editor, chipNode);
1631
- if (chipIdx === -1) return false;
1632
- let segIdx = 0;
1633
- for (let i = 0; i < chipIdx; i++) {
1634
- const child = editor.childNodes[i];
1635
- if (child.nodeType === Node.TEXT_NODE && (child.textContent ?? "") !== "") {
1636
- segIdx++;
1637
- } else if (isChipElement(child)) {
1638
- segIdx++;
1639
- } else if (isBRElement(child)) {
1640
- segIdx++;
1641
- }
1642
- }
1643
- const deletedChip = segments[segIdx];
1644
- const newSegments = removeChipAtIndex(segments, segIdx);
1645
- onChange(newSegments);
1646
- renderSegmentsToDOM(newSegments);
1647
- if (deletedChip?.type === "chip") {
1648
- onChipDelete?.(deletedChip);
1649
- }
1650
- return true;
1651
- },
1652
- [readSegmentsFromDOM, onChange, renderSegmentsToDOM, onChipDelete]
1653
- );
1654
- const revertChipNodeToText = useCallback(
1655
- (editor, chipNode) => {
1656
- const segments = readSegmentsFromDOM();
1657
- const chipIdx = indexOfChildNode(editor, chipNode);
1658
- if (chipIdx === -1) return false;
1659
- let segIdx = 0;
1660
- for (let i = 0; i < chipIdx; i++) {
1661
- const child = editor.childNodes[i];
1662
- if (child.nodeType === Node.TEXT_NODE && (child.textContent ?? "") !== "") {
1663
- segIdx++;
1664
- } else if (isChipElement(child)) {
1665
- segIdx++;
1666
- } else if (isBRElement(child)) {
1667
- segIdx++;
1668
- }
1669
- }
1670
- const revertedChip = segments[segIdx];
1671
- const result = revertChipAtIndex(segments, segIdx);
1672
- if (!result) return false;
1673
- let targetOffset = 0;
1674
- for (let i = 0; i < segIdx; i++) {
1675
- const s = segments[i];
1676
- if (s.type === "text") {
1677
- targetOffset += s.text.length;
1678
- } else {
1679
- targetOffset += s.trigger.length + s.displayText.length;
1680
- }
1681
- }
1682
- targetOffset += result.revertedText.length;
1683
- onChange(result.segments);
1684
- renderSegmentsToDOM(result.segments);
1685
- setCursorAtOffset(editor, targetOffset);
1686
- if (revertedChip?.type === "chip") {
1687
- onChipDelete?.(revertedChip);
1688
- }
1689
- return true;
1690
- },
1691
- [readSegmentsFromDOM, onChange, renderSegmentsToDOM, onChipDelete]
1692
- );
1693
- const handleChipBackspace = useCallback(() => {
1694
- const editor = editorRef.current;
1695
- if (!editor) return false;
1696
- const range = getSelectionRange();
1697
- if (!range || !range.collapsed) return false;
1698
- const node = range.startContainer;
1699
- const offset = range.startOffset;
1700
- if (node === editor && offset > 0) {
1701
- const prevChild = editor.childNodes[offset - 1];
1702
- if (prevChild && isChipElement(prevChild)) {
1703
- if (getChipAutoResolved(prevChild)) {
1704
- return revertChipNodeToText(editor, prevChild);
1705
- }
1706
- return removeChipNodeFromDOM(editor, prevChild);
1707
- }
1708
- }
1709
- if (node.nodeType === Node.TEXT_NODE && offset === 0) {
1710
- const directChild = getDirectChildContaining(editor, node);
1711
- if (!directChild) return false;
1712
- let prevSibling = directChild.previousSibling;
1713
- while (prevSibling && prevSibling.nodeType === Node.TEXT_NODE && prevSibling.textContent === "") {
1714
- prevSibling = prevSibling.previousSibling;
1715
- }
1716
- if (prevSibling && isChipElement(prevSibling)) {
1717
- if (getChipAutoResolved(prevSibling)) {
1718
- return revertChipNodeToText(editor, prevSibling);
1719
- }
1720
- return removeChipNodeFromDOM(editor, prevSibling);
1721
- }
1722
- }
1723
- return false;
1724
- }, [removeChipNodeFromDOM, revertChipNodeToText]);
1725
- const handleChipForwardDelete = useCallback(() => {
1726
- const editor = editorRef.current;
1727
- if (!editor) return false;
1728
- const range = getSelectionRange();
1729
- if (!range || !range.collapsed) return false;
1730
- const node = range.startContainer;
1731
- const offset = range.startOffset;
1732
- if (node === editor && offset < editor.childNodes.length) {
1733
- const nextChild = editor.childNodes[offset];
1734
- if (nextChild && isChipElement(nextChild)) {
1735
- return removeChipNodeFromDOM(editor, nextChild);
1736
- }
1737
- }
1738
- if (node.nodeType === Node.TEXT_NODE && offset === (node.textContent ?? "").length) {
1739
- const directChild = getDirectChildContaining(editor, node);
1740
- if (!directChild) return false;
1741
- let nextSibling = directChild.nextSibling;
1742
- while (nextSibling && nextSibling.nodeType === Node.TEXT_NODE && nextSibling.textContent === "") {
1743
- nextSibling = nextSibling.nextSibling;
1744
- }
1745
- if (nextSibling && isChipElement(nextSibling)) {
1746
- return removeChipNodeFromDOM(editor, nextSibling);
1747
- }
1748
- }
1749
- return false;
1750
- }, [removeChipNodeFromDOM]);
1751
- const autoResolveActiveTrigger = useCallback(
1752
- (trigger) => {
1753
- const segments = readSegmentsFromDOM();
1754
- const query = trigger.query;
1755
- const syntheticSuggestion = {
1756
- value: query,
1757
- label: query
1758
- };
1759
- const displayText = trigger.config.onSelect?.(syntheticSuggestion) ?? query;
1760
- const chipData = {
1761
- value: query,
1762
- displayText: displayText || query,
1763
- autoResolved: true
1764
- };
1765
- const result = resolveChip(segments, trigger, chipData);
1766
- onChange(result.segments);
1767
- renderSegmentsToDOM(result.segments);
1768
- onChipAdd?.({
1769
- type: "chip",
1770
- trigger: trigger.config.char,
1771
- ...chipData
1772
- });
1773
- const editor = editorRef.current;
1774
- if (editor) {
1775
- setCursorAtOffset(editor, result.cursorOffset);
1776
- }
1777
- dismissTrigger();
1778
- },
1779
- [readSegmentsFromDOM, onChange, renderSegmentsToDOM, dismissTrigger, onChipAdd]
1780
- );
1781
- const selectSuggestionInternal = useCallback(
1782
- (suggestion) => {
1783
- if (!activeTrigger) return;
1784
- const segments = readSegmentsFromDOM();
1785
- const displayText = activeTrigger.config.onSelect?.(suggestion) ?? suggestion.label;
1786
- const chipData = {
1787
- value: suggestion.value,
1788
- displayText: displayText || suggestion.label,
1789
- data: suggestion.data
1790
- };
1791
- const result = resolveChip(segments, activeTrigger, chipData);
1792
- onChange(result.segments);
1793
- renderSegmentsToDOM(result.segments);
1794
- onChipAdd?.({
1795
- type: "chip",
1796
- trigger: activeTrigger.config.char,
1797
- ...chipData
1798
- });
1799
- const editor = editorRef.current;
1800
- if (editor) {
1801
- setCursorAtOffset(editor, result.cursorOffset);
1802
- }
1803
- dismissTrigger();
1804
- setTimeout(() => {
1805
- editorRef.current?.focus();
1806
- }, 0);
1807
- },
1808
- [activeTrigger, readSegmentsFromDOM, onChange, renderSegmentsToDOM, dismissTrigger, onChipAdd]
1809
- );
1810
- const selectSuggestion = selectSuggestionInternal;
1811
- const handleKeyDown = useCallback(
1812
- (e) => {
1813
- const applyEditResult = (editor, result) => {
1814
- lastRenderedValue.current = result.segments;
1815
- onChange(result.segments);
1816
- renderSegmentsToDOM(result.segments);
1817
- setCursorAtOffset(editor, result.cursorOffset);
1818
- };
1819
- const tryListContinuation = (editor) => {
1820
- if (!markdownEnabled) return false;
1821
- const segments = readSegmentsFromDOM();
1822
- const cursorPos = getCursorOffset(editor);
1823
- if (cursorPos === null) return false;
1824
- const plainText = segmentsToPlainText(segments);
1825
- if (!getListContext(plainText, cursorPos)) return false;
1826
- const result = insertListContinuation(segments, cursorPos);
1827
- if (result) applyEditResult(editor, result);
1828
- return true;
1829
- };
1830
- if ((e.metaKey || e.ctrlKey) && e.key === "z" && undoBaseState.current) {
1831
- if (undoTimer.current) {
1832
- clearTimeout(undoTimer.current);
1833
- undoTimer.current = null;
1834
- }
1835
- events.pushUndo(undoBaseState.current);
1836
- undoBaseState.current = null;
1837
- }
1838
- if (events.handleKeyDownForUndoRedo(e)) return;
1839
- if (markdownEnabled && (e.metaKey || e.ctrlKey) && !e.shiftKey && (e.key === "b" || e.key === "i")) {
1840
- e.preventDefault();
1841
- const editor = editorRef.current;
1842
- if (!editor) return;
1843
- const offsets = getSelectionOffsets(editor);
1844
- if (!offsets || offsets.start === offsets.end) return;
1845
- const marker = e.key === "b" ? "**" : "*";
1846
- const currentSegments = readSegmentsFromDOM();
1847
- events.pushUndo(currentSegments);
1848
- const result = toggleMarkdownWrap(currentSegments, offsets.start, offsets.end, marker);
1849
- if (!result) return;
1850
- lastRenderedValue.current = result.segments;
1851
- onChange(result.segments);
1852
- renderSegmentsToDOM(result.segments);
1853
- setSelectionAtOffsets(editor, result.selectionStart, result.selectionEnd);
1854
- return;
1855
- }
1856
- if (activeTrigger && activeTrigger.config.mode === "dropdown" && suggestions.length > 0) {
1857
- if (e.key === "ArrowDown") {
1858
- e.preventDefault();
1859
- setSelectedSuggestionIndex((prev) => Math.min(prev + 1, suggestions.length - 1));
1860
- return;
1861
- }
1862
- if (e.key === "ArrowUp") {
1863
- e.preventDefault();
1864
- setSelectedSuggestionIndex((prev) => Math.max(prev - 1, 0));
1865
- return;
1866
- }
1867
- if (e.key === "Enter" || e.key === "Tab") {
1868
- e.preventDefault();
1869
- const selected = suggestions[selectedSuggestionIndex];
1870
- if (selected) {
1871
- selectSuggestionInternal(selected);
1872
- }
1873
- return;
1874
- }
1875
- if (e.key === "Escape") {
1876
- e.preventDefault();
1877
- dismissTrigger();
1878
- return;
1879
- }
1880
- }
1881
- if (e.key === " " && activeTrigger && activeTrigger.config.resolveOnSpace) {
1882
- const query = activeTrigger.query.trim();
1883
- if (query.length > 0) {
1884
- e.preventDefault();
1885
- autoResolveActiveTrigger(activeTrigger);
1886
- return;
1887
- }
1888
- }
1889
- if (markdownEnabled && e.key === "Tab" && !activeTrigger) {
1890
- const editor = editorRef.current;
1891
- if (editor) {
1892
- const segments = readSegmentsFromDOM();
1893
- const plainText = segmentsToPlainText(segments);
1894
- const cursorPos = getCursorOffset(editor);
1895
- if (cursorPos !== null) {
1896
- const ctx = getListContext(plainText, cursorPos);
1897
- if (ctx) {
1898
- e.preventDefault();
1899
- const result = e.shiftKey ? outdentListItem(segments, cursorPos) : indentListItem(segments, cursorPos);
1900
- if (result) applyEditResult(editor, result);
1901
- return;
1902
- }
1903
- }
1904
- }
1905
- }
1906
- if (e.key === "Enter" && e.shiftKey && !e.nativeEvent.isComposing) {
1907
- e.preventDefault();
1908
- const editor = editorRef.current;
1909
- if (editor) {
1910
- if (tryListContinuation(editor)) return;
1911
- const offsets = getSelectionOffsets(editor);
1912
- if (offsets) {
1913
- const currentSegments = readSegmentsFromDOM();
1914
- events.pushUndo(currentSegments);
1915
- const newSegments = replaceTextRange(currentSegments, offsets.start, offsets.end, "\n");
1916
- applyEditResult(editor, { segments: newSegments, cursorOffset: offsets.start + 1 });
1917
- }
1918
- }
1919
- return;
1920
- }
1921
- if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
1922
- const editor = editorRef.current;
1923
- if (editor && tryListContinuation(editor)) {
1924
- e.preventDefault();
1925
- return;
1926
- }
1927
- if (onSubmit) {
1928
- e.preventDefault();
1929
- onSubmit(readSegmentsFromDOM());
1930
- return;
1931
- }
1932
- }
1933
- if (e.key === "Escape" && onEscape) {
1934
- onEscape();
1935
- return;
1936
- }
1937
- if ((e.key === "Backspace" || e.key === "Delete") && !e.nativeEvent.isComposing) {
1938
- const editor = editorRef.current;
1939
- if (editor) {
1940
- const offsets = getSelectionOffsets(editor);
1941
- if (offsets && offsets.start !== offsets.end) {
1942
- e.preventDefault();
1943
- const currentSegments = readSegmentsFromDOM();
1944
- events.pushUndo(currentSegments);
1945
- const newSegments = replaceTextRange(currentSegments, offsets.start, offsets.end, "");
1946
- applyEditResult(editor, { segments: newSegments, cursorOffset: offsets.start });
1947
- runTriggerDetection();
1948
- return;
1949
- }
1950
- }
1951
- }
1952
- if (e.key === "Backspace") {
1953
- const editor = editorRef.current;
1954
- if (editor) {
1955
- const segments = readSegmentsFromDOM();
1956
- const cursorPos = getCursorOffset(editor);
1957
- if (markdownEnabled && cursorPos !== null) {
1958
- const result = removeListPrefix(segments, cursorPos);
1959
- if (result) {
1960
- e.preventDefault();
1961
- applyEditResult(editor, result);
1962
- runTriggerDetection();
1963
- return;
1964
- }
1965
- }
1966
- }
1967
- if (handleChipBackspace()) {
1968
- e.preventDefault();
1969
- runTriggerDetection();
1970
- return;
1971
- }
1972
- }
1973
- if (e.key === "Delete" && handleChipForwardDelete()) {
1974
- e.preventDefault();
1975
- runTriggerDetection();
1976
- return;
1977
- }
1978
- },
1979
- [
1980
- activeTrigger,
1981
- suggestions,
1982
- selectedSuggestionIndex,
1983
- onSubmit,
1984
- onEscape,
1985
- readSegmentsFromDOM,
1986
- onChange,
1987
- renderSegmentsToDOM,
1988
- markdownEnabled,
1989
- dismissTrigger,
1990
- handleChipBackspace,
1991
- handleChipForwardDelete,
1992
- autoResolveActiveTrigger,
1993
- runTriggerDetection,
1994
- selectSuggestionInternal,
1995
- events
1996
- ]
1997
- );
1998
- const handle = useMemo(
1999
- () => ({
2000
- focus: () => editorRef.current?.focus(),
2001
- blur: () => editorRef.current?.blur(),
2002
- insertChip: (chip) => {
2003
- const segments = readSegmentsFromDOM();
2004
- const newChip = { type: "chip", ...chip };
2005
- const newSegments = [...segments, newChip, { type: "text", text: " " }];
2006
- onChange(newSegments);
2007
- renderSegmentsToDOM(newSegments);
2008
- onChipAdd?.(newChip);
2009
- },
2010
- getPlainText: () => segmentsToPlainText(readSegmentsFromDOM()),
2011
- clear: () => {
2012
- onChange([]);
2013
- const editor = editorRef.current;
2014
- if (editor) {
2015
- while (editor.firstChild) editor.removeChild(editor.firstChild);
2016
- }
2017
- events.resetUndoHistory();
2018
- if (undoTimer.current) {
2019
- clearTimeout(undoTimer.current);
2020
- undoTimer.current = null;
2021
- }
2022
- undoBaseState.current = null;
2023
- }
2024
- }),
2025
- [readSegmentsFromDOM, onChange, renderSegmentsToDOM, onChipAdd, events]
2026
- );
2027
- const eventHandlers = useMemo(
2028
- () => ({
2029
- onPaste: events.handlePaste,
2030
- onCopy: events.handleCopy,
2031
- onCut: events.handleCut,
2032
- onDrop: events.handleDrop,
2033
- onDragOver: events.handleDragOver,
2034
- onCompositionStart: events.handleCompositionStart,
2035
- onCompositionEnd: events.handleCompositionEnd,
2036
- onBlur: events.handleBlur
2037
- }),
2038
- [
2039
- events.handlePaste,
2040
- events.handleCopy,
2041
- events.handleCut,
2042
- events.handleDrop,
2043
- events.handleDragOver,
2044
- events.handleCompositionStart,
2045
- events.handleCompositionEnd,
2046
- events.handleBlur
2047
- ]
2048
- );
2049
- return {
2050
- editorRef,
2051
- activeTrigger,
2052
- suggestions,
2053
- suggestionsLoading,
2054
- suggestionsError,
2055
- selectedSuggestionIndex,
2056
- handleInput,
2057
- handleKeyDown,
2058
- handleClick,
2059
- selectSuggestion,
2060
- dismissTrigger,
2061
- handle,
2062
- triggerRect,
2063
- eventHandlers
2064
- };
2065
- }
2066
- function TriggerPopover({
2067
- suggestions,
2068
- loading,
2069
- error,
2070
- emptyMessage,
2071
- selectedIndex,
2072
- onSelect,
2073
- onDismiss,
2074
- triggerRect,
2075
- triggerChar
2076
- }) {
2077
- const popoverRef = useRef(null);
2078
- const selectedRef = useRef(null);
2079
- useEffect(() => {
2080
- selectedRef.current?.scrollIntoView({ block: "nearest" });
2081
- }, [selectedIndex]);
2082
- useEffect(() => {
2083
- const handleClickOutside = (e) => {
2084
- const target = e.target;
2085
- if (popoverRef.current && target instanceof Node && !popoverRef.current.contains(target)) {
2086
- onDismiss();
2087
- }
2088
- };
2089
- document.addEventListener("mousedown", handleClickOutside);
2090
- return () => document.removeEventListener("mousedown", handleClickOutside);
2091
- }, [onDismiss]);
2092
- if (!triggerRect) return null;
2093
- if (suggestions.length === 0 && !loading && !error && !emptyMessage) return null;
2094
- const popoverMaxWidth = Math.min(320, window.innerWidth - 16);
2095
- const left = Math.min(triggerRect.left, window.innerWidth - popoverMaxWidth - 8);
2096
- const style = {
2097
- position: "fixed",
2098
- left: `${Math.max(8, left)}px`,
2099
- top: `${triggerRect.bottom + 4}px`,
2100
- zIndex: 50,
2101
- maxWidth: `${popoverMaxWidth}px`
2102
- };
2103
- return /* @__PURE__ */ jsx(
2104
- "div",
2105
- {
2106
- ref: popoverRef,
2107
- className: cn(
2108
- "max-h-[240px] min-w-[200px] overflow-y-auto",
2109
- "bg-popover rounded-xl border p-2 shadow-md",
2110
- "animate-in fade-in-0 zoom-in-95"
2111
- ),
2112
- style,
2113
- role: "listbox",
2114
- "aria-label": `${triggerChar} suggestions`,
2115
- children: loading ? /* @__PURE__ */ jsx(
2116
- "div",
2117
- {
2118
- role: "option",
2119
- "aria-selected": false,
2120
- className: "text-muted-foreground px-3 py-2 text-sm",
2121
- children: "Loading suggestions..."
2122
- }
2123
- ) : error ? /* @__PURE__ */ jsx("div", { role: "option", "aria-selected": false, className: "text-destructive px-3 py-2 text-sm", children: error }) : suggestions.length === 0 && emptyMessage ? /* @__PURE__ */ jsx(
2124
- "div",
2125
- {
2126
- role: "option",
2127
- "aria-selected": false,
2128
- className: "text-muted-foreground px-3 py-2 text-sm",
2129
- children: emptyMessage
2130
- }
2131
- ) : suggestions.map((suggestion, index) => /* @__PURE__ */ jsxs(
2132
- "button",
2133
- {
2134
- ref: index === selectedIndex ? selectedRef : void 0,
2135
- type: "button",
2136
- role: "option",
2137
- "aria-selected": index === selectedIndex,
2138
- className: cn(
2139
- "text-foreground flex w-full items-start gap-2 rounded-lg px-3 py-2 text-left text-sm",
2140
- "hover:bg-accent cursor-pointer transition-colors",
2141
- index === selectedIndex && "bg-accent"
2142
- ),
2143
- onMouseDown: (e) => {
2144
- e.preventDefault();
2145
- onSelect(suggestion);
2146
- },
2147
- children: [
2148
- suggestion.icon && /* @__PURE__ */ jsx("span", { className: "mt-0.5 shrink-0", children: suggestion.icon }),
2149
- /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
2150
- /* @__PURE__ */ jsx("div", { className: "truncate font-medium", children: suggestion.label }),
2151
- suggestion.description && /* @__PURE__ */ jsx("div", { className: "text-muted-foreground truncate text-xs", children: suggestion.description })
2152
- ] })
2153
- ]
2154
- },
2155
- suggestion.value
2156
- ))
2157
- }
2158
- );
2159
- }
2160
- function AnimatedPlaceholder({ texts, interval = 3e3 }) {
2161
- const [index, setIndex] = useState(0);
2162
- useEffect(() => {
2163
- if (texts.length <= 1) return;
2164
- const id = setInterval(() => {
2165
- setIndex((prev) => (prev + 1) % texts.length);
2166
- }, interval);
2167
- return () => clearInterval(id);
2168
- }, [texts.length, interval]);
2169
- return /* @__PURE__ */ jsx(
2170
- "div",
2171
- {
2172
- className: "pointer-events-none absolute top-0 left-0 overflow-hidden text-sm leading-relaxed select-none",
2173
- style: { color: "var(--prompt-area-placeholder, var(--muted-foreground))" },
2174
- "aria-hidden": "true",
2175
- children: /* @__PURE__ */ jsx(
2176
- "div",
2177
- {
2178
- className: "animate-in fade-in-0 slide-in-from-top-4 duration-300 ease-in-out",
2179
- children: texts[index]
2180
- },
2181
- index
2182
- )
2183
- }
2184
- );
2185
- }
2186
- function RemoveButton({ onClick, label, className }) {
2187
- return /* @__PURE__ */ jsx(
2188
- "button",
2189
- {
2190
- type: "button",
2191
- onClick: (e) => {
2192
- e.stopPropagation();
2193
- onClick();
2194
- },
2195
- className: cn(
2196
- "absolute top-0.5 right-0.5 grid h-3.5 w-3.5 cursor-pointer place-items-center",
2197
- "rounded-full bg-black/60 text-white hover:bg-black/80 dark:bg-white/60 dark:text-black dark:hover:bg-white/80",
2198
- "transition-colors",
2199
- className
2200
- ),
2201
- "aria-label": label,
2202
- children: /* @__PURE__ */ jsxs(
2203
- "svg",
2204
- {
2205
- width: "8",
2206
- height: "8",
2207
- viewBox: "0 0 10 10",
2208
- fill: "none",
2209
- stroke: "currentColor",
2210
- strokeWidth: "1.5",
2211
- strokeLinecap: "round",
2212
- children: [
2213
- /* @__PURE__ */ jsx("line", { x1: "2.75", y1: "2.75", x2: "7.25", y2: "7.25" }),
2214
- /* @__PURE__ */ jsx("line", { x1: "7.25", y1: "2.75", x2: "2.75", y2: "7.25" })
2215
- ]
2216
- }
2217
- )
2218
- }
2219
- );
2220
- }
2221
- function ImageStrip({ images, onRemove, onClick, className }) {
2222
- if (images.length === 0) return null;
2223
- return /* @__PURE__ */ jsx("div", { className: cn("flex flex-wrap gap-2", className), role: "list", "aria-label": "Attached images", children: images.map((image) => /* @__PURE__ */ jsxs(
2224
- "div",
2225
- {
2226
- role: "listitem",
2227
- className: cn(
2228
- "border-border relative h-16 w-16 flex-shrink-0 overflow-hidden rounded-md border",
2229
- onClick && "cursor-pointer"
2230
- ),
2231
- onClick: () => onClick?.(image),
2232
- children: [
2233
- /* @__PURE__ */ jsx(
2234
- "img",
2235
- {
2236
- src: image.url,
2237
- alt: image.alt ?? "Attached image",
2238
- className: "h-full w-full object-cover"
2239
- }
2240
- ),
2241
- image.loading && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-center justify-center bg-black/40", children: /* @__PURE__ */ jsx("div", { className: "h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" }) }),
2242
- onRemove && /* @__PURE__ */ jsx(
2243
- RemoveButton,
2244
- {
2245
- onClick: () => onRemove(image),
2246
- label: `Remove ${image.alt ?? "image"}`
2247
- }
2248
- )
2249
- ]
2250
- },
2251
- image.id
2252
- )) });
2253
- }
2254
- function Svg({ className, children }) {
2255
- return /* @__PURE__ */ jsx(
2256
- "svg",
2257
- {
2258
- xmlns: "http://www.w3.org/2000/svg",
2259
- width: "24",
2260
- height: "24",
2261
- viewBox: "0 0 24 24",
2262
- fill: "none",
2263
- stroke: "currentColor",
2264
- strokeWidth: "2",
2265
- strokeLinecap: "round",
2266
- strokeLinejoin: "round",
2267
- "aria-hidden": "true",
2268
- className,
2269
- children
2270
- }
2271
- );
2272
- }
2273
- var FileBody = /* @__PURE__ */ jsxs(Fragment, { children: [
2274
- /* @__PURE__ */ jsx("path", { d: "M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" }),
2275
- /* @__PURE__ */ jsx("path", { d: "M14 2v5a1 1 0 0 0 1 1h5" })
2276
- ] });
2277
- var File = ({ className }) => /* @__PURE__ */ jsx(Svg, { className, children: FileBody });
2278
- var FileText = ({ className }) => /* @__PURE__ */ jsxs(Svg, { className, children: [
2279
- FileBody,
2280
- /* @__PURE__ */ jsx("path", { d: "M10 9H8" }),
2281
- /* @__PURE__ */ jsx("path", { d: "M16 13H8" }),
2282
- /* @__PURE__ */ jsx("path", { d: "M16 17H8" })
2283
- ] });
2284
- var FileSpreadsheet = ({ className }) => /* @__PURE__ */ jsxs(Svg, { className, children: [
2285
- FileBody,
2286
- /* @__PURE__ */ jsx("path", { d: "M8 13h2" }),
2287
- /* @__PURE__ */ jsx("path", { d: "M14 13h2" }),
2288
- /* @__PURE__ */ jsx("path", { d: "M8 17h2" }),
2289
- /* @__PURE__ */ jsx("path", { d: "M14 17h2" })
2290
- ] });
2291
- var FileCode = ({ className }) => /* @__PURE__ */ jsxs(Svg, { className, children: [
2292
- FileBody,
2293
- /* @__PURE__ */ jsx("path", { d: "M10 12.5 8 15l2 2.5" }),
2294
- /* @__PURE__ */ jsx("path", { d: "m14 12.5 2 2.5-2 2.5" })
2295
- ] });
2296
- var ImageIcon = ({ className }) => /* @__PURE__ */ jsxs(Svg, { className, children: [
2297
- /* @__PURE__ */ jsx("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2", ry: "2" }),
2298
- /* @__PURE__ */ jsx("circle", { cx: "9", cy: "9", r: "2" }),
2299
- /* @__PURE__ */ jsx("path", { d: "m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" })
2300
- ] });
2301
- var COLLAPSE_THRESHOLD = 3;
2302
- function getFileIconKey(type) {
2303
- if (!type) return "default";
2304
- if (type === "application/pdf") return "pdf";
2305
- if (type.includes("spreadsheet") || type === "text/csv") return "spreadsheet";
2306
- if (type.startsWith("text/") || type.includes("javascript") || type.includes("json") || type.includes("xml"))
2307
- return "code";
2308
- if (type.startsWith("image/")) return "image";
2309
- return "default";
2310
- }
2311
- var FILE_ICONS = {
2312
- pdf: FileText,
2313
- spreadsheet: FileSpreadsheet,
2314
- code: FileCode,
2315
- image: ImageIcon,
2316
- default: File
2317
- };
2318
- function formatFileSize(bytes) {
2319
- if (bytes < 1024) return `${bytes} B`;
2320
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
2321
- if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
2322
- return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
2323
- }
2324
- function getExtensionLabel(name) {
2325
- const dot = name.lastIndexOf(".");
2326
- if (dot === -1 || dot === name.length - 1) return null;
2327
- return name.slice(dot + 1).toUpperCase();
2328
- }
2329
- function FileCard({
2330
- file,
2331
- compact,
2332
- onRemove,
2333
- onClick
2334
- }) {
2335
- const ext = getExtensionLabel(file.name);
2336
- const sizeStr = file.size != null ? formatFileSize(file.size) : null;
2337
- const meta = [ext, sizeStr].filter(Boolean).join(" \xB7 ");
2338
- return /* @__PURE__ */ jsxs(
2339
- "div",
2340
- {
2341
- role: "listitem",
2342
- className: cn(
2343
- "border-border relative flex flex-shrink-0 items-center gap-2 overflow-hidden rounded-lg border transition-colors",
2344
- "hover:bg-accent",
2345
- compact ? "h-10 w-36 px-2" : "h-14 w-48 px-3",
2346
- onClick && "cursor-pointer"
2347
- ),
2348
- onClick: () => onClick?.(file),
2349
- children: [
2350
- (() => {
2351
- const Icon = FILE_ICONS[getFileIconKey(file.type)];
2352
- return /* @__PURE__ */ jsx(
2353
- Icon,
2354
- {
2355
- className: cn("text-muted-foreground flex-shrink-0", compact ? "h-4 w-4" : "h-5 w-5")
2356
- }
2357
- );
2358
- })(),
2359
- /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
2360
- /* @__PURE__ */ jsx(
2361
- "div",
2362
- {
2363
- className: cn("truncate font-medium", compact ? "text-xs" : "text-sm"),
2364
- title: file.name,
2365
- children: file.name
2366
- }
2367
- ),
2368
- !compact && meta && /* @__PURE__ */ jsx("div", { className: "text-muted-foreground truncate text-xs", children: meta })
2369
- ] }),
2370
- file.loading && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-center justify-center bg-black/40", children: /* @__PURE__ */ jsx("div", { className: "h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" }) }),
2371
- onRemove && /* @__PURE__ */ jsx(RemoveButton, { onClick: () => onRemove(file), label: `Remove ${file.name}` })
2372
- ]
2373
- }
2374
- );
2375
- }
2376
- function FileStrip({ files, onRemove, onClick, className }) {
2377
- const [expanded, setExpanded] = useState(false);
2378
- const popoverRef = useRef(null);
2379
- const toggleRef = useRef(null);
2380
- useEffect(() => {
2381
- if (!expanded) return;
2382
- const handleClick = (e) => {
2383
- const target = e.target;
2384
- if (popoverRef.current && !popoverRef.current.contains(target) && !toggleRef.current?.contains(target)) {
2385
- setExpanded(false);
2386
- }
2387
- };
2388
- document.addEventListener("mousedown", handleClick);
2389
- return () => document.removeEventListener("mousedown", handleClick);
2390
- }, [expanded]);
2391
- if (files.length === 0) return null;
2392
- const collapsible = files.length > COLLAPSE_THRESHOLD;
2393
- const compact = collapsible;
2394
- const hiddenCount = files.length - COLLAPSE_THRESHOLD;
2395
- const visibleFiles = files.slice(0, COLLAPSE_THRESHOLD);
2396
- return /* @__PURE__ */ jsxs("div", { className: cn("relative", className), children: [
2397
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", role: "list", "aria-label": "Attached files", children: [
2398
- (collapsible ? visibleFiles : files).map((file) => /* @__PURE__ */ jsx(
2399
- FileCard,
2400
- {
2401
- file,
2402
- compact,
2403
- onRemove,
2404
- onClick
2405
- },
2406
- file.id
2407
- )),
2408
- collapsible && /* @__PURE__ */ jsx("div", { role: "listitem", children: /* @__PURE__ */ jsx(
2409
- "button",
2410
- {
2411
- ref: toggleRef,
2412
- type: "button",
2413
- onClick: () => setExpanded((v) => !v),
2414
- className: cn(
2415
- "border-border text-muted-foreground hover:bg-accent flex flex-shrink-0 cursor-pointer items-center justify-center rounded-lg border transition-colors",
2416
- compact ? "h-10 px-3 text-xs" : "h-14 px-4 text-sm"
2417
- ),
2418
- children: expanded ? "Show less" : `+${hiddenCount} more`
2419
- }
2420
- ) })
2421
- ] }),
2422
- expanded && /* @__PURE__ */ jsx(
2423
- "div",
2424
- {
2425
- ref: popoverRef,
2426
- className: cn(
2427
- "bg-popover border-border absolute bottom-full left-0 z-10 mb-2 max-h-48 overflow-y-auto rounded-lg border p-2 shadow-lg"
2428
- ),
2429
- children: /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", role: "list", "aria-label": "More attached files", children: files.slice(COLLAPSE_THRESHOLD).map((file) => /* @__PURE__ */ jsx(
2430
- FileCard,
2431
- {
2432
- file,
2433
- compact,
2434
- onRemove,
2435
- onClick
2436
- },
2437
- file.id
2438
- )) })
2439
- }
2440
- )
2441
- ] });
2442
- }
2443
- function PromptArea({
2444
- value,
2445
- onChange,
2446
- triggers,
2447
- placeholder,
2448
- className,
2449
- disabled = false,
2450
- markdown,
2451
- onSubmit,
2452
- onEscape,
2453
- onChipClick,
2454
- onChipAdd,
2455
- onChipDelete,
2456
- onLinkClick,
2457
- onPaste,
2458
- onUndo,
2459
- onRedo,
2460
- minHeight = 80,
2461
- maxHeight,
2462
- autoFocus = false,
2463
- autoGrow = false,
2464
- "aria-label": ariaLabel,
2465
- "data-test-id": dataTestId,
2466
- images = [],
2467
- imagePosition = "above",
2468
- onImagePaste,
2469
- onImageRemove,
2470
- onImageClick,
2471
- files = [],
2472
- filePosition = "above",
2473
- onFileRemove,
2474
- onFileClick,
2475
- ref
2476
- }) {
2477
- const {
2478
- editorRef,
2479
- activeTrigger,
2480
- suggestions,
2481
- suggestionsLoading,
2482
- suggestionsError,
2483
- selectedSuggestionIndex,
2484
- handleInput,
2485
- handleKeyDown,
2486
- handleClick,
2487
- selectSuggestion,
2488
- dismissTrigger,
2489
- handle,
2490
- triggerRect,
2491
- eventHandlers
2492
- } = usePromptArea({
2493
- value,
2494
- onChange,
2495
- triggers,
2496
- onSubmit,
2497
- onEscape,
2498
- onChipClick,
2499
- onChipAdd,
2500
- onChipDelete,
2501
- onLinkClick,
2502
- onPaste,
2503
- onUndo,
2504
- onRedo,
2505
- onImagePaste,
2506
- markdown
2507
- });
2508
- useImperativeHandle(ref, () => handle, [handle]);
2509
- useEffect(() => {
2510
- if (autoFocus) {
2511
- editorRef.current?.focus();
2512
- }
2513
- }, [autoFocus, editorRef]);
2514
- const [isFocused, setIsFocused] = useState(false);
2515
- const [editorHeight, setEditorHeight] = useState(void 0);
2516
- const syncHeight = useCallback(() => {
2517
- const el = editorRef.current;
2518
- if (!el) return;
2519
- el.style.height = "auto";
2520
- const contentHeight = el.scrollHeight;
2521
- el.style.height = `${contentHeight}px`;
2522
- setEditorHeight(contentHeight);
2523
- }, [editorRef]);
2524
- const handleFocus = useCallback(() => {
2525
- if (!autoGrow) return;
2526
- setIsFocused(true);
2527
- syncHeight();
2528
- }, [autoGrow, syncHeight]);
2529
- const handleBlurWithShrink = useCallback(() => {
2530
- eventHandlers.onBlur();
2531
- if (!autoGrow) return;
2532
- setTimeout(() => {
2533
- const editor = editorRef.current;
2534
- if (!editor) return;
2535
- const activeEl = document.activeElement;
2536
- if (activeEl && editor.parentElement?.contains(activeEl)) return;
2537
- setIsFocused(false);
2538
- setEditorHeight(void 0);
2539
- }, BLUR_DELAY_MS);
2540
- }, [eventHandlers, autoGrow, editorRef]);
2541
- const handleInputWithGrow = useCallback(() => {
2542
- handleInput();
2543
- if (autoGrow && isFocused) {
2544
- syncHeight();
2545
- }
2546
- }, [handleInput, autoGrow, isFocused, syncHeight]);
2547
- useEffect(() => {
2548
- if (autoGrow && isFocused) {
2549
- requestAnimationFrame(() => syncHeight());
2550
- }
2551
- }, [value, autoGrow, isFocused, syncHeight]);
2552
- const [hasOverflow, setHasOverflow] = useState(false);
2553
- const overflowTimerRef = useRef(null);
2554
- useEffect(() => {
2555
- if (!autoGrow) return;
2556
- const checkOverflow = () => {
2557
- if (isFocused) {
2558
- setHasOverflow(false);
2559
- return;
2560
- }
2561
- const el = editorRef.current;
2562
- if (!el) return;
2563
- setHasOverflow(el.scrollHeight > el.clientHeight);
2564
- };
2565
- const delay = isFocused ? 0 : 160;
2566
- overflowTimerRef.current = setTimeout(checkOverflow, delay);
2567
- return () => {
2568
- if (overflowTimerRef.current !== null) {
2569
- clearTimeout(overflowTimerRef.current);
2570
- }
2571
- };
2572
- }, [autoGrow, isFocused, value, editorRef]);
2573
- const editorStyle = useMemo(() => {
2574
- if (!autoGrow) {
2575
- return {
2576
- minHeight: `${minHeight}px`,
2577
- ...maxHeight ? { maxHeight: `${maxHeight}px`, overflowY: "auto" } : {}
2578
- };
2579
- }
2580
- return {
2581
- height: isFocused && editorHeight ? `${editorHeight}px` : `${minHeight}px`,
2582
- minHeight: `${minHeight}px`,
2583
- maxHeight: "70dvh",
2584
- overflowY: isFocused ? "auto" : "hidden",
2585
- transition: "height 150ms ease-out"
2586
- };
2587
- }, [autoGrow, minHeight, maxHeight, isFocused, editorHeight]);
2588
- const isEmpty = value.length === 0 || value.length === 1 && value[0].type === "text" && value[0].text === "";
2589
- const imageStrip = images.length > 0 ? /* @__PURE__ */ jsx(
2590
- ImageStrip,
2591
- {
2592
- images,
2593
- onRemove: onImageRemove,
2594
- onClick: onImageClick,
2595
- className: imagePosition === "above" ? "pb-2" : "pt-2"
2596
- }
2597
- ) : null;
2598
- const fileStrip = files.length > 0 ? /* @__PURE__ */ jsx(
2599
- FileStrip,
2600
- {
2601
- files,
2602
- onRemove: onFileRemove,
2603
- onClick: onFileClick,
2604
- className: filePosition === "above" ? "pb-2" : "pt-2"
2605
- }
2606
- ) : null;
2607
- return /* @__PURE__ */ jsxs("div", { className: cn("prompt-area-container relative", className), children: [
2608
- imagePosition === "above" && imageStrip,
2609
- filePosition === "above" && fileStrip,
2610
- /* @__PURE__ */ jsxs("div", { className: "relative", children: [
2611
- /* @__PURE__ */ jsx(
2612
- "div",
2613
- {
2614
- ref: editorRef,
2615
- contentEditable: !disabled,
2616
- suppressContentEditableWarning: true,
2617
- role: "textbox",
2618
- "aria-label": ariaLabel ?? "Text input",
2619
- "aria-multiline": "true",
2620
- "aria-disabled": disabled || void 0,
2621
- "data-test-id": dataTestId,
2622
- className: cn(
2623
- "prompt-area-editor",
2624
- "w-full min-w-0 break-words whitespace-pre-wrap outline-none",
2625
- "text-sm leading-relaxed",
2626
- disabled && "cursor-not-allowed opacity-50"
2627
- ),
2628
- style: editorStyle,
2629
- onFocus: handleFocus,
2630
- onInput: autoGrow ? handleInputWithGrow : handleInput,
2631
- onKeyDown: handleKeyDown,
2632
- onClick: handleClick,
2633
- onPaste: eventHandlers.onPaste,
2634
- onCopy: eventHandlers.onCopy,
2635
- onCut: eventHandlers.onCut,
2636
- onDrop: eventHandlers.onDrop,
2637
- onDragOver: eventHandlers.onDragOver,
2638
- onCompositionStart: eventHandlers.onCompositionStart,
2639
- onCompositionEnd: eventHandlers.onCompositionEnd,
2640
- onBlur: autoGrow ? handleBlurWithShrink : eventHandlers.onBlur
2641
- }
2642
- ),
2643
- autoGrow && hasOverflow && !isFocused && /* @__PURE__ */ jsx(
2644
- "div",
2645
- {
2646
- "aria-hidden": "true",
2647
- className: "pointer-events-auto absolute right-0 bottom-0 left-0 cursor-pointer",
2648
- style: { height: "32px" },
2649
- onClick: () => editorRef.current?.focus(),
2650
- children: /* @__PURE__ */ jsx(
2651
- "div",
2652
- {
2653
- className: "h-full w-full",
2654
- style: {
2655
- background: "linear-gradient(to bottom, transparent, color-mix(in srgb, var(--prompt-area-surface, var(--background)) 80%, transparent), var(--prompt-area-surface, var(--background)))"
2656
- }
2657
- }
2658
- )
2659
- }
2660
- ),
2661
- isEmpty && placeholder && (Array.isArray(placeholder) ? /* @__PURE__ */ jsx(AnimatedPlaceholder, { texts: placeholder }) : /* @__PURE__ */ jsx(
2662
- "div",
2663
- {
2664
- className: "pointer-events-none absolute top-0 left-0 text-sm leading-relaxed select-none",
2665
- style: { color: "var(--prompt-area-placeholder, var(--muted-foreground))" },
2666
- "aria-hidden": "true",
2667
- children: placeholder
2668
- }
2669
- ))
2670
- ] }),
2671
- filePosition === "below" && fileStrip,
2672
- imagePosition === "below" && imageStrip,
2673
- activeTrigger && activeTrigger.config.mode === "dropdown" && /* @__PURE__ */ jsx(
2674
- TriggerPopover,
2675
- {
2676
- suggestions,
2677
- loading: suggestionsLoading,
2678
- error: suggestionsError,
2679
- emptyMessage: activeTrigger.config.emptyMessage,
2680
- selectedIndex: selectedSuggestionIndex,
2681
- onSelect: selectSuggestion,
2682
- onDismiss: dismissTrigger,
2683
- triggerRect,
2684
- triggerChar: activeTrigger.config.char
2685
- }
2686
- )
2687
- ] });
2688
- }
2689
-
2690
- export { BLUR_DELAY_MS, PromptArea, detectActiveTrigger, isValidTriggerPosition, mergeAdjacentTextSegments, parseInlineMarkdown, plainTextToSegments, resolveChip, resolveTriggersInSegments, segmentsEqual, segmentsToPlainText, usePromptArea };
2691
- //# sourceMappingURL=chunk-E7HUXORB.js.map
2692
- //# sourceMappingURL=chunk-E7HUXORB.js.map