wiki-viewer 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/README.md +328 -99
- package/.next/standalone/agents/bootstrap-prompt.md +1 -0
- package/.next/standalone/agents/wiki-viewer-skill/SKILL.md +236 -0
- package/.next/standalone/bin/wiki-viewer.js +57 -4
- package/.next/standalone/docs/agent-collab-plan.md +1615 -0
- package/.next/standalone/docs/agent-collab-v2-plan.md +771 -0
- package/.next/standalone/package.json +19 -2
- package/.next/standalone/pnpm-lock.yaml +1368 -325
- package/.next/standalone/pnpm-workspace.yaml +7 -1
- package/.next/standalone/src/app/api/agent/activity/route.ts +58 -0
- package/.next/standalone/src/app/api/agent/admin/agents/[agentId]/revoke/route.ts +40 -0
- package/.next/standalone/src/app/api/agent/admin/agents/route.ts +33 -0
- package/.next/standalone/src/app/api/agent/admin/registrations/[regId]/approve/route.ts +83 -0
- package/.next/standalone/src/app/api/agent/admin/registrations/[regId]/deny/route.ts +36 -0
- package/.next/standalone/src/app/api/agent/admin/registrations/route.ts +27 -0
- package/.next/standalone/src/app/api/agent/events/[...path]/route.ts +136 -0
- package/.next/standalone/src/app/api/agent/files/[...path]/route.ts +202 -0
- package/.next/standalone/src/app/api/agent/internal/span/route.ts +117 -0
- package/.next/standalone/src/app/api/agent/register/[regId]/route.ts +50 -0
- package/.next/standalone/src/app/api/agent/register/route.ts +110 -0
- package/.next/standalone/src/app/api/agent/settings/route.ts +30 -0
- package/.next/standalone/src/app/api/agent/settings/token/regenerate/route.ts +20 -0
- package/.next/standalone/src/app/api/agent/sidecar/[...path]/route.ts +49 -0
- package/.next/standalone/src/app/api/agents/install/route.ts +83 -0
- package/.next/standalone/src/app/api/agents/skill/route.ts +20 -0
- package/.next/standalone/src/app/api/agents/skill.tar.gz/route.ts +87 -0
- package/.next/standalone/src/app/api/auth/[...all]/route.ts +7 -0
- package/.next/standalone/src/app/api/owner/init/route.ts +14 -0
- package/.next/standalone/src/app/api/system/auth-settings/route.ts +85 -0
- package/.next/standalone/src/app/api/system/browse/route.ts +4 -0
- package/.next/standalone/src/app/api/system/clear-root/route.ts +8 -1
- package/.next/standalone/src/app/api/system/config/route.ts +5 -1
- package/.next/standalone/src/app/api/system/pins/route.ts +7 -0
- package/.next/standalone/src/app/api/system/reveal/route.ts +7 -0
- package/.next/standalone/src/app/api/system/root-status/route.ts +5 -1
- package/.next/standalone/src/app/api/system/set-root/route.ts +7 -0
- package/.next/standalone/src/app/api/wiki/app/route.ts +15 -0
- package/.next/standalone/src/app/api/wiki/content/route.ts +110 -11
- package/.next/standalone/src/app/api/wiki/folder/route.ts +7 -0
- package/.next/standalone/src/app/api/wiki/move/route.ts +7 -0
- package/.next/standalone/src/app/api/wiki/new-file/route.ts +55 -0
- package/.next/standalone/src/app/api/wiki/page/route.ts +7 -0
- package/.next/standalone/src/app/api/wiki/route.ts +10 -0
- package/.next/standalone/src/app/api/wiki/slugs/route.ts +5 -1
- package/.next/standalone/src/app/api/wiki/upload/route.ts +7 -0
- package/.next/standalone/src/app/api/wiki/watch/route.ts +6 -1
- package/.next/standalone/src/app/globals.css +70 -0
- package/.next/standalone/src/app/page.tsx +461 -217
- package/.next/standalone/src/app/signin/page.tsx +217 -0
- package/.next/standalone/src/components/ai-panel/activity-row.tsx +64 -0
- package/.next/standalone/src/components/ai-panel/ai-panel.tsx +322 -0
- package/.next/standalone/src/components/ai-panel/token-section.tsx +312 -0
- package/.next/standalone/src/components/auth-settings-sheet.tsx +209 -0
- package/.next/standalone/src/components/editor/bubble-menu.tsx +41 -2
- package/.next/standalone/src/components/editor/comment-pip.tsx +56 -0
- package/.next/standalone/src/components/editor/comment-thread.tsx +252 -0
- package/.next/standalone/src/components/editor/editor.tsx +513 -10
- package/.next/standalone/src/components/editor/extensions/proof-span.ts +60 -0
- package/.next/standalone/src/components/editor/extensions.ts +2 -0
- package/.next/standalone/src/components/editor/proof-span-popover.tsx +161 -0
- package/.next/standalone/src/components/editor/suggest-edit-popover.tsx +228 -0
- package/.next/standalone/src/components/editor/suggestion-card.tsx +201 -0
- package/.next/standalone/src/components/view-width-toggle.tsx +47 -0
- package/.next/standalone/src/components/wiki/markdown-preview.tsx +120 -0
- package/.next/standalone/src/lib/auth/allowlist.ts +50 -0
- package/.next/standalone/src/lib/auth/client.ts +4 -0
- package/.next/standalone/src/lib/auth/csrf.ts +68 -0
- package/.next/standalone/src/lib/auth/server.ts +164 -0
- package/.next/standalone/src/lib/config.ts +4 -0
- package/.next/standalone/src/lib/markdown/parse-frontmatter.ts +20 -0
- package/.next/standalone/src/lib/markdown/sanitize-schema.ts +106 -0
- package/.next/standalone/src/lib/markdown/to-html.ts +46 -8
- package/.next/standalone/src/lib/markdown/to-markdown.ts +14 -0
- package/.next/standalone/src/lib/proof/activity-shared.ts +31 -0
- package/.next/standalone/src/lib/proof/activity.ts +74 -0
- package/.next/standalone/src/lib/proof/auth.ts +132 -0
- package/.next/standalone/src/lib/proof/block-refs.ts +133 -0
- package/.next/standalone/src/lib/proof/blocks.ts +73 -0
- package/.next/standalone/src/lib/proof/client-auth.ts +23 -0
- package/.next/standalone/src/lib/proof/event-bus.ts +47 -0
- package/.next/standalone/src/lib/proof/file-lock.ts +38 -0
- package/.next/standalone/src/lib/proof/glob.ts +51 -0
- package/.next/standalone/src/lib/proof/idempotency.ts +32 -0
- package/.next/standalone/src/lib/proof/mutex.ts +32 -0
- package/.next/standalone/src/lib/proof/ops-applier.ts +678 -0
- package/.next/standalone/src/lib/proof/owner-auth.ts +2 -0
- package/.next/standalone/src/lib/proof/pending.ts +97 -0
- package/.next/standalone/src/lib/proof/proof-span.ts +141 -0
- package/.next/standalone/src/lib/proof/rate-limit.ts +53 -0
- package/.next/standalone/src/lib/proof/register-rate-limit.ts +53 -0
- package/.next/standalone/src/lib/proof/registry.ts +190 -0
- package/.next/standalone/src/lib/proof/sidecar.ts +66 -0
- package/.next/standalone/src/lib/proof/suggest-capture.ts +69 -0
- package/.next/standalone/src/lib/proof/types.ts +170 -0
- package/.next/standalone/src/lib/proof-config.ts +14 -0
- package/.next/standalone/src/middleware.ts +38 -0
- package/.next/standalone/src/stores/ai-panel-store.ts +58 -3
- package/.next/standalone/src/stores/editor-store.ts +115 -9
- package/.next/standalone/src/stores/proof-store.ts +123 -0
- package/.next/standalone/src/stores/view-width-store.ts +62 -0
- package/.next/standalone/src/tests/proof/activity-aggregator.test.ts +146 -0
- package/.next/standalone/src/tests/proof/agent-ownership.test.ts +140 -0
- package/.next/standalone/src/tests/proof/agents-install.test.ts +57 -0
- package/.next/standalone/src/tests/proof/better-auth.test.ts +68 -0
- package/.next/standalone/src/tests/proof/block-refs.test.ts +108 -0
- package/.next/standalone/src/tests/proof/blocks.test.ts +92 -0
- package/.next/standalone/src/tests/proof/comments-ops.test.ts +177 -0
- package/.next/standalone/src/tests/proof/editor-roundtrip.test.ts +85 -0
- package/.next/standalone/src/tests/proof/external-edit.test.ts +58 -0
- package/.next/standalone/src/tests/proof/file-lock.test.ts +80 -0
- package/.next/standalone/src/tests/proof/glob.test.ts +82 -0
- package/.next/standalone/src/tests/proof/helpers/auth-session.ts +27 -0
- package/.next/standalone/src/tests/proof/markdown-to-html.test.ts +46 -0
- package/.next/standalone/src/tests/proof/ops-applier.test.ts +368 -0
- package/.next/standalone/src/tests/proof/owner-init.test.ts +2 -0
- package/.next/standalone/src/tests/proof/parse-frontmatter.test.ts +60 -0
- package/.next/standalone/src/tests/proof/proof-span.test.ts +98 -0
- package/.next/standalone/src/tests/proof/rate-limit.test.ts +54 -0
- package/.next/standalone/src/tests/proof/registration-flow.test.ts +330 -0
- package/.next/standalone/src/tests/proof/registry.test.ts +169 -0
- package/.next/standalone/src/tests/proof/routes.test.ts +516 -0
- package/.next/standalone/src/tests/proof/sidecar-trim.test.ts +58 -0
- package/.next/standalone/src/tests/proof/suggestion-ops.test.ts +280 -0
- package/.next/standalone/src/tests/proof/wiki-content-put.test.ts +140 -0
- package/.next/standalone/src/tests/proof/wiki-routes-auth.test.ts +208 -0
- package/README.md +328 -99
- package/bin/wiki-viewer.js +57 -4
- package/package.json +19 -2
|
@@ -3,19 +3,27 @@
|
|
|
3
3
|
import { cellAround, isInTable } from "@tiptap/pm/tables";
|
|
4
4
|
import { EditorContent, useEditor } from "@tiptap/react";
|
|
5
5
|
import { Code2, FilePlus, Loader2, Sparkles } from "lucide-react";
|
|
6
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
6
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
7
7
|
import { findNodeByPath } from "@/lib/cabinets/tree";
|
|
8
8
|
import { markdownToHtml } from "@/lib/markdown/to-html";
|
|
9
9
|
import { htmlToMarkdown } from "@/lib/markdown/to-markdown";
|
|
10
10
|
import { useAIPanelStore } from "@/stores/ai-panel-store";
|
|
11
11
|
import { useEditorStore } from "@/stores/editor-store";
|
|
12
12
|
import { useTreeStore } from "@/stores/tree-store";
|
|
13
|
+
import { useViewWidthStore, VIEW_WIDTH_CSS } from "@/stores/view-width-store";
|
|
13
14
|
import { useWikiSlugsStore } from "@/stores/wiki-slugs-store";
|
|
14
15
|
import type { TreeNode } from "@/types";
|
|
16
|
+
import { useProofStore } from "@/stores/proof-store";
|
|
17
|
+
import { captureSuggestion } from "@/lib/proof/suggest-capture";
|
|
15
18
|
import { EditorBubbleMenu } from "./bubble-menu";
|
|
16
19
|
import { EditorToolbar } from "./editor-toolbar";
|
|
17
20
|
import { editorExtensions } from "./extensions";
|
|
18
21
|
import { FolderIndex } from "./folder-index";
|
|
22
|
+
import { CommentPip } from "./comment-pip";
|
|
23
|
+
import { CommentThread } from "./comment-thread";
|
|
24
|
+
import { ProofSpanPopover } from "./proof-span-popover";
|
|
25
|
+
import { SuggestionCard } from "./suggestion-card";
|
|
26
|
+
import { SuggestEditPopover } from "./suggest-edit-popover";
|
|
19
27
|
import { SlashCommands } from "./slash-commands";
|
|
20
28
|
import { TableMenu } from "./table-menu";
|
|
21
29
|
import {
|
|
@@ -136,8 +144,11 @@ export function KBEditor() {
|
|
|
136
144
|
isLoading,
|
|
137
145
|
loadStatus,
|
|
138
146
|
createMissingPage,
|
|
147
|
+
editMode,
|
|
148
|
+
setEditMode,
|
|
139
149
|
} = useEditorStore();
|
|
140
150
|
const nodes = useTreeStore((s) => s.nodes);
|
|
151
|
+
const editorMaxW = useViewWidthStore((s) => VIEW_WIDTH_CSS[s.width]);
|
|
141
152
|
const isRtl = frontmatter?.dir === "rtl";
|
|
142
153
|
const { open: openAI, clearMessages } = useAIPanelStore();
|
|
143
154
|
const { open: openWikiCreate, Dialog: WikiCreateDialog } =
|
|
@@ -167,9 +178,323 @@ export function KBEditor() {
|
|
|
167
178
|
void useWikiSlugsStore.getState().load();
|
|
168
179
|
}, []);
|
|
169
180
|
|
|
181
|
+
// Load sidecar when the current path changes.
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (!currentPath) return;
|
|
184
|
+
void useProofStore.getState().loadSidecar(currentPath);
|
|
185
|
+
}, [currentPath]);
|
|
186
|
+
|
|
187
|
+
// Subscribe to chokidar SSE: when current file changes on disk, reload sidecar.
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
if (typeof window === "undefined") return;
|
|
190
|
+
const es = new EventSource("/api/wiki/watch");
|
|
191
|
+
es.onmessage = (evt: MessageEvent<string>) => {
|
|
192
|
+
try {
|
|
193
|
+
const data = JSON.parse(evt.data) as { type: string; path: string };
|
|
194
|
+
const activePath = useEditorStore.getState().currentPath;
|
|
195
|
+
if (
|
|
196
|
+
(data.type === "change" || data.type === "add") &&
|
|
197
|
+
activePath &&
|
|
198
|
+
data.path === activePath
|
|
199
|
+
) {
|
|
200
|
+
// loadSnapshot first so server-side readSnapshot detects
|
|
201
|
+
// fingerprint mismatch, emits file.externallyEdited, and persists
|
|
202
|
+
// the sidecar. Then loadSidecar to refresh comments/suggestions.
|
|
203
|
+
void useProofStore
|
|
204
|
+
.getState()
|
|
205
|
+
.loadSnapshot(activePath)
|
|
206
|
+
.then(() => useProofStore.getState().loadSidecar(activePath));
|
|
207
|
+
}
|
|
208
|
+
} catch {
|
|
209
|
+
// ignore malformed events
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
return () => {
|
|
213
|
+
es.close();
|
|
214
|
+
};
|
|
215
|
+
}, []);
|
|
216
|
+
|
|
217
|
+
// Proof-span popover state.
|
|
218
|
+
const [proofTarget, setProofTarget] = useState<HTMLElement | null>(null);
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Ref to the editor scroll container. Used to compute block positions
|
|
222
|
+
* relative to the scrollable area for suggestion cards and comment pips.
|
|
223
|
+
*
|
|
224
|
+
* Phase D coordination: comment-pip positioning uses this same ref and the
|
|
225
|
+
* same blockRefPositions map computed below.
|
|
226
|
+
*/
|
|
227
|
+
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
228
|
+
|
|
229
|
+
/** Map of block ref → position relative to scroll container */
|
|
230
|
+
const [blockRefPositions, setBlockRefPositions] = useState<
|
|
231
|
+
Map<string, { top: number; left: number; width: number; bottom: number }>
|
|
232
|
+
>(new Map());
|
|
233
|
+
|
|
234
|
+
// Subscribe to snapshot data for suggestion cards.
|
|
235
|
+
// NOTE: select the RAW stored references here — returning a freshly built
|
|
236
|
+
// array (e.g. `?? []` or `.filter(...)`) on every call makes
|
|
237
|
+
// useSyncExternalStore think the snapshot changed each render, which spins
|
|
238
|
+
// into a "Maximum update depth exceeded" loop. Derive defaults/filters below.
|
|
239
|
+
const snapshotBlocksRaw = useProofStore((s) =>
|
|
240
|
+
currentPath ? s.byPath[currentPath]?.snapshotBlocks : undefined
|
|
241
|
+
);
|
|
242
|
+
const suggestionsRaw = useProofStore((s) =>
|
|
243
|
+
currentPath ? s.byPath[currentPath]?.sidecar?.suggestions : undefined
|
|
244
|
+
);
|
|
245
|
+
const snapshotRevision = useProofStore((s) =>
|
|
246
|
+
currentPath ? (s.byPath[currentPath]?.snapshotRevision ?? 0) : 0
|
|
247
|
+
);
|
|
248
|
+
const commentsRaw = useProofStore((s) =>
|
|
249
|
+
currentPath ? s.byPath[currentPath]?.sidecar?.comments : undefined
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
const snapshotBlocks = useMemo(() => snapshotBlocksRaw ?? [], [snapshotBlocksRaw]);
|
|
253
|
+
const comments = useMemo(() => commentsRaw ?? [], [commentsRaw]);
|
|
254
|
+
const pendingSuggestions = useMemo(
|
|
255
|
+
() => suggestionsRaw?.filter((sg) => sg.status === "pending") ?? [],
|
|
256
|
+
[suggestionsRaw],
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
/** Group comments by block ref for pip rendering. */
|
|
260
|
+
const commentsByRef = useMemo(() => {
|
|
261
|
+
const map: Record<string, typeof comments> = {};
|
|
262
|
+
for (const c of comments) {
|
|
263
|
+
(map[c.ref] ??= []).push(c);
|
|
264
|
+
}
|
|
265
|
+
return map;
|
|
266
|
+
}, [comments]);
|
|
267
|
+
|
|
268
|
+
/** Tracks which block's comment thread is open and its anchor element. */
|
|
269
|
+
const [threadTarget, setThreadTarget] = useState<{ blockRef: string; el: HTMLElement } | null>(null);
|
|
270
|
+
|
|
271
|
+
/** Tracks the open human "suggest edit" popover (block + anchor + content). */
|
|
272
|
+
const [suggestTarget, setSuggestTarget] = useState<
|
|
273
|
+
{ blockRef: string; markdown: string; anchor: { top: number; left: number } } | null
|
|
274
|
+
>(null);
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Resolve the current editor selection to a top-level block.
|
|
278
|
+
*
|
|
279
|
+
* Primary strategy: map the selection to its top-level ProseMirror child
|
|
280
|
+
* INDEX, then look up snapshotBlocks[index] — the same index-based mapping
|
|
281
|
+
* used by the position-tracker effect. This is robust even when the DOM
|
|
282
|
+
* `data-block-ref` annotation has not been applied yet (e.g. snapshot still
|
|
283
|
+
* loading), which previously made the suggest/comment buttons silently
|
|
284
|
+
* no-op. Falls back to walking the DOM for an existing [data-block-ref].
|
|
285
|
+
*/
|
|
286
|
+
const resolveSelectionBlock = useCallback((): {
|
|
287
|
+
blockRef: string;
|
|
288
|
+
blockEl: HTMLElement;
|
|
289
|
+
markdown: string;
|
|
290
|
+
} | null => {
|
|
291
|
+
if (!editorRef.current) return null;
|
|
292
|
+
const view = editorRef.current.view;
|
|
293
|
+
const { from } = view.state.selection;
|
|
294
|
+
const path = useEditorStore.getState().currentPath ?? "";
|
|
295
|
+
const blocks = useProofStore.getState().byPath[path]?.snapshotBlocks ?? [];
|
|
296
|
+
|
|
297
|
+
// Find the top-level child index containing the selection head.
|
|
298
|
+
const $pos = view.state.doc.resolve(from);
|
|
299
|
+
const topIndex = $pos.depth > 0 ? $pos.index(0) : 0;
|
|
300
|
+
|
|
301
|
+
const proseMirror = scrollContainerRef.current?.querySelector(".ProseMirror");
|
|
302
|
+
const children = proseMirror
|
|
303
|
+
? (Array.from(proseMirror.children) as HTMLElement[])
|
|
304
|
+
: [];
|
|
305
|
+
const blockEl = children[topIndex] ?? null;
|
|
306
|
+
|
|
307
|
+
// Prefer the index-aligned snapshot block; fall back to the DOM attr.
|
|
308
|
+
const block = blocks[topIndex];
|
|
309
|
+
let blockRef: string | null =
|
|
310
|
+
block?.ref ?? blockEl?.getAttribute("data-block-ref") ?? null;
|
|
311
|
+
const markdown = block?.markdown ?? "";
|
|
312
|
+
|
|
313
|
+
if (!blockRef && blockEl) {
|
|
314
|
+
// Last-resort: DOM walk from selection anchor.
|
|
315
|
+
const domAt = view.domAtPos(from);
|
|
316
|
+
const node: HTMLElement | null =
|
|
317
|
+
domAt.node.nodeType === Node.ELEMENT_NODE
|
|
318
|
+
? (domAt.node as HTMLElement)
|
|
319
|
+
: domAt.node.parentElement;
|
|
320
|
+
const found = node?.closest<HTMLElement>("[data-block-ref]") ?? null;
|
|
321
|
+
blockRef = found?.getAttribute("data-block-ref") ?? null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (!blockRef || !blockEl) return null;
|
|
325
|
+
return { blockRef, blockEl, markdown };
|
|
326
|
+
}, []);
|
|
327
|
+
|
|
328
|
+
const openSuggestForSelection = useCallback(() => {
|
|
329
|
+
const resolved = resolveSelectionBlock();
|
|
330
|
+
if (!resolved) return;
|
|
331
|
+
const rect = resolved.blockEl.getBoundingClientRect();
|
|
332
|
+
setSuggestTarget({
|
|
333
|
+
blockRef: resolved.blockRef,
|
|
334
|
+
markdown: resolved.markdown,
|
|
335
|
+
anchor: { top: rect.bottom + 4, left: rect.left },
|
|
336
|
+
});
|
|
337
|
+
}, [resolveSelectionBlock]);
|
|
338
|
+
|
|
339
|
+
const openCommentForSelection = useCallback(() => {
|
|
340
|
+
const resolved = resolveSelectionBlock();
|
|
341
|
+
if (!resolved) return;
|
|
342
|
+
setThreadTarget({ blockRef: resolved.blockRef, el: resolved.blockEl });
|
|
343
|
+
}, [resolveSelectionBlock]);
|
|
344
|
+
|
|
345
|
+
// ── Suggesting mode: capture human block edits as suggestions ──────────────
|
|
346
|
+
//
|
|
347
|
+
// In suggesting mode the editor stays editable but edits never touch the
|
|
348
|
+
// file. On flush (leaving a block or blurring the editor) we diff each
|
|
349
|
+
// top-level block against the snapshot, emit a human `suggestion.add` for
|
|
350
|
+
// every changed/added/removed block, then revert the editor to the snapshot
|
|
351
|
+
// so the pending suggestion cards render over the original content.
|
|
352
|
+
|
|
353
|
+
/** Set true whenever the user edits while in suggesting mode. */
|
|
354
|
+
const suggestDirtyRef = useRef(false);
|
|
355
|
+
/** Guards against re-entrant flushes (capture is async). */
|
|
356
|
+
const flushingRef = useRef(false);
|
|
357
|
+
/** Top-level block index that currently holds the selection. */
|
|
358
|
+
const activeBlockIndexRef = useRef<number | null>(null);
|
|
359
|
+
|
|
360
|
+
const normalizeMd = (s: string): string => s.replace(/\s+$/g, "").trimStart();
|
|
361
|
+
|
|
362
|
+
const flushSuggestions = useCallback(async () => {
|
|
363
|
+
if (flushingRef.current) return;
|
|
364
|
+
if (useEditorStore.getState().editMode !== "suggesting") return;
|
|
365
|
+
if (!suggestDirtyRef.current) return;
|
|
366
|
+
const ed = editorRef.current;
|
|
367
|
+
const path = useEditorStore.getState().currentPath;
|
|
368
|
+
if (!ed || !path) return;
|
|
369
|
+
|
|
370
|
+
const proseMirror = scrollContainerRef.current?.querySelector(".ProseMirror");
|
|
371
|
+
if (!proseMirror) return;
|
|
372
|
+
const children = Array.from(proseMirror.children) as HTMLElement[];
|
|
373
|
+
const snapBlocks =
|
|
374
|
+
useProofStore.getState().byPath[path]?.snapshotBlocks ?? [];
|
|
375
|
+
if (snapBlocks.length === 0) return;
|
|
376
|
+
|
|
377
|
+
flushingRef.current = true;
|
|
378
|
+
suggestDirtyRef.current = false;
|
|
379
|
+
try {
|
|
380
|
+
const getRevision = () =>
|
|
381
|
+
useProofStore.getState().byPath[path]?.snapshotRevision ?? 0;
|
|
382
|
+
const refresh = async () => {
|
|
383
|
+
await useProofStore.getState().loadSnapshot(path);
|
|
384
|
+
await useProofStore.getState().loadSidecar(path);
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const count = Math.max(children.length, snapBlocks.length);
|
|
388
|
+
let captured = false;
|
|
389
|
+
for (let i = 0; i < count; i++) {
|
|
390
|
+
const el = children[i];
|
|
391
|
+
const snap = snapBlocks[i];
|
|
392
|
+
const curMd = el ? htmlToMarkdown(el.outerHTML).trim() : null;
|
|
393
|
+
|
|
394
|
+
if (snap && curMd !== null) {
|
|
395
|
+
if (normalizeMd(curMd) !== normalizeMd(snap.markdown)) {
|
|
396
|
+
const ok = await captureSuggestion({
|
|
397
|
+
path,
|
|
398
|
+
ref: snap.ref,
|
|
399
|
+
kind: "replace",
|
|
400
|
+
markdown: curMd,
|
|
401
|
+
getRevision,
|
|
402
|
+
refresh,
|
|
403
|
+
});
|
|
404
|
+
captured = captured || ok;
|
|
405
|
+
}
|
|
406
|
+
} else if (snap && curMd === null) {
|
|
407
|
+
const ok = await captureSuggestion({
|
|
408
|
+
path,
|
|
409
|
+
ref: snap.ref,
|
|
410
|
+
kind: "delete",
|
|
411
|
+
getRevision,
|
|
412
|
+
refresh,
|
|
413
|
+
});
|
|
414
|
+
captured = captured || ok;
|
|
415
|
+
} else if (!snap && curMd !== null && curMd.length > 0) {
|
|
416
|
+
// New trailing block: suggest inserting after the last known block.
|
|
417
|
+
const lastRef = snapBlocks[snapBlocks.length - 1]?.ref;
|
|
418
|
+
if (lastRef) {
|
|
419
|
+
const ok = await captureSuggestion({
|
|
420
|
+
path,
|
|
421
|
+
ref: lastRef,
|
|
422
|
+
kind: "insertAfter",
|
|
423
|
+
markdown: curMd,
|
|
424
|
+
getRevision,
|
|
425
|
+
refresh,
|
|
426
|
+
});
|
|
427
|
+
captured = captured || ok;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (captured) {
|
|
433
|
+
// Reload sidecar so the new pending suggestion cards appear, then
|
|
434
|
+
// revert the editor to the snapshot (file unchanged).
|
|
435
|
+
await refresh();
|
|
436
|
+
const freshSnap =
|
|
437
|
+
useProofStore.getState().byPath[path]?.snapshotBlocks ?? snapBlocks;
|
|
438
|
+
const snapshotMarkdown = freshSnap.map((b) => b.markdown).join("\n\n");
|
|
439
|
+
isLoadingRef.current = true;
|
|
440
|
+
const html = await markdownToHtml(snapshotMarkdown, path);
|
|
441
|
+
ed.commands.setContent(html);
|
|
442
|
+
setTimeout(() => {
|
|
443
|
+
isLoadingRef.current = false;
|
|
444
|
+
}, 50);
|
|
445
|
+
}
|
|
446
|
+
} finally {
|
|
447
|
+
flushingRef.current = false;
|
|
448
|
+
}
|
|
449
|
+
}, []);
|
|
450
|
+
|
|
451
|
+
// Load snapshot (ordered block list) when path changes so suggestion cards
|
|
452
|
+
// can look up block content by ref.
|
|
453
|
+
useEffect(() => {
|
|
454
|
+
if (!currentPath) return;
|
|
455
|
+
void useProofStore.getState().loadSnapshot(currentPath);
|
|
456
|
+
}, [currentPath]);
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* After content renders, walk `.ProseMirror > *` to build ref→position map.
|
|
460
|
+
* Matches by index: the i-th ProseMirror child = snapshotBlocks[i].
|
|
461
|
+
*
|
|
462
|
+
* Phase D coordination: this effect also annotates each child element with
|
|
463
|
+
* data-block-ref for any consumer that needs CSS/query-based lookup.
|
|
464
|
+
*/
|
|
465
|
+
useEffect(() => {
|
|
466
|
+
if (!currentPath || snapshotBlocks.length === 0 || !scrollContainerRef.current) return;
|
|
467
|
+
const container = scrollContainerRef.current;
|
|
468
|
+
const proseMirror = container.querySelector(".ProseMirror");
|
|
469
|
+
if (!proseMirror) return;
|
|
470
|
+
const children = Array.from(proseMirror.children) as HTMLElement[];
|
|
471
|
+
const containerRect = container.getBoundingClientRect();
|
|
472
|
+
const next = new Map<string, { top: number; left: number; width: number; bottom: number }>();
|
|
473
|
+
for (let i = 0; i < Math.min(children.length, snapshotBlocks.length); i++) {
|
|
474
|
+
const el = children[i];
|
|
475
|
+
const block = snapshotBlocks[i];
|
|
476
|
+
// Annotate DOM element — Phase D comment-pip and other consumers read this
|
|
477
|
+
el.setAttribute("data-block-ref", block.ref);
|
|
478
|
+
const rect = el.getBoundingClientRect();
|
|
479
|
+
next.set(block.ref, {
|
|
480
|
+
top: rect.top - containerRect.top + container.scrollTop,
|
|
481
|
+
left: rect.left - containerRect.left,
|
|
482
|
+
width: rect.width,
|
|
483
|
+
bottom: rect.bottom - containerRect.top + container.scrollTop,
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
setBlockRefPositions(next);
|
|
487
|
+
}, [currentPath, snapshotBlocks]);
|
|
488
|
+
|
|
170
489
|
const handleUpdate = useCallback(
|
|
171
490
|
({ editor }: { editor: ReturnType<typeof useEditor> }) => {
|
|
172
491
|
if (isLoadingRef.current || !editor) return;
|
|
492
|
+
// In suggesting mode, mark the edit dirty so the next block-change or
|
|
493
|
+
// blur flushes it into suggestions. Still push content to the store so
|
|
494
|
+
// the store guard (no autosave in suggesting mode) keeps it in sync.
|
|
495
|
+
if (useEditorStore.getState().editMode === "suggesting") {
|
|
496
|
+
suggestDirtyRef.current = true;
|
|
497
|
+
}
|
|
173
498
|
const html = editor.getHTML();
|
|
174
499
|
const md = htmlToMarkdown(html);
|
|
175
500
|
useEditorStore.getState().updateContent(md);
|
|
@@ -181,10 +506,25 @@ export function KBEditor() {
|
|
|
181
506
|
extensions: editorExtensions,
|
|
182
507
|
content: "",
|
|
183
508
|
onUpdate: handleUpdate,
|
|
509
|
+
onBlur: () => {
|
|
510
|
+
void flushSuggestions();
|
|
511
|
+
},
|
|
512
|
+
onSelectionUpdate: ({ editor: ed }) => {
|
|
513
|
+
if (useEditorStore.getState().editMode !== "suggesting") return;
|
|
514
|
+
const { from } = ed.state.selection;
|
|
515
|
+
const $pos = ed.state.doc.resolve(from);
|
|
516
|
+
const idx = $pos.depth > 0 ? $pos.index(0) : 0;
|
|
517
|
+
const prev = activeBlockIndexRef.current;
|
|
518
|
+
activeBlockIndexRef.current = idx;
|
|
519
|
+
// Moved to a different top-level block — flush edits to the prior one.
|
|
520
|
+
if (prev !== null && prev !== idx && suggestDirtyRef.current) {
|
|
521
|
+
void flushSuggestions();
|
|
522
|
+
}
|
|
523
|
+
},
|
|
184
524
|
editorProps: {
|
|
185
525
|
attributes: {
|
|
186
526
|
class:
|
|
187
|
-
"focus:outline-none min-h-[calc(100vh-12rem)] px-4 sm:px-8 py-6 max-w-
|
|
527
|
+
"focus:outline-none min-h-[calc(100vh-12rem)] px-4 sm:px-8 py-6 max-w-[var(--editor-max-w,48rem)] mx-auto",
|
|
188
528
|
},
|
|
189
529
|
handleKeyDown: (view, event) => {
|
|
190
530
|
if (
|
|
@@ -348,6 +688,10 @@ export function KBEditor() {
|
|
|
348
688
|
immediatelyRender: false,
|
|
349
689
|
});
|
|
350
690
|
|
|
691
|
+
// Stable ref to the editor so callbacks with empty deps reach the live instance.
|
|
692
|
+
const editorRef = useRef<typeof editor>(editor);
|
|
693
|
+
editorRef.current = editor;
|
|
694
|
+
|
|
351
695
|
// When content updates from store (after loadPage), set it in editor
|
|
352
696
|
const prevPathRef = useRef<string | null>(null);
|
|
353
697
|
const renderedKeyRef = useRef<string | null>(null);
|
|
@@ -563,12 +907,136 @@ export function KBEditor() {
|
|
|
563
907
|
</div>
|
|
564
908
|
) : (
|
|
565
909
|
<div className="flex-1 relative" dir={isRtl ? "rtl" : undefined}>
|
|
910
|
+
{editMode === "suggesting" && (
|
|
911
|
+
<div className="absolute top-0 inset-x-0 z-20 flex items-center justify-center gap-2 px-3 py-1 bg-primary/10 border-b border-primary/20 text-[11px] text-primary pointer-events-none">
|
|
912
|
+
Suggesting mode · your edits become suggestions for review
|
|
913
|
+
</div>
|
|
914
|
+
)}
|
|
566
915
|
<div
|
|
567
|
-
|
|
916
|
+
ref={scrollContainerRef}
|
|
917
|
+
className={`absolute inset-0 overflow-y-auto ${
|
|
918
|
+
editMode === "suggesting" ? "pt-7" : ""
|
|
919
|
+
}`}
|
|
920
|
+
style={{ ["--editor-max-w" as string]: editorMaxW }}
|
|
568
921
|
data-editor-scroll
|
|
569
922
|
>
|
|
923
|
+
{/* Absolutely-positioned overlay for comment pips and suggestion cards.
|
|
924
|
+
height:0 so it doesn't push content; children overflow freely.
|
|
925
|
+
Positions from blockRefPositions are relative to scroll container top. */}
|
|
926
|
+
<div
|
|
927
|
+
aria-hidden="true"
|
|
928
|
+
className="relative pointer-events-none"
|
|
929
|
+
style={{ height: 0 }}
|
|
930
|
+
>
|
|
931
|
+
{/* Comment pips — one per block with at least one comment */}
|
|
932
|
+
{Object.entries(commentsByRef).map(([blockRef, blockComments]) => {
|
|
933
|
+
const pos = blockRefPositions.get(blockRef);
|
|
934
|
+
if (!pos) return null;
|
|
935
|
+
return (
|
|
936
|
+
<div key={`pip-${blockRef}`} style={{ pointerEvents: "auto" }}>
|
|
937
|
+
<CommentPip
|
|
938
|
+
blockRef={blockRef}
|
|
939
|
+
comments={blockComments}
|
|
940
|
+
top={pos.top + 4}
|
|
941
|
+
left={Math.max(0, pos.left - 20)}
|
|
942
|
+
onClick={() => {
|
|
943
|
+
const el = scrollContainerRef.current?.querySelector(
|
|
944
|
+
`[data-block-ref="${blockRef}"]`,
|
|
945
|
+
) as HTMLElement | null;
|
|
946
|
+
if (el) setThreadTarget({ blockRef, el });
|
|
947
|
+
}}
|
|
948
|
+
/>
|
|
949
|
+
</div>
|
|
950
|
+
);
|
|
951
|
+
})}
|
|
952
|
+
|
|
953
|
+
{/* Suggestion cards — one per pending suggestion */}
|
|
954
|
+
{currentPath && pendingSuggestions.map((sg) => {
|
|
955
|
+
const pos = blockRefPositions.get(sg.ref);
|
|
956
|
+
const currentBlock = snapshotBlocks.find((b) => b.ref === sg.ref);
|
|
957
|
+
if (!pos || !currentBlock) return null;
|
|
958
|
+
const cardTop =
|
|
959
|
+
sg.kind === "insertAfter" ? pos.bottom + 4 :
|
|
960
|
+
sg.kind === "insertBefore" ? Math.max(0, pos.top - 80) :
|
|
961
|
+
pos.top;
|
|
962
|
+
return (
|
|
963
|
+
<div key={`sug-${sg.id}`} style={{ pointerEvents: "auto" }}>
|
|
964
|
+
<SuggestionCard
|
|
965
|
+
path={currentPath}
|
|
966
|
+
suggestion={sg}
|
|
967
|
+
currentMarkdown={currentBlock.markdown}
|
|
968
|
+
baseRevision={snapshotRevision}
|
|
969
|
+
getLatestRevision={() =>
|
|
970
|
+
useProofStore.getState().byPath[currentPath]?.snapshotRevision ?? 0
|
|
971
|
+
}
|
|
972
|
+
top={cardTop}
|
|
973
|
+
left={pos.left}
|
|
974
|
+
width={pos.width}
|
|
975
|
+
onSettled={() => {
|
|
976
|
+
void useProofStore.getState().loadSidecar(currentPath);
|
|
977
|
+
void useProofStore.getState().loadSnapshot(currentPath);
|
|
978
|
+
}}
|
|
979
|
+
/>
|
|
980
|
+
</div>
|
|
981
|
+
);
|
|
982
|
+
})}
|
|
983
|
+
</div>
|
|
984
|
+
|
|
985
|
+
{/* Comment thread — Portal-rendered, driven by threadTarget */}
|
|
986
|
+
{threadTarget && currentPath && (
|
|
987
|
+
<CommentThread
|
|
988
|
+
path={currentPath}
|
|
989
|
+
blockRef={threadTarget.blockRef}
|
|
990
|
+
comments={commentsByRef[threadTarget.blockRef] ?? []}
|
|
991
|
+
anchorEl={threadTarget.el}
|
|
992
|
+
onClose={() => setThreadTarget(null)}
|
|
993
|
+
/>
|
|
994
|
+
)}
|
|
995
|
+
|
|
996
|
+
{/* Human suggest-edit popover — driven by suggestTarget */}
|
|
997
|
+
{suggestTarget && currentPath && (
|
|
998
|
+
<SuggestEditPopover
|
|
999
|
+
path={currentPath}
|
|
1000
|
+
blockRef={suggestTarget.blockRef}
|
|
1001
|
+
currentMarkdown={suggestTarget.markdown}
|
|
1002
|
+
anchor={suggestTarget.anchor}
|
|
1003
|
+
onClose={() => setSuggestTarget(null)}
|
|
1004
|
+
/>
|
|
1005
|
+
)}
|
|
1006
|
+
|
|
570
1007
|
<EditorContent editor={editor} />
|
|
571
|
-
|
|
1008
|
+
{/* Proof-span hover delegation */}
|
|
1009
|
+
<div
|
|
1010
|
+
aria-hidden="true"
|
|
1011
|
+
className="contents"
|
|
1012
|
+
onMouseOver={(e) => {
|
|
1013
|
+
const span = (e.target as HTMLElement).closest<HTMLElement>(".proof-span");
|
|
1014
|
+
if (span && span !== proofTarget) setProofTarget(span);
|
|
1015
|
+
}}
|
|
1016
|
+
onMouseOut={(e) => {
|
|
1017
|
+
const related = e.relatedTarget as HTMLElement | null;
|
|
1018
|
+
if (!related?.closest(".proof-span")) setProofTarget(null);
|
|
1019
|
+
}}
|
|
1020
|
+
/>
|
|
1021
|
+
{currentPath && (
|
|
1022
|
+
<ProofSpanPopover
|
|
1023
|
+
targetEl={proofTarget}
|
|
1024
|
+
path={currentPath}
|
|
1025
|
+
onClose={() => setProofTarget(null)}
|
|
1026
|
+
onComment={() => {
|
|
1027
|
+
if (!proofTarget) return;
|
|
1028
|
+
const blockEl = proofTarget.closest<HTMLElement>("[data-block-ref]");
|
|
1029
|
+
if (!blockEl) return;
|
|
1030
|
+
const blockRef = blockEl.getAttribute("data-block-ref");
|
|
1031
|
+
if (blockRef) setThreadTarget({ blockRef, el: blockEl });
|
|
1032
|
+
}}
|
|
1033
|
+
/>
|
|
1034
|
+
)}
|
|
1035
|
+
<EditorBubbleMenu
|
|
1036
|
+
editor={editor}
|
|
1037
|
+
onSuggestEdit={openSuggestForSelection}
|
|
1038
|
+
onComment={openCommentForSelection}
|
|
1039
|
+
/>
|
|
572
1040
|
<TableMenu editor={editor} />
|
|
573
1041
|
<SlashCommands editor={editor} />
|
|
574
1042
|
<WikiLinkPicker
|
|
@@ -577,7 +1045,7 @@ export function KBEditor() {
|
|
|
577
1045
|
/>
|
|
578
1046
|
|
|
579
1047
|
{/* AI Edit Prompt + slash hint */}
|
|
580
|
-
<div className="max-w-
|
|
1048
|
+
<div className="max-w-[var(--editor-max-w,48rem)] mx-auto px-8 pb-8 flex items-center gap-4">
|
|
581
1049
|
<button
|
|
582
1050
|
onClick={handleOpenAI}
|
|
583
1051
|
className="group flex items-center gap-2 text-[13px] text-muted-foreground/50 hover:text-muted-foreground transition-colors cursor-pointer"
|
|
@@ -618,11 +1086,46 @@ export function KBEditor() {
|
|
|
618
1086
|
</kbd>{" "}
|
|
619
1087
|
commands
|
|
620
1088
|
</span>
|
|
621
|
-
<
|
|
622
|
-
{
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
1089
|
+
<div className="flex items-center gap-3">
|
|
1090
|
+
{/* Mode toggle */}
|
|
1091
|
+
<div
|
|
1092
|
+
className="flex items-center rounded-md border border-border overflow-hidden text-[10.5px]"
|
|
1093
|
+
role="radiogroup"
|
|
1094
|
+
aria-label="Edit mode"
|
|
1095
|
+
>
|
|
1096
|
+
<button
|
|
1097
|
+
type="button"
|
|
1098
|
+
role="radio"
|
|
1099
|
+
aria-checked={editMode === "editing"}
|
|
1100
|
+
onClick={() => setEditMode("editing")}
|
|
1101
|
+
className={`px-2 py-0.5 transition-colors ${
|
|
1102
|
+
editMode === "editing"
|
|
1103
|
+
? "bg-primary text-primary-foreground"
|
|
1104
|
+
: "text-muted-foreground hover:bg-accent"
|
|
1105
|
+
}`}
|
|
1106
|
+
>
|
|
1107
|
+
Editing
|
|
1108
|
+
</button>
|
|
1109
|
+
<button
|
|
1110
|
+
type="button"
|
|
1111
|
+
role="radio"
|
|
1112
|
+
aria-checked={editMode === "suggesting"}
|
|
1113
|
+
onClick={() => setEditMode("suggesting")}
|
|
1114
|
+
className={`px-2 py-0.5 transition-colors ${
|
|
1115
|
+
editMode === "suggesting"
|
|
1116
|
+
? "bg-primary text-primary-foreground"
|
|
1117
|
+
: "text-muted-foreground hover:bg-accent"
|
|
1118
|
+
}`}
|
|
1119
|
+
>
|
|
1120
|
+
Suggesting
|
|
1121
|
+
</button>
|
|
1122
|
+
</div>
|
|
1123
|
+
<span>
|
|
1124
|
+
{saveStatus === "saving" && "Saving..."}
|
|
1125
|
+
{saveStatus === "saved" && "Saved"}
|
|
1126
|
+
{saveStatus === "error" && "Save failed"}
|
|
1127
|
+
</span>
|
|
1128
|
+
</div>
|
|
626
1129
|
</div>
|
|
627
1130
|
</>
|
|
628
1131
|
)}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Mark, mergeAttributes } from "@tiptap/core";
|
|
2
|
+
|
|
3
|
+
export const ProofSpan = Mark.create({
|
|
4
|
+
name: "proofSpan",
|
|
5
|
+
priority: 900,
|
|
6
|
+
inclusive: false,
|
|
7
|
+
keepOnSplit: false,
|
|
8
|
+
|
|
9
|
+
addAttributes() {
|
|
10
|
+
return {
|
|
11
|
+
spanId: {
|
|
12
|
+
default: null,
|
|
13
|
+
parseHTML: (el) => el.getAttribute("id"),
|
|
14
|
+
renderHTML: (a) => ({ id: a.spanId }),
|
|
15
|
+
},
|
|
16
|
+
origin: {
|
|
17
|
+
default: "ai",
|
|
18
|
+
parseHTML: (el) => el.getAttribute("origin"),
|
|
19
|
+
renderHTML: (a) => ({ origin: a.origin }),
|
|
20
|
+
},
|
|
21
|
+
basis: {
|
|
22
|
+
default: null,
|
|
23
|
+
parseHTML: (el) => el.getAttribute("basis"),
|
|
24
|
+
renderHTML: (a) => ({ basis: a.basis }),
|
|
25
|
+
},
|
|
26
|
+
basisDetail: {
|
|
27
|
+
default: null,
|
|
28
|
+
parseHTML: (el) => el.getAttribute("basis-detail"),
|
|
29
|
+
renderHTML: (a) => ({ "basis-detail": a.basisDetail }),
|
|
30
|
+
},
|
|
31
|
+
by: {
|
|
32
|
+
default: null,
|
|
33
|
+
parseHTML: (el) => el.getAttribute("by"),
|
|
34
|
+
renderHTML: (a) => ({ by: a.by }),
|
|
35
|
+
},
|
|
36
|
+
at: {
|
|
37
|
+
default: null,
|
|
38
|
+
parseHTML: (el) => el.getAttribute("at"),
|
|
39
|
+
renderHTML: (a) => ({ at: a.at }),
|
|
40
|
+
},
|
|
41
|
+
inResponseTo: {
|
|
42
|
+
default: null,
|
|
43
|
+
parseHTML: (el) => el.getAttribute("in-response-to"),
|
|
44
|
+
renderHTML: (a) => ({ "in-response-to": a.inResponseTo }),
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
parseHTML() {
|
|
50
|
+
return [{ tag: "proof-span" }];
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
renderHTML({ HTMLAttributes }) {
|
|
54
|
+
return [
|
|
55
|
+
"proof-span",
|
|
56
|
+
mergeAttributes(HTMLAttributes, { class: "proof-span" }),
|
|
57
|
+
0,
|
|
58
|
+
];
|
|
59
|
+
},
|
|
60
|
+
});
|
|
@@ -28,6 +28,7 @@ import { createLowlight } from "lowlight";
|
|
|
28
28
|
import { CalloutExtension } from "./callout-extension";
|
|
29
29
|
import { DragHandle } from "./extensions/drag-handle";
|
|
30
30
|
import { HeadingAnchors } from "./extensions/heading-anchors";
|
|
31
|
+
import { ProofSpan } from "./extensions/proof-span";
|
|
31
32
|
import { ResizableImage } from "./extensions/resizable-image";
|
|
32
33
|
import { WikiLink } from "./wiki-link-extension";
|
|
33
34
|
|
|
@@ -139,6 +140,7 @@ export const editorExtensions = [
|
|
|
139
140
|
},
|
|
140
141
|
}),
|
|
141
142
|
CalloutExtension,
|
|
143
|
+
ProofSpan,
|
|
142
144
|
TextAlign.configure({ types: ["heading", "paragraph"] }),
|
|
143
145
|
Subscript,
|
|
144
146
|
Superscript,
|