@bendyline/docblocks-react 2.7.2 → 2.7.3

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.
@@ -4,23 +4,52 @@ import {
4
4
 
5
5
  // src/Export/run-export.ts
6
6
  import {
7
+ isSvgImageSource,
7
8
  parseFrontmatter,
8
9
  parseMarkdown,
9
10
  readFrontmatterThemeId,
10
- splitFrontmatterBlock
11
+ splitFrontmatterBlock,
12
+ svgSourceFromBytes
11
13
  } from "@bendyline/squisq/markdown";
14
+ async function diagramRendererFor(overrides) {
15
+ if (overrides.diagramRenderer) return overrides.diagramRenderer;
16
+ const module = await import("@bendyline/squisq-react/diagram-pictures");
17
+ return module.createDiagramPictureRenderer();
18
+ }
12
19
  async function pictureDiagrams(doc, overrides, kinds) {
13
20
  try {
14
- const [{ rasterizeDiagrams }, render] = await Promise.all([
21
+ const [{ rasterizeDiagrams, EXPORT_DIAGRAM_KINDS }, render] = await Promise.all([
15
22
  import("@bendyline/squisq-formats/diagrams"),
16
- overrides.diagramRenderer ?? import("@bendyline/squisq-react/diagram-pictures").then(
17
- (module) => module.createDiagramPictureRenderer()
18
- )
23
+ diagramRendererFor(overrides)
19
24
  ]);
20
- return await rasterizeDiagrams(doc, render, { kinds, urlPrefix: "docblocks-diagram" });
25
+ return await rasterizeDiagrams(doc, render, {
26
+ kinds: kinds === "all" ? EXPORT_DIAGRAM_KINDS : kinds,
27
+ urlPrefix: "docblocks-diagram"
28
+ });
29
+ } catch {
30
+ return { markdownDoc: doc, images: /* @__PURE__ */ new Map(), mermaid: /* @__PURE__ */ new Map(), svg: /* @__PURE__ */ new Map() };
31
+ }
32
+ }
33
+ async function pictureSvgAttachments(attachments, overrides) {
34
+ const pictures = /* @__PURE__ */ new Map();
35
+ const sources = /* @__PURE__ */ new Set();
36
+ for (const [url, data] of attachments) {
37
+ if (isSvgImageSource(url)) sources.add(svgSourceFromBytes(data).trim());
38
+ }
39
+ if (sources.size === 0) return pictures;
40
+ try {
41
+ const render = await diagramRendererFor(overrides);
42
+ for (const source of sources) {
43
+ const diagram = { kind: "svg", markdown: "", source, template: "", alt: "" };
44
+ const picture = await render(diagram).catch(() => null);
45
+ if (picture && picture.width > 0 && picture.height > 0) pictures.set(source, picture);
46
+ }
21
47
  } catch {
22
- return { markdownDoc: doc, images: /* @__PURE__ */ new Map(), mermaid: /* @__PURE__ */ new Map() };
23
48
  }
49
+ return pictures;
50
+ }
51
+ function imageBytes(images) {
52
+ return [...images].map(([url, image]) => [url, image.data]);
24
53
  }
25
54
  var MIME_TYPES = {
26
55
  docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
@@ -58,6 +87,19 @@ function buildFilename(selectedFile, format) {
58
87
  return base + FORMAT_EXTENSIONS[format];
59
88
  }
60
89
  async function runExport(markdown, selectedFile, options, mediaContainer, saveBlob, converterOverrides = {}) {
90
+ const warnings = [];
91
+ await exportDocument(
92
+ markdown,
93
+ selectedFile,
94
+ options,
95
+ mediaContainer,
96
+ saveBlob,
97
+ converterOverrides,
98
+ warnings
99
+ );
100
+ return { warnings };
101
+ }
102
+ async function exportDocument(markdown, selectedFile, options, mediaContainer, saveBlob, converterOverrides, warnings) {
61
103
  const filename = buildFilename(selectedFile, options.format);
62
104
  const themeId = exporterThemeId(markdown, options.themeId);
63
105
  if (options.format === "md") {
@@ -72,25 +114,30 @@ async function runExport(markdown, selectedFile, options, mediaContainer, saveBl
72
114
  if (options.format === "docx") {
73
115
  const { markdownDocToDocx } = await import("@bendyline/squisq-formats/docx");
74
116
  const pictured = await pictureDiagrams(doc, converterOverrides);
75
- const images = new Map([
76
- ...mediaContainer ? await resolveImages(doc, mediaContainer) : [],
77
- ...pictured.images
78
- ]);
79
- const buf = await markdownDocToDocx(pictured.markdownDoc, { themeId, images });
117
+ const { svg } = await pictureDiagrams(doc, converterOverrides, ["svg"]);
118
+ const attachments = await resolveImages(doc, mediaContainer);
119
+ const svgAttachments = await pictureSvgAttachments(imageBytes(attachments), converterOverrides);
120
+ const images = new Map([...attachments, ...pictured.images]);
121
+ const buf = await markdownDocToDocx(pictured.markdownDoc, {
122
+ themeId,
123
+ images,
124
+ svgIslandFallbacks: new Map([...svg, ...svgAttachments])
125
+ });
80
126
  await saveExportBlob(new Blob([buf], { type: MIME_TYPES.docx }), filename, saveBlob);
81
127
  return;
82
128
  }
83
129
  if (options.format === "pdf") {
84
130
  const { markdownDocToPdf } = await import("@bendyline/squisq-formats/pdf");
85
- const pictured = await pictureDiagrams(doc, converterOverrides);
86
- const images = new Map([
87
- ...mediaContainer ? await resolveImages(doc, mediaContainer) : [],
88
- ...pictured.images
89
- ]);
131
+ const pictured = await pictureDiagrams(doc, converterOverrides, "all");
132
+ const attachments = await resolveImages(doc, mediaContainer);
133
+ const svgPictures = await pictureSvgAttachments(imageBytes(attachments), converterOverrides);
134
+ const images = new Map([...withSvgPictures(attachments, svgPictures), ...pictured.images]);
90
135
  const buf = await markdownDocToPdf(pictured.markdownDoc, {
91
136
  themeId,
92
137
  pageSize: options.pageSize,
93
- images
138
+ images,
139
+ // Replaced characters are reported to the user, not just the console.
140
+ onWarning: (message) => warnings.push(message)
94
141
  });
95
142
  await saveExportBlob(new Blob([buf], { type: MIME_TYPES.pdf }), filename, saveBlob);
96
143
  return;
@@ -123,8 +170,13 @@ async function runExport(markdown, selectedFile, options, mediaContainer, saveBl
123
170
  enrichedDoc.themeId = themeId;
124
171
  }
125
172
  const images = mediaContainer ? await resolveImageData(doc, mediaContainer) : void 0;
126
- const { mermaid } = await pictureDiagrams(doc, converterOverrides, ["mermaid"]);
127
- const buf = await docToPptx(enrichedDoc, { themeId, images, diagramImages: mermaid });
173
+ const { mermaid, svg } = await pictureDiagrams(doc, converterOverrides, ["mermaid", "svg"]);
174
+ const svgAttachments = await pictureSvgAttachments(images ?? [], converterOverrides);
175
+ const buf = await docToPptx(enrichedDoc, {
176
+ themeId,
177
+ images,
178
+ diagramImages: new Map([...mermaid, ...svg, ...svgAttachments])
179
+ });
128
180
  await saveExportBlob(new Blob([buf], { type: MIME_TYPES.pptx }), filename, saveBlob);
129
181
  return;
130
182
  }
@@ -288,8 +340,9 @@ function collectMarkdownImageUrls(doc) {
288
340
  return Array.from(new Set(urls));
289
341
  }
290
342
  async function resolveImages(doc, container) {
291
- const urls = collectMarkdownImageUrls(doc);
292
343
  const map = /* @__PURE__ */ new Map();
344
+ if (!container) return map;
345
+ const urls = collectMarkdownImageUrls(doc);
293
346
  for (const url of urls) {
294
347
  if (map.has(url)) continue;
295
348
  if (isExternalUrl(url)) continue;
@@ -301,6 +354,22 @@ async function resolveImages(doc, container) {
301
354
  }
302
355
  return map;
303
356
  }
357
+ function withSvgPictures(images, pictures) {
358
+ const result = /* @__PURE__ */ new Map();
359
+ for (const [url, image] of images) {
360
+ const picture = isSvgImageSource(url) ? pictures.get(svgSourceFromBytes(image.data).trim()) : void 0;
361
+ result.set(
362
+ url,
363
+ picture ? {
364
+ data: picture.data,
365
+ contentType: "image/png",
366
+ width: picture.width,
367
+ height: picture.height
368
+ } : image
369
+ );
370
+ }
371
+ return result;
372
+ }
304
373
  async function resolveImageData(doc, container) {
305
374
  const urls = collectMarkdownImageUrls(doc);
306
375
  const map = /* @__PURE__ */ new Map();
@@ -39,19 +39,25 @@ var SHELL_SHORTCUTS = Object.freeze([
39
39
  whileTyping: true,
40
40
  action: "Show or hide the file pane"
41
41
  },
42
+ // Leaving a text field for the files is the point of this key — the search
43
+ // box included, which Ctrl+Shift+F has just focused. It only moves focus.
42
44
  {
43
45
  id: "focus-files",
44
46
  key: "e",
45
47
  accel: true,
46
48
  shift: true,
47
49
  label: "Ctrl+Shift+E",
50
+ whileTyping: true,
48
51
  action: "Move focus to the files"
49
52
  },
53
+ // Starting another document is most often done from inside one. A dialog's
54
+ // own name box keeps these keys, so a second prompt can't stack.
50
55
  {
51
56
  id: "new-document",
52
57
  key: "n",
53
58
  accel: true,
54
59
  label: "Ctrl+N",
60
+ whileEditing: true,
55
61
  action: "New document",
56
62
  browserReserved: true
57
63
  },
@@ -61,6 +67,7 @@ var SHELL_SHORTCUTS = Object.freeze([
61
67
  accel: true,
62
68
  shift: true,
63
69
  label: "Ctrl+Shift+N",
70
+ whileEditing: true,
64
71
  action: "New folder",
65
72
  browserReserved: true
66
73
  },
@@ -82,10 +89,12 @@ var SHELL_SHORTCUTS = Object.freeze([
82
89
  ]);
83
90
  function isTypingTarget(target) {
84
91
  if (!(target instanceof HTMLElement)) return false;
85
- if (target.isContentEditable) return true;
92
+ if (isDocumentEditorTarget(target)) return true;
86
93
  const tag = target.tagName;
87
- if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
88
- return target.closest(".monaco-editor") !== null;
94
+ return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
95
+ }
96
+ function isDocumentEditorTarget(target) {
97
+ return target.isContentEditable || target.closest(".monaco-editor") !== null;
89
98
  }
90
99
  function matchShortcut(event, _layoutMode) {
91
100
  return SHELL_SHORTCUTS.find((binding) => bindingMatches(binding, event))?.id ?? null;
@@ -97,7 +106,8 @@ function bindingMatches(binding, event) {
97
106
  if (binding.accel !== accel) return false;
98
107
  if ((binding.shift ?? false) !== event.shiftKey) return false;
99
108
  if (binding.key !== key) return false;
100
- return binding.whileTyping === true || !isTypingTarget(event.target);
109
+ if (binding.whileTyping === true || !isTypingTarget(event.target)) return true;
110
+ return binding.whileEditing === true && event.target instanceof HTMLElement && isDocumentEditorTarget(event.target);
101
111
  }
102
112
  function isFindShortcut(event) {
103
113
  const binding = SHELL_SHORTCUTS.find((candidate) => candidate.id === "find");
@@ -1,6 +1,7 @@
1
1
  import {
2
+ AiReportButton,
2
3
  chatOnce
3
- } from "./chunk-NAH52736.js";
4
+ } from "./chunk-YPIVCCTB.js";
4
5
  import {
5
6
  unavailableSentence,
6
7
  useAiStatus
@@ -164,6 +165,19 @@ function PresentationMaker({
164
165
  }) {
165
166
  return createPortal(/* @__PURE__ */ jsx(PresentationDialog, { ai, onClose }), document.body);
166
167
  }
168
+ function AiSuggestionReport({ ai, hints }) {
169
+ const status = useAiStatus(ai);
170
+ const suggestions = hints.blocks.flatMap((block) => block.ai ?? []);
171
+ if (suggestions.length === 0) return null;
172
+ return /* @__PURE__ */ jsx(
173
+ AiReportButton,
174
+ {
175
+ feature: "Suggest slides",
176
+ getOutput: () => suggestions.map((entry) => [entry.headline, ...entry.points.map((point) => `- ${point}`)].join("\n")).join("\n\n"),
177
+ model: status?.kind === "ready" ? status.model?.label ?? null : null
178
+ }
179
+ );
180
+ }
167
181
  function AiRefineButton({
168
182
  ai,
169
183
  disabled,
@@ -455,7 +469,13 @@ function PresentationDialog({ ai, onClose }) {
455
469
  ] }),
456
470
  /* @__PURE__ */ jsx("p", { role: "status", "aria-live": "polite", children: plan && projection?.doc ? `${plan.beats.length} slides \xB7 ${projection.doc.duration.toFixed(1)} seconds \xB7 ${doc?.presentationNarration ? "Follows narration" : "Estimated pacing \u2014 add narration to synchronize"}` : "Preparing dynamic slides\u2026" }),
457
471
  (error || generated?.error || projection?.error) && /* @__PURE__ */ jsx("p", { role: "alert", children: error ?? generated?.error ?? projection?.error }),
458
- notice && /* @__PURE__ */ jsx("p", { role: "status", children: notice }),
472
+ notice && /* @__PURE__ */ jsxs("p", { role: "status", children: [
473
+ notice,
474
+ ai && !running && /* @__PURE__ */ jsxs(Fragment, { children: [
475
+ " ",
476
+ /* @__PURE__ */ jsx(AiSuggestionReport, { ai, hints })
477
+ ] })
478
+ ] }),
459
479
  plan && beat && section && /* @__PURE__ */ jsxs("div", { className: "db-presentation-workspace", children: [
460
480
  /* @__PURE__ */ jsx("nav", { className: "db-presentation-beats", "aria-label": "Presentation slides", children: plan.beats.map((item, index) => {
461
481
  const owner = sectionAt(sections, item.sourceStart);
@@ -0,0 +1,168 @@
1
+ // src/Ai/AiGenerationStatus.tsx
2
+ import { useEffect, useState } from "react";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ var LABELS = {
5
+ starting: "Waiting for model\u2026",
6
+ queued: "Waiting for an available model\u2026",
7
+ loading_model: "Loading model\u2026",
8
+ prefill: "Processing input (prefill)\u2026",
9
+ reasoning: "Thinking\u2026",
10
+ generating: "Writing\u2026"
11
+ };
12
+ function duration(milliseconds) {
13
+ const seconds = Math.max(0, Math.floor(milliseconds / 1e3));
14
+ return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, "0")}s`;
15
+ }
16
+ function AiGenerationStatus({
17
+ state,
18
+ characters
19
+ }) {
20
+ const [now, setNow] = useState(Date.now);
21
+ useEffect(() => {
22
+ const timer = setInterval(() => setNow(Date.now()), 1e3);
23
+ return () => clearInterval(timer);
24
+ }, []);
25
+ const progress = state.progress;
26
+ const label = progress ? LABELS[progress.phase] : "Waiting for response\u2026";
27
+ const percent = progress?.phase === "prefill" ? progress.percent : null;
28
+ const details = [`${duration(now - state.startedAt)} elapsed`];
29
+ if (progress?.outputTokens != null) {
30
+ details.push(`${progress.outputTokens.toLocaleString()} tokens generated`);
31
+ } else if (characters > 0) {
32
+ details.push(`${characters.toLocaleString()} characters received`);
33
+ }
34
+ if (progress?.tokensPerSecond != null) {
35
+ details.push(`${progress.tokensPerSecond.toFixed(1)} tokens/s`);
36
+ }
37
+ if (now - state.lastActivityAt >= 15e3) {
38
+ details.push(
39
+ progress || characters > 0 ? `Last update ${duration(now - state.lastActivityAt)} ago` : "No progress updates received"
40
+ );
41
+ }
42
+ return /* @__PURE__ */ jsxs("div", { className: "db-ai-generation-status", children: [
43
+ /* @__PURE__ */ jsxs("p", { className: "db-ai-progress", role: "status", children: [
44
+ /* @__PURE__ */ jsx("span", { className: "db-ai-spinner", "aria-hidden": "true" }),
45
+ label,
46
+ percent != null ? ` ${Math.floor(percent)}%` : ""
47
+ ] }),
48
+ percent != null && /* @__PURE__ */ jsx("progress", { "aria-label": label.replace("\u2026", ""), max: 100, value: percent }),
49
+ /* @__PURE__ */ jsx("p", { className: "db-ai-note", "aria-live": "off", children: details.join(" \xB7 ") })
50
+ ] });
51
+ }
52
+
53
+ // src/Ai/study-aids.ts
54
+ import {
55
+ parseFrontmatter,
56
+ setFrontmatterValues,
57
+ splitFrontmatterBlock
58
+ } from "@bendyline/squisq/markdown";
59
+ var STUDY_AID_KINDS = ["summary", "quiz", "flashcards"];
60
+ var STUDY_AID_LABELS = {
61
+ summary: {
62
+ menu: "Summarize document\u2026",
63
+ dialogTitle: "Summarize with AI",
64
+ title: "Summary",
65
+ noun: "summary",
66
+ feature: "Summarize document"
67
+ },
68
+ quiz: {
69
+ menu: "Create quiz\u2026",
70
+ dialogTitle: "Create a quiz with AI",
71
+ title: "Quiz",
72
+ noun: "quiz",
73
+ feature: "Create quiz"
74
+ },
75
+ flashcards: {
76
+ menu: "Create flashcards\u2026",
77
+ dialogTitle: "Create flashcards with AI",
78
+ title: "Flashcards",
79
+ noun: "flashcards",
80
+ feature: "Create flashcards"
81
+ }
82
+ };
83
+ var STUDY_AID_FOLDER = "ai";
84
+ function studyAidPath(kind) {
85
+ return `${STUDY_AID_FOLDER}/${kind}.md`;
86
+ }
87
+ var KEY_KIND = "docblocks-generated";
88
+ var KEY_SOURCE = "docblocks-source";
89
+ var KEY_SOURCE_SHA256 = "docblocks-source-sha256";
90
+ var KEY_GENERATED_AT = "docblocks-generated-at";
91
+ var KEY_MODEL = "docblocks-model";
92
+ function isStudyAidKind(value) {
93
+ return value === "summary" || value === "quiz" || value === "flashcards";
94
+ }
95
+ function withStudyAidFrontmatter(body, title, provenance) {
96
+ return setFrontmatterValues(`
97
+ ${body.replace(/^\n+/u, "")}`, {
98
+ title,
99
+ // Squisq opens a document carrying this hint in its Study view.
100
+ "display-mode": provenance.kind === "summary" ? null : "flashcards",
101
+ [KEY_KIND]: provenance.kind,
102
+ [KEY_SOURCE]: provenance.source,
103
+ [KEY_SOURCE_SHA256]: provenance.sourceSha256,
104
+ [KEY_GENERATED_AT]: provenance.generatedAt,
105
+ [KEY_MODEL]: provenance.model
106
+ });
107
+ }
108
+ function readStudyAidProvenance(markdown) {
109
+ const { frontmatter } = splitFrontmatterBlock(markdown);
110
+ if (frontmatter === null) return null;
111
+ const yaml = frontmatter.replace(/^---\r?\n/u, "").replace(/\r?\n---\s*$/u, "");
112
+ const values = parseFrontmatter(yaml);
113
+ if (!values) return null;
114
+ const kind = values[KEY_KIND];
115
+ const generatedAt = values[KEY_GENERATED_AT];
116
+ if (!isStudyAidKind(kind) || typeof generatedAt !== "string") return null;
117
+ const text = (value) => typeof value === "string" && value.trim() ? value : null;
118
+ return {
119
+ kind,
120
+ source: text(values[KEY_SOURCE]) ?? "",
121
+ sourceSha256: text(values[KEY_SOURCE_SHA256]),
122
+ generatedAt,
123
+ model: text(values[KEY_MODEL])
124
+ };
125
+ }
126
+ function studyAidBody(markdown) {
127
+ return splitFrontmatterBlock(markdown).body.replace(/^\s+/u, "");
128
+ }
129
+ function sourceBody(markdown) {
130
+ return splitFrontmatterBlock(markdown).body.replace(/\r\n?/gu, "\n").replace(/[ \t]+$/gmu, "").trim();
131
+ }
132
+ async function studySourceDigest(markdown) {
133
+ const subtle = globalThis.crypto?.subtle;
134
+ if (!subtle) return null;
135
+ const digest = await subtle.digest("SHA-256", new TextEncoder().encode(sourceBody(markdown)));
136
+ return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
137
+ }
138
+ async function studyAidIsStale(provenance, markdown) {
139
+ if (!provenance?.sourceSha256) return null;
140
+ const digest = await studySourceDigest(markdown);
141
+ return digest === null ? null : digest !== provenance.sourceSha256;
142
+ }
143
+ async function readStudyAid(container, kind) {
144
+ const data = await container.readFile(studyAidPath(kind));
145
+ if (data === null) return null;
146
+ const markdown = new TextDecoder().decode(data);
147
+ return { markdown, provenance: readStudyAidProvenance(markdown) };
148
+ }
149
+ async function writeStudyAid(container, kind, markdown) {
150
+ await container.writeFile(
151
+ studyAidPath(kind),
152
+ new TextEncoder().encode(markdown),
153
+ "text/markdown"
154
+ );
155
+ }
156
+
157
+ export {
158
+ AiGenerationStatus,
159
+ STUDY_AID_KINDS,
160
+ STUDY_AID_LABELS,
161
+ withStudyAidFrontmatter,
162
+ readStudyAidProvenance,
163
+ studyAidBody,
164
+ studySourceDigest,
165
+ studyAidIsStale,
166
+ readStudyAid,
167
+ writeStudyAid
168
+ };