@bendyline/docblocks-react 2.6.1 → 2.7.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 (54) hide show
  1. package/README.md +3 -10
  2. package/THIRD_PARTY_NOTICES.txt +4284 -3651
  3. package/dist/AiDiagrams-XWOM3YZX.js +13 -0
  4. package/dist/AiEditorAssistant-M4LI2CRY.js +771 -0
  5. package/dist/AudioExportDialog-F23QXNPB.js +253 -0
  6. package/dist/DocumentConflictDialog-5LRVIGXS.js +111 -0
  7. package/dist/ExportDialog-L6FG6W3H.js +9 -0
  8. package/dist/{ExportToolbarControls-QYLBRWM7.js → ExportToolbarControls-BRCPZ7H3.js} +108 -42
  9. package/dist/{GitUI-L5T3XHZG.js → GitUI-LZW3IUXO.js} +5 -5
  10. package/dist/PresentationMaker-622NHMIN.js +9 -0
  11. package/dist/QuickOpenDialog-2KK4SEBU.js +356 -0
  12. package/dist/Settings-DuXk6baH.d.ts +144 -0
  13. package/dist/{ShareDialog-4OX3UPJ4.js → ShareDialog-N7GPM4SB.js} +10 -59
  14. package/dist/SpeechToolbarControl-UZQKGQWD.js +434 -0
  15. package/dist/TextFromNarration-XYRAB6O2.js +598 -0
  16. package/dist/WorkspaceSearchPanel-OUQQ5DOM.js +328 -0
  17. package/dist/chunk-2IYBBFQX.js +201 -0
  18. package/dist/{chunk-LG6HAWCK.js → chunk-56KO2LYN.js} +35 -4
  19. package/dist/chunk-5OZBGIIT.js +1826 -0
  20. package/dist/chunk-6B7G33D5.js +88 -0
  21. package/dist/chunk-ARATARVJ.js +40 -0
  22. package/dist/{chunk-ODYNOPC3.js → chunk-BCQZTWE5.js} +57 -17
  23. package/dist/chunk-EA4BO6E4.js +73 -0
  24. package/dist/chunk-G4MFB3IO.js +208 -0
  25. package/dist/chunk-I44HKDQX.js +314 -0
  26. package/dist/chunk-K4I3NJFL.js +49 -0
  27. package/dist/chunk-KBDZDFPU.js +61 -0
  28. package/dist/{chunk-FNSRCCBC.js → chunk-KWG6UP5O.js} +0 -10
  29. package/dist/chunk-MMOTEQK6.js +518 -0
  30. package/dist/chunk-OINKNX26.js +574 -0
  31. package/dist/chunk-Q5ZVP5LV.js +1711 -0
  32. package/dist/chunk-RP3DVNTI.js +62 -0
  33. package/dist/{chunk-FMDLZ4ZG.js → chunk-SGQKTKDX.js} +2 -2
  34. package/dist/chunk-TBLZBNQF.js +25 -0
  35. package/dist/{chunk-M5Y5WO7Z.js → chunk-UG4G7PJ4.js} +1 -1
  36. package/dist/chunk-UONFWKJ7.js +22 -0
  37. package/dist/chunk-VO2JYYQZ.js +40 -0
  38. package/dist/{chunk-EBWYGTN7.js → chunk-X437UAF7.js} +6 -0
  39. package/dist/chunk-YS4HVWIO.js +63 -0
  40. package/dist/editor.d.ts +36 -1
  41. package/dist/editor.js +16 -1
  42. package/dist/export/index.d.ts +27 -11
  43. package/dist/export/index.js +11 -7
  44. package/dist/index.d.ts +100 -13
  45. package/dist/index.js +3916 -1633
  46. package/dist/{run-export-I2XOVGG4.js → run-export-47X5FTYZ.js} +3 -3
  47. package/dist/settings/index.d.ts +32 -101
  48. package/dist/settings/index.js +14 -2
  49. package/package.json +9 -9
  50. package/src/styles/docblocks.css +2337 -650
  51. package/dist/ExportDialog-Q4VWI43H.js +0 -9
  52. package/dist/chunk-JDJVRDOP.js +0 -92
  53. package/dist/chunk-Q6XNIKDG.js +0 -669
  54. package/dist/{GitToolbarControl-O5AXBC6T.js → GitToolbarControl-J4UYZDV6.js} +3 -3
@@ -0,0 +1,771 @@
1
+ import {
2
+ AiDiagramDialog
3
+ } from "./chunk-OINKNX26.js";
4
+ import {
5
+ useConfirmDialog
6
+ } from "./chunk-6B7G33D5.js";
7
+ import {
8
+ useMenuKeyboard
9
+ } from "./chunk-MRUK56JS.js";
10
+ import {
11
+ AI_INSTRUCTION_CHARACTERS,
12
+ AI_REVIEW_DOCUMENT_CHARACTERS,
13
+ AI_REWRITE_SELECTION_CHARACTERS,
14
+ applyAiReviewFinding,
15
+ buildDraftContinuation,
16
+ buildDraftRequest,
17
+ buildReviewRequest,
18
+ findUniqueExcerpt,
19
+ parseAiReviewResponse,
20
+ sanitizeGeneratedMarkdown
21
+ } from "./chunk-5OZBGIIT.js";
22
+ import {
23
+ unavailableSentence,
24
+ useAiStatus
25
+ } from "./chunk-K4I3NJFL.js";
26
+ import {
27
+ Dialog
28
+ } from "./chunk-56KO2LYN.js";
29
+
30
+ // src/Ai/AiEditorAssistant.tsx
31
+ import { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
32
+ import { createPortal } from "react-dom";
33
+ import {
34
+ useEditorContext,
35
+ useEditorContextMenuItems
36
+ } from "@bendyline/squisq-editor-react";
37
+
38
+ // src/Ai/AiGenerationStatus.tsx
39
+ import { useEffect, useState } from "react";
40
+ import { jsx, jsxs } from "react/jsx-runtime";
41
+ var LABELS = {
42
+ starting: "Waiting for model\u2026",
43
+ queued: "Waiting for an available model\u2026",
44
+ loading_model: "Loading model\u2026",
45
+ prefill: "Processing input (prefill)\u2026",
46
+ reasoning: "Thinking\u2026",
47
+ generating: "Writing\u2026"
48
+ };
49
+ function duration(milliseconds) {
50
+ const seconds = Math.max(0, Math.floor(milliseconds / 1e3));
51
+ return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, "0")}s`;
52
+ }
53
+ function AiGenerationStatus({
54
+ state,
55
+ characters
56
+ }) {
57
+ const [now, setNow] = useState(Date.now);
58
+ useEffect(() => {
59
+ const timer = setInterval(() => setNow(Date.now()), 1e3);
60
+ return () => clearInterval(timer);
61
+ }, []);
62
+ const progress = state.progress;
63
+ const label = progress ? LABELS[progress.phase] : "Waiting for response\u2026";
64
+ const percent = progress?.phase === "prefill" ? progress.percent : null;
65
+ const details = [`${duration(now - state.startedAt)} elapsed`];
66
+ if (progress?.outputTokens != null) {
67
+ details.push(`${progress.outputTokens.toLocaleString()} tokens generated`);
68
+ } else if (characters > 0) {
69
+ details.push(`${characters.toLocaleString()} characters received`);
70
+ }
71
+ if (progress?.tokensPerSecond != null) {
72
+ details.push(`${progress.tokensPerSecond.toFixed(1)} tokens/s`);
73
+ }
74
+ if (now - state.lastActivityAt >= 15e3) {
75
+ details.push(
76
+ progress || characters > 0 ? `Last update ${duration(now - state.lastActivityAt)} ago` : "No progress updates received"
77
+ );
78
+ }
79
+ return /* @__PURE__ */ jsxs("div", { className: "db-ai-generation-status", children: [
80
+ /* @__PURE__ */ jsxs("p", { className: "db-ai-progress", role: "status", children: [
81
+ /* @__PURE__ */ jsx("span", { className: "db-ai-spinner", "aria-hidden": "true" }),
82
+ label,
83
+ percent != null ? ` ${Math.floor(percent)}%` : ""
84
+ ] }),
85
+ percent != null && /* @__PURE__ */ jsx("progress", { "aria-label": label.replace("\u2026", ""), max: 100, value: percent }),
86
+ /* @__PURE__ */ jsx("p", { className: "db-ai-note", "aria-live": "off", children: details.join(" \xB7 ") })
87
+ ] });
88
+ }
89
+
90
+ // src/Ai/draft-capacity.ts
91
+ import { AI_WIRE_LIMITS } from "@bendyline/docblocks/host";
92
+ function estimateTokens(text) {
93
+ return Math.ceil(new TextEncoder().encode(text).length / 3);
94
+ }
95
+ function draftCapacityNotice(request, model, expectedOutput = "") {
96
+ const characters = request.messages.reduce((sum, message) => sum + message.content.length, 0);
97
+ if (characters > AI_WIRE_LIMITS.promptCharacters) {
98
+ return {
99
+ blocked: true,
100
+ message: "The source and draft together exceed the request size this app can send. Your draft is preserved. Work on a smaller section, or copy the draft before closing."
101
+ };
102
+ }
103
+ const capacity = model?.contextWindow;
104
+ if (!capacity || capacity <= 0) return null;
105
+ const input = request.messages.reduce(
106
+ (sum, message) => sum + estimateTokens(message.content) + 8,
107
+ 0
108
+ );
109
+ const reserve = Math.min(4096, Math.floor(capacity / 4));
110
+ if (input + estimateTokens(expectedOutput) + reserve <= capacity) return null;
111
+ return {
112
+ blocked: false,
113
+ message: `This request may exceed ${model.label}'s reported ${capacity.toLocaleString()}-token context. Input, thinking, and the draft share that space. This is a size estimate, not an exact token count; local runtime limits may be smaller. For a complete result, use a smaller selection or choose a model with more context in Settings.`
114
+ };
115
+ }
116
+
117
+ // src/Ai/AiEditorAssistant.tsx
118
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
119
+ function AiToolbarControl({
120
+ ai,
121
+ readOnly = false,
122
+ openPanel,
123
+ onOpenPanel
124
+ }) {
125
+ const {
126
+ activeView,
127
+ editorMode,
128
+ getSelection,
129
+ insertBlockAfterCursor,
130
+ layoutMode,
131
+ markdownSource,
132
+ replaceSelection,
133
+ selectionVersion
134
+ } = useEditorContext();
135
+ const status = useAiStatus(ai);
136
+ const [menuOpen, setMenuOpen] = useState2(false);
137
+ const [draftMode, setDraftMode] = useState2(null);
138
+ const [capturedSelection, setCapturedSelection] = useState2(null);
139
+ const [diagramSelection, setDiagramSelection] = useState2(null);
140
+ const rootRef = useRef(null);
141
+ const { menuRef, triggerRef, handleMenuKeyDown, handleTriggerKeyDown, closeMenu } = useMenuKeyboard(menuOpen, setMenuOpen);
142
+ void activeView;
143
+ void selectionVersion;
144
+ const selection = getSelection();
145
+ useEffect2(() => {
146
+ if (!menuOpen) return;
147
+ const onPointerDown = (event) => {
148
+ if (rootRef.current && !rootRef.current.contains(event.target)) setMenuOpen(false);
149
+ };
150
+ document.addEventListener("pointerdown", onPointerDown);
151
+ return () => document.removeEventListener("pointerdown", onPointerDown);
152
+ }, [menuOpen]);
153
+ const ready = status?.kind === "ready" && status.model !== null;
154
+ const openDiagram = () => {
155
+ const current = getSelection();
156
+ if (!current) return;
157
+ setDiagramSelection(current);
158
+ closeMenu(false);
159
+ };
160
+ const contextMenuItems = editorMode === "markdown" && ready && !readOnly ? [
161
+ {
162
+ id: "docblocks-ai-diagram",
163
+ label: "Diagram this\u2026",
164
+ group: "ai",
165
+ when: "selection",
166
+ disabled: (context) => !context.editable,
167
+ onSelect: openDiagram
168
+ }
169
+ ] : [];
170
+ useEditorContextMenuItems(contextMenuItems);
171
+ if (editorMode !== "markdown" || status?.kind === "unavailable" && status.reason === "opt-out") {
172
+ return null;
173
+ }
174
+ const editableSelection = !readOnly && ready ? selection : null;
175
+ const canCompose = editableSelection?.empty === true;
176
+ const canRewrite = editableSelection?.empty === false && (editableSelection.markdown ?? editableSelection.text).length <= AI_REWRITE_SELECTION_CHARACTERS;
177
+ const openDraft = (mode) => {
178
+ const current = getSelection();
179
+ if (!current) return;
180
+ setCapturedSelection(current);
181
+ setDraftMode(mode);
182
+ closeMenu(false);
183
+ };
184
+ const closeDraft = () => {
185
+ setDraftMode(null);
186
+ setCapturedSelection(null);
187
+ };
188
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
189
+ /* @__PURE__ */ jsxs2("div", { className: "db-toolbar-menu db-ai-toolbar", ref: rootRef, children: [
190
+ /* @__PURE__ */ jsx2(
191
+ "button",
192
+ {
193
+ ref: triggerRef,
194
+ type: "button",
195
+ className: `squisq-toolbar-button db-ai-toolbar-trigger${openPanel ? " squisq-toolbar-button--active" : ""}`,
196
+ onClick: () => setMenuOpen((open) => !open),
197
+ onKeyDown: handleTriggerKeyDown,
198
+ "aria-label": "AI actions",
199
+ "aria-haspopup": "menu",
200
+ "aria-expanded": menuOpen,
201
+ "data-tooltip": "AI actions",
202
+ children: /* @__PURE__ */ jsx2(SparklesGlyph, {})
203
+ }
204
+ ),
205
+ menuOpen && /* @__PURE__ */ jsxs2(
206
+ "div",
207
+ {
208
+ ref: menuRef,
209
+ className: "db-toolbar-menu-dropdown db-ai-toolbar-menu",
210
+ role: "menu",
211
+ "aria-label": "AI actions",
212
+ onKeyDown: handleMenuKeyDown,
213
+ children: [
214
+ !ready && /* @__PURE__ */ jsx2("div", { className: "db-ai-toolbar-status", role: "status", children: unavailableSentence(status) }),
215
+ /* @__PURE__ */ jsx2(
216
+ "button",
217
+ {
218
+ type: "button",
219
+ role: "menuitem",
220
+ tabIndex: -1,
221
+ className: "db-toolbar-menu-item",
222
+ disabled: !canCompose,
223
+ onClick: () => openDraft("compose"),
224
+ children: "Add content\u2026"
225
+ }
226
+ ),
227
+ /* @__PURE__ */ jsx2(
228
+ "button",
229
+ {
230
+ type: "button",
231
+ role: "menuitem",
232
+ tabIndex: -1,
233
+ className: "db-toolbar-menu-item",
234
+ disabled: !canRewrite,
235
+ onClick: () => openDraft("rewrite"),
236
+ children: "Rewrite selection\u2026"
237
+ }
238
+ ),
239
+ /* @__PURE__ */ jsx2(
240
+ "button",
241
+ {
242
+ type: "button",
243
+ role: "menuitem",
244
+ tabIndex: -1,
245
+ className: "db-toolbar-menu-item",
246
+ disabled: !ready || readOnly,
247
+ onClick: () => {
248
+ closeMenu(false);
249
+ onOpenPanel("review");
250
+ },
251
+ children: openPanel === "review" ? "Show document review" : "Review document\u2026"
252
+ }
253
+ ),
254
+ /* @__PURE__ */ jsx2(
255
+ "button",
256
+ {
257
+ type: "button",
258
+ role: "menuitem",
259
+ tabIndex: -1,
260
+ className: "db-toolbar-menu-item",
261
+ disabled: !ready || readOnly || layoutMode !== "document",
262
+ onClick: () => {
263
+ closeMenu(false);
264
+ onOpenPanel("illustrate");
265
+ },
266
+ children: openPanel === "illustrate" ? "Show illustrations" : "Illustrate document\u2026"
267
+ }
268
+ ),
269
+ /* @__PURE__ */ jsx2(
270
+ "button",
271
+ {
272
+ type: "button",
273
+ role: "menuitem",
274
+ tabIndex: -1,
275
+ className: "db-toolbar-menu-item",
276
+ disabled: !editableSelection,
277
+ onClick: openDiagram,
278
+ children: "Insert diagram\u2026"
279
+ }
280
+ ),
281
+ ready && !selection && /* @__PURE__ */ jsx2("div", { className: "db-ai-toolbar-status", children: "Switch to Write or Source to edit with AI." }),
282
+ ready && selection && !selection.empty && !canRewrite && /* @__PURE__ */ jsx2("div", { className: "db-ai-toolbar-status", children: "The selection is too long to rewrite." }),
283
+ ready && layoutMode !== "document" && /* @__PURE__ */ jsx2("div", { className: "db-ai-toolbar-status", children: "Switch to the Document layout to illustrate." })
284
+ ]
285
+ }
286
+ )
287
+ ] }),
288
+ draftMode && capturedSelection && /* @__PURE__ */ jsx2(
289
+ AiDraftDialog,
290
+ {
291
+ ai,
292
+ model: status?.kind === "ready" ? status.model : null,
293
+ mode: draftMode,
294
+ documentSource: markdownSource,
295
+ capturedSelection,
296
+ getSelection,
297
+ replaceSelection,
298
+ onClose: closeDraft
299
+ }
300
+ ),
301
+ diagramSelection && /* @__PURE__ */ jsx2(
302
+ AiDiagramDialog,
303
+ {
304
+ ai,
305
+ documentSource: markdownSource,
306
+ capturedSelection: diagramSelection,
307
+ getSelection,
308
+ insertBlockAfterCursor,
309
+ onClose: () => setDiagramSelection(null)
310
+ }
311
+ )
312
+ ] });
313
+ }
314
+ function AiDraftDialog({
315
+ ai,
316
+ model,
317
+ mode,
318
+ documentSource,
319
+ capturedSelection,
320
+ getSelection,
321
+ replaceSelection,
322
+ onClose
323
+ }) {
324
+ const defaultInstructions = mode === "compose" ? "Add a concise introduction for this document." : "Improve clarity and flow while preserving the meaning and voice.";
325
+ const [instructions, setInstructions] = useState2("");
326
+ const [draft, setDraft] = useState2(null);
327
+ const generatedDraftRef = useRef("");
328
+ const carriedEditsRef = useRef(false);
329
+ const [originalRequest, setOriginalRequest] = useState2(null);
330
+ const [incomplete, setIncomplete] = useState2(false);
331
+ const [prefixCharacters, setPrefixCharacters] = useState2(0);
332
+ const [running, setRunning] = useState2(false);
333
+ const [generation, setGeneration] = useState2(null);
334
+ const [error, setError] = useState2(null);
335
+ const [finishNote, setFinishNote] = useState2(null);
336
+ const promptRef = useRef(null);
337
+ const handleRef = useRef(null);
338
+ const requestRef = useRef(0);
339
+ const { confirm: confirmAction, confirmDialog } = useConfirmDialog();
340
+ const selectedMarkdown = capturedSelection.markdown ?? capturedSelection.text;
341
+ const draftRequest = useMemo(
342
+ () => ({
343
+ ...buildDraftRequest({
344
+ mode,
345
+ instructions: instructions.trim() || defaultInstructions,
346
+ documentSource,
347
+ selectedText: selectedMarkdown
348
+ }),
349
+ ...model ? { model: model.id } : {}
350
+ }),
351
+ [mode, instructions, defaultInstructions, documentSource, selectedMarkdown, model]
352
+ );
353
+ const continuationRequest = originalRequest && draft ? buildDraftContinuation({ ...originalRequest, ...model ? { model: model.id } : {} }, draft) : null;
354
+ const capacity = draftCapacityNotice(
355
+ incomplete && continuationRequest ? continuationRequest : draftRequest,
356
+ model,
357
+ incomplete || mode === "compose" ? "" : selectedMarkdown
358
+ );
359
+ useEffect2(
360
+ () => () => {
361
+ requestRef.current += 1;
362
+ handleRef.current?.cancel();
363
+ },
364
+ []
365
+ );
366
+ const start = useCallback(
367
+ (request, prefix = "") => {
368
+ const requestId = ++requestRef.current;
369
+ handleRef.current?.cancel();
370
+ generatedDraftRef.current = prefix;
371
+ setDraft(prefix);
372
+ setPrefixCharacters(prefix.length);
373
+ setError(null);
374
+ setFinishNote(null);
375
+ setIncomplete(false);
376
+ setRunning(true);
377
+ const startedAt = Date.now();
378
+ setGeneration({ progress: null, startedAt, lastActivityAt: startedAt });
379
+ const handle = ai.chat(request, (event) => {
380
+ if (requestRef.current !== requestId) return;
381
+ if (event.kind === "progress") {
382
+ setGeneration(
383
+ (current) => current && {
384
+ ...current,
385
+ progress: event.progress,
386
+ lastActivityAt: Date.now()
387
+ }
388
+ );
389
+ return;
390
+ }
391
+ if (event.kind === "delta") {
392
+ generatedDraftRef.current += event.text;
393
+ setDraft(generatedDraftRef.current);
394
+ setGeneration(
395
+ (current) => current && {
396
+ ...current,
397
+ lastActivityAt: Date.now(),
398
+ progress: {
399
+ phase: "generating",
400
+ percent: null,
401
+ outputTokens: current.progress?.outputTokens ?? null,
402
+ tokensPerSecond: current.progress?.tokensPerSecond ?? null
403
+ }
404
+ }
405
+ );
406
+ return;
407
+ }
408
+ setRunning(false);
409
+ if (event.kind === "error") {
410
+ setError(event.error.message);
411
+ setIncomplete(Boolean(generatedDraftRef.current.trim()));
412
+ return;
413
+ }
414
+ const emptyContinuation = Boolean(prefix) && !event.completion.text.trim();
415
+ const finished = event.completion.finishReason === "stop" && !emptyContinuation;
416
+ generatedDraftRef.current = prefix || !finished ? prefix + event.completion.text : sanitizeGeneratedMarkdown(event.completion.text);
417
+ setDraft(generatedDraftRef.current);
418
+ setIncomplete(!finished && Boolean(generatedDraftRef.current.trim()));
419
+ if (event.completion.finishReason === "cancelled") {
420
+ setFinishNote("Generation stopped. The draft may be incomplete.");
421
+ }
422
+ if (event.completion.finishReason === "length") {
423
+ setFinishNote(
424
+ generatedDraftRef.current.trim() ? "Draft incomplete: the model reached a limit. Continue the draft to request the rest. If this repeats, use a smaller selection or a model with more context. Your draft and original document are preserved." : "The model reached a limit before producing a draft. Try a smaller selection or a model with more context. Your original document is unchanged."
425
+ );
426
+ }
427
+ if (emptyContinuation && event.completion.finishReason === "stop") {
428
+ setFinishNote(
429
+ "The model returned no additional text. Your draft is preserved and may still be incomplete. Try a smaller selection or a model with more context."
430
+ );
431
+ }
432
+ });
433
+ handleRef.current = handle;
434
+ },
435
+ [ai]
436
+ );
437
+ const generate = useCallback(async () => {
438
+ if (running || draftCapacityNotice(draftRequest, model)?.blocked) return;
439
+ const previousRequest = requestRef.current;
440
+ if (draft !== null && (carriedEditsRef.current || draft !== generatedDraftRef.current)) {
441
+ const accepted = await confirmAction({
442
+ title: "Regenerate AI draft",
443
+ message: "This will remove all edits you have made. Continue?",
444
+ confirmLabel: "Continue",
445
+ destructive: true
446
+ });
447
+ if (!accepted || requestRef.current !== previousRequest) return;
448
+ }
449
+ carriedEditsRef.current = false;
450
+ setOriginalRequest(draftRequest);
451
+ start(draftRequest);
452
+ }, [running, draftRequest, model, draft, confirmAction, start]);
453
+ const continueDraft = () => {
454
+ if (running || !incomplete || !draft || !continuationRequest || capacity?.blocked) return;
455
+ carriedEditsRef.current || (carriedEditsRef.current = draft !== generatedDraftRef.current);
456
+ start(continuationRequest, draft);
457
+ };
458
+ const stop = useCallback(() => handleRef.current?.cancel(), []);
459
+ const apply = useCallback(async () => {
460
+ if (running) return;
461
+ const replacement = sanitizeGeneratedMarkdown(draft ?? "");
462
+ if (!replacement) return;
463
+ if (incomplete) {
464
+ const previousRequest = requestRef.current;
465
+ const accepted = await confirmAction({
466
+ title: "Use incomplete AI draft?",
467
+ message: mode === "rewrite" ? "This draft did not finish. Replacing the selection may remove content that has not been rewritten. Use it only if you have reviewed and completed it yourself." : "This draft did not finish. Use it only if you have reviewed and completed it yourself.",
468
+ confirmLabel: mode === "rewrite" ? "Replace anyway" : "Insert anyway",
469
+ destructive: mode === "rewrite"
470
+ });
471
+ if (!accepted || requestRef.current !== previousRequest) return;
472
+ }
473
+ const current = getSelection();
474
+ if (!current || current.view !== capturedSelection.view) {
475
+ setError("Return to the original editor view and try again.");
476
+ return;
477
+ }
478
+ if (mode === "compose" && !current.empty) {
479
+ setError("The cursor selection changed. Close this dialog and choose Add content again.");
480
+ return;
481
+ }
482
+ if (mode === "rewrite" && (current.empty || current.text !== capturedSelection.text || (current.markdown ?? current.text) !== selectedMarkdown)) {
483
+ setError("The selected text changed. Close this dialog and select it again.");
484
+ return;
485
+ }
486
+ if (!replaceSelection(replacement)) {
487
+ setError("DocBlocks could not apply the draft in the current editor view.");
488
+ return;
489
+ }
490
+ onClose();
491
+ }, [
492
+ capturedSelection,
493
+ selectedMarkdown,
494
+ draft,
495
+ getSelection,
496
+ mode,
497
+ onClose,
498
+ replaceSelection,
499
+ running,
500
+ incomplete,
501
+ confirmAction
502
+ ]);
503
+ const dialog = /* @__PURE__ */ jsxs2(
504
+ Dialog,
505
+ {
506
+ title: mode === "compose" ? "Add content with AI" : "Rewrite with AI",
507
+ onClose,
508
+ size: "wide",
509
+ closeOnBackdrop: !running,
510
+ initialFocusRef: promptRef,
511
+ className: "db-ai-draft-dialog",
512
+ bodyClassName: "db-ai-draft-body",
513
+ footerClassName: "db-ai-draft-footer",
514
+ footer: /* @__PURE__ */ jsxs2(Fragment, { children: [
515
+ (running || finishNote || error || capacity) && /* @__PURE__ */ jsxs2("div", { className: "db-ai-footer-status", children: [
516
+ running && generation && /* @__PURE__ */ jsx2(
517
+ AiGenerationStatus,
518
+ {
519
+ state: generation,
520
+ characters: Math.max(0, (draft?.length ?? 0) - prefixCharacters)
521
+ }
522
+ ),
523
+ finishNote && /* @__PURE__ */ jsx2("p", { className: "db-ai-note", children: finishNote }),
524
+ !running && capacity && /* @__PURE__ */ jsx2("p", { className: "db-ai-note", role: "status", children: capacity.message }),
525
+ error && /* @__PURE__ */ jsx2("p", { className: "db-ai-error", role: "alert", children: error })
526
+ ] }),
527
+ /* @__PURE__ */ jsxs2("div", { className: "db-ai-draft-actions", children: [
528
+ /* @__PURE__ */ jsx2("button", { type: "button", onClick: onClose, children: "Cancel" }),
529
+ running ? /* @__PURE__ */ jsx2("button", { type: "button", className: "db-ai-secondary-action", onClick: stop, children: "Stop" }) : draft !== null ? /* @__PURE__ */ jsxs2(Fragment, { children: [
530
+ /* @__PURE__ */ jsx2("button", { type: "button", className: "db-ai-secondary-action", onClick: generate, children: "Regenerate" }),
531
+ incomplete && draft.trim() && /* @__PURE__ */ jsx2("button", { type: "button", disabled: capacity?.blocked, onClick: continueDraft, children: "Continue draft" })
532
+ ] }) : null,
533
+ /* @__PURE__ */ jsx2("button", { type: "button", disabled: !draft?.trim() || running, onClick: apply, children: mode === "compose" ? "Insert" : "Replace selection" })
534
+ ] })
535
+ ] }),
536
+ children: [
537
+ /* @__PURE__ */ jsxs2("label", { className: "db-ai-field", children: [
538
+ /* @__PURE__ */ jsx2("span", { children: mode === "compose" ? "What should be added?" : "How should this be rewritten?" }),
539
+ /* @__PURE__ */ jsx2(
540
+ "textarea",
541
+ {
542
+ ref: promptRef,
543
+ value: instructions,
544
+ maxLength: AI_INSTRUCTION_CHARACTERS,
545
+ rows: 2,
546
+ placeholder: defaultInstructions,
547
+ onChange: (event) => setInstructions(event.currentTarget.value)
548
+ }
549
+ )
550
+ ] }),
551
+ mode === "rewrite" && /* @__PURE__ */ jsxs2("div", { className: "db-ai-selection-preview", children: [
552
+ /* @__PURE__ */ jsx2("span", { children: "Selected text" }),
553
+ /* @__PURE__ */ jsx2("blockquote", { tabIndex: 0, "aria-label": "Selected text", children: capturedSelection.text })
554
+ ] }),
555
+ draft === null ? /* @__PURE__ */ jsx2("div", { className: "db-ai-draft-start", children: /* @__PURE__ */ jsx2("button", { type: "button", onClick: generate, disabled: capacity?.blocked, children: capacity && !capacity.blocked ? "Generate anyway" : "Generate" }) }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
556
+ /* @__PURE__ */ jsxs2("label", { className: "db-ai-field db-ai-draft-field", children: [
557
+ /* @__PURE__ */ jsx2("span", { children: "AI draft" }),
558
+ /* @__PURE__ */ jsx2(
559
+ "textarea",
560
+ {
561
+ className: "db-ai-draft-output",
562
+ value: draft,
563
+ rows: 10,
564
+ placeholder: running ? "The draft will appear here." : "",
565
+ "aria-busy": running,
566
+ readOnly: running,
567
+ onChange: (event) => setDraft(event.currentTarget.value)
568
+ }
569
+ )
570
+ ] }),
571
+ /* @__PURE__ */ jsx2("p", { className: "db-ai-disclaimer", children: "Review and edit AI-generated content before adding it." })
572
+ ] })
573
+ ]
574
+ }
575
+ );
576
+ const dialogs = /* @__PURE__ */ jsxs2(Fragment, { children: [
577
+ dialog,
578
+ confirmDialog
579
+ ] });
580
+ return typeof document === "undefined" ? dialogs : createPortal(dialogs, document.body);
581
+ }
582
+ function AiReviewPanel({ ai, onClose }) {
583
+ const { markdownSource, replaceAll } = useEditorContext();
584
+ const [phase, setPhase] = useState2("idle");
585
+ const [findings, setFindings] = useState2([]);
586
+ const [reviewedSource, setReviewedSource] = useState2(null);
587
+ const [resolvedIds, setResolvedIds] = useState2(() => /* @__PURE__ */ new Set());
588
+ const [error, setError] = useState2(null);
589
+ const [applyErrorId, setApplyErrorId] = useState2(null);
590
+ const handleRef = useRef(null);
591
+ const requestRef = useRef(0);
592
+ const startedRef = useRef(false);
593
+ const cancelReview = useCallback(() => {
594
+ requestRef.current += 1;
595
+ handleRef.current?.cancel();
596
+ handleRef.current = null;
597
+ setPhase("idle");
598
+ }, []);
599
+ const runReview = useCallback(() => {
600
+ if (!markdownSource.trim()) {
601
+ setFindings([]);
602
+ setReviewedSource(markdownSource);
603
+ setResolvedIds(/* @__PURE__ */ new Set());
604
+ setError(null);
605
+ setPhase("ready");
606
+ return;
607
+ }
608
+ if (markdownSource.length > AI_REVIEW_DOCUMENT_CHARACTERS) {
609
+ setError("This document is too long for one AI review pass. Review a shorter document.");
610
+ setPhase("error");
611
+ return;
612
+ }
613
+ handleRef.current?.cancel();
614
+ const requestId = ++requestRef.current;
615
+ const source = markdownSource;
616
+ setPhase("running");
617
+ setFindings([]);
618
+ setResolvedIds(/* @__PURE__ */ new Set());
619
+ setApplyErrorId(null);
620
+ setError(null);
621
+ const handle = ai.chat(buildReviewRequest(source), (event) => {
622
+ if (requestRef.current !== requestId || event.kind === "delta" || event.kind === "progress")
623
+ return;
624
+ handleRef.current = null;
625
+ if (event.kind === "error") {
626
+ setError(event.error.message);
627
+ setPhase("error");
628
+ return;
629
+ }
630
+ const parsed = parseAiReviewResponse(event.completion.text);
631
+ if (!parsed) {
632
+ setError("The AI returned a review DocBlocks could not read. Try reviewing again.");
633
+ setPhase("error");
634
+ return;
635
+ }
636
+ setFindings(parsed);
637
+ setReviewedSource(source);
638
+ setPhase("ready");
639
+ });
640
+ handleRef.current = handle;
641
+ }, [ai, markdownSource]);
642
+ useEffect2(() => {
643
+ if (startedRef.current) return;
644
+ startedRef.current = true;
645
+ runReview();
646
+ }, [runReview]);
647
+ useEffect2(
648
+ () => () => {
649
+ requestRef.current += 1;
650
+ handleRef.current?.cancel();
651
+ handleRef.current = null;
652
+ startedRef.current = false;
653
+ },
654
+ []
655
+ );
656
+ const applyFinding = useCallback(
657
+ (finding) => {
658
+ const next = applyAiReviewFinding(markdownSource, finding);
659
+ if (next === null) {
660
+ setApplyErrorId(finding.id);
661
+ return;
662
+ }
663
+ replaceAll(next);
664
+ setReviewedSource(next);
665
+ setApplyErrorId(null);
666
+ setResolvedIds((current) => /* @__PURE__ */ new Set([...current, finding.id]));
667
+ },
668
+ [markdownSource, replaceAll]
669
+ );
670
+ const dismissFinding = useCallback((id) => {
671
+ setApplyErrorId((current) => current === id ? null : current);
672
+ setResolvedIds((current) => /* @__PURE__ */ new Set([...current, id]));
673
+ }, []);
674
+ const visibleFindings = findings.filter((finding) => !resolvedIds.has(finding.id));
675
+ const documentChanged = reviewedSource !== null && markdownSource !== reviewedSource;
676
+ return /* @__PURE__ */ jsxs2("aside", { className: "db-ai-review-panel", "aria-label": "AI document review", children: [
677
+ /* @__PURE__ */ jsxs2("div", { className: "db-ai-review-header", children: [
678
+ /* @__PURE__ */ jsxs2("div", { children: [
679
+ /* @__PURE__ */ jsx2("h2", { children: "AI review" }),
680
+ phase === "ready" && /* @__PURE__ */ jsxs2("span", { className: "db-ai-review-count", children: [
681
+ visibleFindings.length,
682
+ " ",
683
+ visibleFindings.length === 1 ? "finding" : "findings"
684
+ ] })
685
+ ] }),
686
+ /* @__PURE__ */ jsx2(
687
+ "button",
688
+ {
689
+ type: "button",
690
+ className: "db-ai-review-close",
691
+ onClick: onClose,
692
+ "aria-label": "Close AI review",
693
+ children: "\xD7"
694
+ }
695
+ )
696
+ ] }),
697
+ /* @__PURE__ */ jsx2("div", { className: "db-ai-review-actions", children: phase === "running" ? /* @__PURE__ */ jsx2("button", { type: "button", onClick: cancelReview, children: "Stop review" }) : /* @__PURE__ */ jsx2("button", { type: "button", onClick: runReview, children: phase === "idle" ? "Review document" : "Review again" }) }),
698
+ phase === "running" && /* @__PURE__ */ jsxs2("div", { className: "db-ai-review-empty", role: "status", children: [
699
+ /* @__PURE__ */ jsx2("span", { className: "db-ai-spinner", "aria-hidden": "true" }),
700
+ " Reviewing this document\u2026"
701
+ ] }),
702
+ phase === "error" && error && /* @__PURE__ */ jsx2("div", { className: "db-ai-review-empty db-ai-error", role: "alert", children: error }),
703
+ documentChanged && phase === "ready" && /* @__PURE__ */ jsx2("div", { className: "db-ai-review-stale", role: "status", children: "The document changed after this review. Suggestions are applied only when their original text still matches exactly." }),
704
+ phase === "ready" && visibleFindings.length === 0 && /* @__PURE__ */ jsx2("div", { className: "db-ai-review-empty", children: findings.length === 0 ? "Nothing to report." : "All findings have been handled." }),
705
+ /* @__PURE__ */ jsx2("ol", { className: "db-ai-review-list", children: visibleFindings.map((finding) => {
706
+ const canApply = finding.replacement !== null && findUniqueExcerpt(markdownSource, finding.originalText) !== null;
707
+ return /* @__PURE__ */ jsxs2("li", { className: "db-ai-review-finding", children: [
708
+ /* @__PURE__ */ jsxs2("div", { className: "db-ai-review-finding-meta", children: [
709
+ /* @__PURE__ */ jsx2(
710
+ "span",
711
+ {
712
+ className: `db-ai-review-severity db-ai-review-severity--${finding.severity}`,
713
+ children: finding.severity
714
+ }
715
+ ),
716
+ /* @__PURE__ */ jsx2("span", { children: finding.category })
717
+ ] }),
718
+ /* @__PURE__ */ jsx2("h3", { children: finding.message }),
719
+ finding.rationale && /* @__PURE__ */ jsx2("p", { children: finding.rationale }),
720
+ /* @__PURE__ */ jsxs2("div", { className: "db-ai-review-excerpt", children: [
721
+ /* @__PURE__ */ jsx2("span", { children: "Current text" }),
722
+ /* @__PURE__ */ jsx2("blockquote", { children: finding.originalText })
723
+ ] }),
724
+ finding.replacement !== null && /* @__PURE__ */ jsxs2("div", { className: "db-ai-review-excerpt db-ai-review-excerpt--replacement", children: [
725
+ /* @__PURE__ */ jsx2("span", { children: "Suggested" }),
726
+ /* @__PURE__ */ jsx2("blockquote", { children: finding.replacement })
727
+ ] }),
728
+ applyErrorId === finding.id && /* @__PURE__ */ jsx2("p", { className: "db-ai-error", role: "alert", children: "This text no longer has one exact match. Review the document again before applying it." }),
729
+ /* @__PURE__ */ jsxs2("div", { className: "db-ai-review-finding-actions", children: [
730
+ /* @__PURE__ */ jsx2("button", { type: "button", onClick: () => dismissFinding(finding.id), children: "Dismiss" }),
731
+ finding.replacement !== null && /* @__PURE__ */ jsx2(
732
+ "button",
733
+ {
734
+ type: "button",
735
+ disabled: !canApply,
736
+ title: canApply ? void 0 : "The reviewed text no longer has one exact match",
737
+ onClick: () => applyFinding(finding),
738
+ children: "Apply"
739
+ }
740
+ )
741
+ ] })
742
+ ] }, finding.id);
743
+ }) }),
744
+ /* @__PURE__ */ jsx2("p", { className: "db-ai-review-disclaimer", children: "AI suggestions can be wrong. Review each change." })
745
+ ] });
746
+ }
747
+ function SparklesGlyph() {
748
+ return /* @__PURE__ */ jsxs2(
749
+ "svg",
750
+ {
751
+ viewBox: "0 0 18 18",
752
+ width: "18",
753
+ height: "18",
754
+ fill: "none",
755
+ stroke: "currentColor",
756
+ strokeWidth: "1.35",
757
+ strokeLinecap: "round",
758
+ strokeLinejoin: "round",
759
+ "aria-hidden": "true",
760
+ children: [
761
+ /* @__PURE__ */ jsx2("path", { d: "M7.2 2.2c.45 2.2 1.75 3.5 3.95 3.95-2.2.45-3.5 1.75-3.95 3.95-.45-2.2-1.75-3.5-3.95-3.95C5.45 5.7 6.75 4.4 7.2 2.2Z" }),
762
+ /* @__PURE__ */ jsx2("path", { d: "M13.1 9.2c.3 1.45 1.15 2.3 2.6 2.6-1.45.3-2.3 1.15-2.6 2.6-.3-1.45-1.15-2.3-2.6-2.6 1.45-.3 2.3-1.15 2.6-2.6Z" })
763
+ ]
764
+ }
765
+ );
766
+ }
767
+ export {
768
+ AiDraftDialog,
769
+ AiReviewPanel,
770
+ AiToolbarControl
771
+ };