@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.
- package/THIRD_PARTY_NOTICES.txt +215 -23
- package/dist/{AiDiagrams-LFV4YU6W.js → AiDiagrams-QE3LVYFK.js} +2 -2
- package/dist/{AiEditorAssistant-GN2KXKZD.js → AiEditorAssistant-YLJ5OZK2.js} +203 -164
- package/dist/AiStudyDialog-4TQZZGU3.js +1150 -0
- package/dist/{ExportToolbarControls-2FEIHNMW.js → ExportToolbarControls-A2YBKUUD.js} +32 -3
- package/dist/{KeyboardShortcutsDialog-NOD4P25G.js → KeyboardShortcutsDialog-G5OBDTNR.js} +1 -1
- package/dist/{PresentationMaker-ITNP5SDO.js → PresentationMaker-P2JP6UUX.js} +2 -2
- package/dist/{Settings-RDYvDDI0.d.ts → Settings-ByNImjk5.d.ts} +2 -4
- package/dist/{chunk-X3DDTJKR.js → chunk-3CE5RH2B.js} +255 -17
- package/dist/{chunk-HRWEGPTY.js → chunk-6PA7QBZT.js} +90 -21
- package/dist/{chunk-K7XJDJUJ.js → chunk-ACQF63SB.js} +14 -4
- package/dist/{chunk-SSB43524.js → chunk-EOU2JWAQ.js} +22 -2
- package/dist/chunk-XNT66JC2.js +168 -0
- package/dist/{chunk-NAH52736.js → chunk-YPIVCCTB.js} +969 -98
- package/dist/{chunk-BLUEZG7T.js → chunk-ZMCQO57Q.js} +1 -1
- package/dist/editor.js +3 -3
- package/dist/export/index.d.ts +10 -2
- package/dist/export/index.js +1 -1
- package/dist/index.d.ts +14 -4
- package/dist/index.js +365 -381
- package/dist/{run-export-CS3HRNAZ.js → run-export-OHRNT2TP.js} +1 -1
- package/dist/settings/index.d.ts +1 -1
- package/dist/settings/index.js +1 -1
- package/package.json +8 -8
- package/src/styles/docblocks.css +497 -57
|
@@ -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
|
|
17
|
-
(module) => module.createDiagramPictureRenderer()
|
|
18
|
-
)
|
|
23
|
+
diagramRendererFor(overrides)
|
|
19
24
|
]);
|
|
20
|
-
return await rasterizeDiagrams(doc, render, {
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
]);
|
|
79
|
-
const buf = await markdownDocToDocx(pictured.markdownDoc, {
|
|
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
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
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
|
|
92
|
+
if (isDocumentEditorTarget(target)) return true;
|
|
86
93
|
const tag = target.tagName;
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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-
|
|
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__ */
|
|
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
|
+
};
|