@lalternative/editor 0.1.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.
- package/dist/chunk-7GOSVZTI.js +114 -0
- package/dist/chunk-7GOSVZTI.js.map +1 -0
- package/dist/editor.css +32 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/revisions.d.ts +63 -0
- package/dist/revisions.js +111 -0
- package/dist/revisions.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// src/document.ts
|
|
2
|
+
function blocksFromDoc(doc) {
|
|
3
|
+
return (doc.content ?? []).map(textOf).filter((text) => text.trim() !== "");
|
|
4
|
+
}
|
|
5
|
+
function shapesFromDoc(doc) {
|
|
6
|
+
return (doc.content ?? []).filter((node) => textOf(node).trim() !== "").map(
|
|
7
|
+
(node) => node.type === "heading" ? { type: "heading", level: Number(node.attrs?.level ?? 2) } : { type: "paragraph" }
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
function docFromBlocks(blocks, shapes = []) {
|
|
11
|
+
const content = [];
|
|
12
|
+
blocks.forEach((block, i) => {
|
|
13
|
+
const text = block.trim();
|
|
14
|
+
if (text === "") return;
|
|
15
|
+
const shape = shapes[i];
|
|
16
|
+
if (shape?.type === "heading") {
|
|
17
|
+
content.push({
|
|
18
|
+
type: "heading",
|
|
19
|
+
attrs: { level: shape.level ?? 2 },
|
|
20
|
+
content: [{ type: "text", text }]
|
|
21
|
+
});
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
content.push({ type: "paragraph", content: [{ type: "text", text }] });
|
|
25
|
+
});
|
|
26
|
+
return { type: "doc", content };
|
|
27
|
+
}
|
|
28
|
+
function textOf(node) {
|
|
29
|
+
if (typeof node.text === "string") return node.text;
|
|
30
|
+
return (node.content ?? []).map(textOf).join("");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/diff.ts
|
|
34
|
+
var PAIR_THRESHOLD = 0.34;
|
|
35
|
+
function diffBlocks(before, after) {
|
|
36
|
+
const common = longestCommonSubsequence(before, after);
|
|
37
|
+
const changes = [];
|
|
38
|
+
let i = 0;
|
|
39
|
+
let j = 0;
|
|
40
|
+
let seq = 0;
|
|
41
|
+
const id = () => `c${seq++}`;
|
|
42
|
+
const flush = (removed, added) => {
|
|
43
|
+
const balanced = removed.length === added.length;
|
|
44
|
+
const paired = Math.min(removed.length, added.length);
|
|
45
|
+
let p = 0;
|
|
46
|
+
for (; p < paired; p++) {
|
|
47
|
+
if (!balanced && similarity(removed[p], added[p]) < PAIR_THRESHOLD) break;
|
|
48
|
+
changes.push({ id: id(), kind: "replace", before: removed[p], after: added[p] });
|
|
49
|
+
}
|
|
50
|
+
for (let r = p; r < removed.length; r++) {
|
|
51
|
+
changes.push({ id: id(), kind: "delete", before: removed[r], after: "" });
|
|
52
|
+
}
|
|
53
|
+
for (let a = p; a < added.length; a++) {
|
|
54
|
+
changes.push({ id: id(), kind: "insert", before: "", after: added[a] });
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
for (const anchor of common) {
|
|
58
|
+
const removed = [];
|
|
59
|
+
const added = [];
|
|
60
|
+
while (i < before.length && before[i] !== anchor) removed.push(before[i++]);
|
|
61
|
+
while (j < after.length && after[j] !== anchor) added.push(after[j++]);
|
|
62
|
+
flush(removed, added);
|
|
63
|
+
changes.push({ id: id(), kind: "keep", before: anchor, after: anchor });
|
|
64
|
+
i++;
|
|
65
|
+
j++;
|
|
66
|
+
}
|
|
67
|
+
flush(before.slice(i), after.slice(j));
|
|
68
|
+
return changes;
|
|
69
|
+
}
|
|
70
|
+
function longestCommonSubsequence(a, b) {
|
|
71
|
+
const table = Array.from(
|
|
72
|
+
{ length: a.length + 1 },
|
|
73
|
+
() => new Array(b.length + 1).fill(0)
|
|
74
|
+
);
|
|
75
|
+
for (let i2 = a.length - 1; i2 >= 0; i2--) {
|
|
76
|
+
for (let j2 = b.length - 1; j2 >= 0; j2--) {
|
|
77
|
+
table[i2][j2] = a[i2] === b[j2] ? table[i2 + 1][j2 + 1] + 1 : Math.max(table[i2 + 1][j2], table[i2][j2 + 1]);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const common = [];
|
|
81
|
+
let i = 0;
|
|
82
|
+
let j = 0;
|
|
83
|
+
while (i < a.length && j < b.length) {
|
|
84
|
+
if (a[i] === b[j]) {
|
|
85
|
+
common.push(a[i]);
|
|
86
|
+
i++;
|
|
87
|
+
j++;
|
|
88
|
+
} else if (table[i + 1][j] >= table[i][j + 1]) {
|
|
89
|
+
i++;
|
|
90
|
+
} else {
|
|
91
|
+
j++;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return common;
|
|
95
|
+
}
|
|
96
|
+
function similarity(a, b) {
|
|
97
|
+
const left = words(a);
|
|
98
|
+
const right = words(b);
|
|
99
|
+
if (left.size === 0 || right.size === 0) return 0;
|
|
100
|
+
let shared = 0;
|
|
101
|
+
for (const word of left) if (right.has(word)) shared++;
|
|
102
|
+
return shared / Math.max(left.size, right.size);
|
|
103
|
+
}
|
|
104
|
+
function words(s) {
|
|
105
|
+
return new Set(s.toLowerCase().match(/\p{L}+/gu) ?? []);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export {
|
|
109
|
+
blocksFromDoc,
|
|
110
|
+
shapesFromDoc,
|
|
111
|
+
docFromBlocks,
|
|
112
|
+
diffBlocks
|
|
113
|
+
};
|
|
114
|
+
//# sourceMappingURL=chunk-7GOSVZTI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/document.ts","../src/diff.ts"],"sourcesContent":["/**\n * RichNode is one node of a TipTap document.\n *\n * The index signature is what lets a document cross the boundary with an\n * editor handle typed as a plain object, which is how TipTap hands its JSON\n * back. Without it every call site pays for the mismatch in casts.\n */\nexport interface RichNode {\n type?: string;\n attrs?: Record<string, unknown>;\n content?: RichNode[];\n text?: string;\n [key: string]: unknown;\n}\n\nexport type RichDoc = RichNode;\n\nexport interface BlockShape {\n type: \"paragraph\" | \"heading\";\n level?: number;\n}\n\n/**\n * blocksFromDoc flattens a TipTap document to the text of its top-level nodes.\n *\n * Marks are dropped rather than encoded: the diff compares prose, and a bold\n * span that moved inside an otherwise identical sentence is not a change worth\n * asking the reviewer about.\n */\nexport function blocksFromDoc(doc: RichDoc): string[] {\n return (doc.content ?? []).map(textOf).filter((text) => text.trim() !== \"\");\n}\n\n/** shapesFromDoc records what each block was, so a revision can be rebuilt\n * without turning every heading into a paragraph. */\nexport function shapesFromDoc(doc: RichDoc): BlockShape[] {\n return (doc.content ?? [])\n .filter((node) => textOf(node).trim() !== \"\")\n .map((node) =>\n node.type === \"heading\"\n ? { type: \"heading\" as const, level: Number(node.attrs?.level ?? 2) }\n : { type: \"paragraph\" as const },\n );\n}\n\nexport function docFromBlocks(blocks: string[], shapes: BlockShape[] = []): RichDoc {\n const content: RichNode[] = [];\n blocks.forEach((block, i) => {\n const text = block.trim();\n if (text === \"\") return;\n\n const shape = shapes[i];\n if (shape?.type === \"heading\") {\n content.push({\n type: \"heading\",\n attrs: { level: shape.level ?? 2 },\n content: [{ type: \"text\", text }],\n });\n return;\n }\n content.push({ type: \"paragraph\", content: [{ type: \"text\", text }] });\n });\n return { type: \"doc\", content };\n}\n\nfunction textOf(node: RichNode): string {\n if (typeof node.text === \"string\") return node.text;\n return (node.content ?? []).map(textOf).join(\"\");\n}\n","export type ChangeKind = \"keep\" | \"insert\" | \"delete\" | \"replace\";\n\nexport interface BlockChange {\n id: string;\n kind: ChangeKind;\n before: string;\n after: string;\n}\n\n// Below this, two blocks are different things rather than one rewritten twice,\n// and pairing them would hide a deletion behind an unrelated insertion.\nconst PAIR_THRESHOLD = 0.34;\n\n/**\n * diffBlocks compares two documents one block at a time.\n *\n * The block is the unit because it is the unit a reviewer accepts or rejects:\n * a finer diff turns one editorial decision into a dozen clicks, and a coarser\n * one makes rejecting a single sentence cost the whole section.\n *\n * Accepting every change rebuilds `after` exactly; rejecting every change\n * rebuilds `before` exactly. The UI relies on both.\n */\nexport function diffBlocks(before: string[], after: string[]): BlockChange[] {\n const common = longestCommonSubsequence(before, after);\n\n const changes: BlockChange[] = [];\n let i = 0;\n let j = 0;\n let seq = 0;\n const id = () => `c${seq++}`;\n\n const flush = (removed: string[], added: string[]) => {\n // Blocks that vanish and appear at the same place between two unchanged\n // anchors are one rewrite, and the reviewer wants one decision rather than\n // two that can be answered inconsistently. When the counts differ, the\n // extras are genuine insertions or deletions, so only pair the ones that\n // read as rewrites of each other.\n const balanced = removed.length === added.length;\n const paired = Math.min(removed.length, added.length);\n let p = 0;\n for (; p < paired; p++) {\n if (!balanced && similarity(removed[p], added[p]) < PAIR_THRESHOLD) break;\n changes.push({ id: id(), kind: \"replace\", before: removed[p], after: added[p] });\n }\n for (let r = p; r < removed.length; r++) {\n changes.push({ id: id(), kind: \"delete\", before: removed[r], after: \"\" });\n }\n for (let a = p; a < added.length; a++) {\n changes.push({ id: id(), kind: \"insert\", before: \"\", after: added[a] });\n }\n };\n\n for (const anchor of common) {\n const removed: string[] = [];\n const added: string[] = [];\n while (i < before.length && before[i] !== anchor) removed.push(before[i++]);\n while (j < after.length && after[j] !== anchor) added.push(after[j++]);\n flush(removed, added);\n changes.push({ id: id(), kind: \"keep\", before: anchor, after: anchor });\n i++;\n j++;\n }\n flush(before.slice(i), after.slice(j));\n\n return changes;\n}\n\nfunction longestCommonSubsequence(a: string[], b: string[]): string[] {\n const table: number[][] = Array.from({ length: a.length + 1 }, () =>\n new Array<number>(b.length + 1).fill(0),\n );\n for (let i = a.length - 1; i >= 0; i--) {\n for (let j = b.length - 1; j >= 0; j--) {\n table[i][j] =\n a[i] === b[j] ? table[i + 1][j + 1] + 1 : Math.max(table[i + 1][j], table[i][j + 1]);\n }\n }\n\n const common: string[] = [];\n let i = 0;\n let j = 0;\n while (i < a.length && j < b.length) {\n if (a[i] === b[j]) {\n common.push(a[i]);\n i++;\n j++;\n } else if (table[i + 1][j] >= table[i][j + 1]) {\n i++;\n } else {\n j++;\n }\n }\n return common;\n}\n\n// Word overlap rather than character distance: an LLM rewrite keeps most of the\n// vocabulary and reorders it, which edit distance scores as a near-total change.\nfunction similarity(a: string, b: string): number {\n const left = words(a);\n const right = words(b);\n if (left.size === 0 || right.size === 0) return 0;\n\n let shared = 0;\n for (const word of left) if (right.has(word)) shared++;\n return shared / Math.max(left.size, right.size);\n}\n\nfunction words(s: string): Set<string> {\n return new Set(s.toLowerCase().match(/\\p{L}+/gu) ?? []);\n}\n"],"mappings":"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","names":["i","j"]}
|
package/dist/editor.css
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/* Colours are variables so a host theme can override them without forking the
|
|
2
|
+
package. The defaults read on both a light and a dark background. */
|
|
3
|
+
:root {
|
|
4
|
+
--lalt-editor-insert: #1d4ed8;
|
|
5
|
+
--lalt-editor-insert-bg: rgb(29 78 216 / 0.08);
|
|
6
|
+
--lalt-editor-delete: #b91c1c;
|
|
7
|
+
--lalt-editor-delete-bg: rgb(185 28 28 / 0.06);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.lalt-revision {
|
|
11
|
+
display: block;
|
|
12
|
+
border-radius: 0.375rem;
|
|
13
|
+
padding: 0.125rem 0.25rem;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.lalt-revision--insert {
|
|
17
|
+
color: var(--lalt-editor-insert);
|
|
18
|
+
background: var(--lalt-editor-insert-bg);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.lalt-revision--delete {
|
|
22
|
+
color: var(--lalt-editor-delete);
|
|
23
|
+
background: var(--lalt-editor-delete-bg);
|
|
24
|
+
text-decoration: line-through;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* A decided suggestion stops competing for attention but stays visible, so the
|
|
28
|
+
reviewer can see what they already answered without re-reading the diff. */
|
|
29
|
+
.lalt-revision--decided {
|
|
30
|
+
opacity: 0.45;
|
|
31
|
+
background: none;
|
|
32
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RichNode is one node of a TipTap document.
|
|
3
|
+
*
|
|
4
|
+
* The index signature is what lets a document cross the boundary with an
|
|
5
|
+
* editor handle typed as a plain object, which is how TipTap hands its JSON
|
|
6
|
+
* back. Without it every call site pays for the mismatch in casts.
|
|
7
|
+
*/
|
|
8
|
+
interface RichNode {
|
|
9
|
+
type?: string;
|
|
10
|
+
attrs?: Record<string, unknown>;
|
|
11
|
+
content?: RichNode[];
|
|
12
|
+
text?: string;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
type RichDoc = RichNode;
|
|
16
|
+
interface BlockShape {
|
|
17
|
+
type: "paragraph" | "heading";
|
|
18
|
+
level?: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* blocksFromDoc flattens a TipTap document to the text of its top-level nodes.
|
|
22
|
+
*
|
|
23
|
+
* Marks are dropped rather than encoded: the diff compares prose, and a bold
|
|
24
|
+
* span that moved inside an otherwise identical sentence is not a change worth
|
|
25
|
+
* asking the reviewer about.
|
|
26
|
+
*/
|
|
27
|
+
declare function blocksFromDoc(doc: RichDoc): string[];
|
|
28
|
+
/** shapesFromDoc records what each block was, so a revision can be rebuilt
|
|
29
|
+
* without turning every heading into a paragraph. */
|
|
30
|
+
declare function shapesFromDoc(doc: RichDoc): BlockShape[];
|
|
31
|
+
declare function docFromBlocks(blocks: string[], shapes?: BlockShape[]): RichDoc;
|
|
32
|
+
|
|
33
|
+
type ChangeKind = "keep" | "insert" | "delete" | "replace";
|
|
34
|
+
interface BlockChange {
|
|
35
|
+
id: string;
|
|
36
|
+
kind: ChangeKind;
|
|
37
|
+
before: string;
|
|
38
|
+
after: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* diffBlocks compares two documents one block at a time.
|
|
42
|
+
*
|
|
43
|
+
* The block is the unit because it is the unit a reviewer accepts or rejects:
|
|
44
|
+
* a finer diff turns one editorial decision into a dozen clicks, and a coarser
|
|
45
|
+
* one makes rejecting a single sentence cost the whole section.
|
|
46
|
+
*
|
|
47
|
+
* Accepting every change rebuilds `after` exactly; rejecting every change
|
|
48
|
+
* rebuilds `before` exactly. The UI relies on both.
|
|
49
|
+
*/
|
|
50
|
+
declare function diffBlocks(before: string[], after: string[]): BlockChange[];
|
|
51
|
+
|
|
52
|
+
export { type BlockChange, type BlockShape, type ChangeKind, type RichDoc, type RichNode, blocksFromDoc, diffBlocks, docFromBlocks, shapesFromDoc };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { BlockChange, RichDoc } from './index.js';
|
|
2
|
+
export { ChangeKind } from './index.js';
|
|
3
|
+
|
|
4
|
+
type Decision = "pending" | "accepted" | "rejected";
|
|
5
|
+
interface RevisionState {
|
|
6
|
+
changes: BlockChange[];
|
|
7
|
+
decisions: Record<string, Decision>;
|
|
8
|
+
pending: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* openRevision starts a review of what the model returned.
|
|
12
|
+
*
|
|
13
|
+
* Nothing is applied yet: the document the user sees keeps reading as it did,
|
|
14
|
+
* with the proposals shown beside it, until they decide one at a time.
|
|
15
|
+
*/
|
|
16
|
+
declare function openRevision(before: string[], after: string[]): RevisionState;
|
|
17
|
+
declare function accept(state: RevisionState, id: string): RevisionState;
|
|
18
|
+
declare function reject(state: RevisionState, id: string): RevisionState;
|
|
19
|
+
declare function acceptAll(state: RevisionState): RevisionState;
|
|
20
|
+
declare function rejectAll(state: RevisionState): RevisionState;
|
|
21
|
+
/**
|
|
22
|
+
* resolvedBlocks is the document as it currently stands, counting every
|
|
23
|
+
* decision made so far and leaving undecided changes at their original text.
|
|
24
|
+
*
|
|
25
|
+
* It is always a valid document, so the editor can save mid-review rather than
|
|
26
|
+
* forcing the user to finish before they are allowed to keep their work.
|
|
27
|
+
*/
|
|
28
|
+
declare function resolvedBlocks(state: RevisionState): string[];
|
|
29
|
+
/**
|
|
30
|
+
* resolved is resolvedBlocks with the origin of each block.
|
|
31
|
+
*
|
|
32
|
+
* `sourceIndex` is where the block sat in the document under review, or -1 for
|
|
33
|
+
* one the model added. A caller restoring block shapes needs it: accepting an
|
|
34
|
+
* insertion shifts every later block, so positions in the result no longer
|
|
35
|
+
* line up with positions in the source.
|
|
36
|
+
*/
|
|
37
|
+
declare function resolved(state: RevisionState): Array<{
|
|
38
|
+
text: string;
|
|
39
|
+
sourceIndex: number;
|
|
40
|
+
}>;
|
|
41
|
+
|
|
42
|
+
interface UseRevisions {
|
|
43
|
+
state: RevisionState | null;
|
|
44
|
+
/** The document as it stands, counting the decisions made so far. */
|
|
45
|
+
doc: RichDoc | null;
|
|
46
|
+
pending: number;
|
|
47
|
+
open: (revised: string[]) => void;
|
|
48
|
+
accept: (id: string) => void;
|
|
49
|
+
reject: (id: string) => void;
|
|
50
|
+
acceptAll: () => void;
|
|
51
|
+
rejectAll: () => void;
|
|
52
|
+
/** Ends the review, keeping whatever `doc` currently reads as. */
|
|
53
|
+
close: () => void;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* useRevisions holds one review of what a model proposed.
|
|
57
|
+
*
|
|
58
|
+
* The suggestions never enter the document: they live here, beside it, so the
|
|
59
|
+
* editor's own content stays valid and saveable at every point of the review.
|
|
60
|
+
*/
|
|
61
|
+
declare function useRevisions(source: RichDoc | null): UseRevisions;
|
|
62
|
+
|
|
63
|
+
export { BlockChange, type Decision, type RevisionState, type UseRevisions, accept, acceptAll, openRevision, reject, rejectAll, resolved, resolvedBlocks, useRevisions };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {
|
|
2
|
+
blocksFromDoc,
|
|
3
|
+
diffBlocks,
|
|
4
|
+
docFromBlocks,
|
|
5
|
+
shapesFromDoc
|
|
6
|
+
} from "./chunk-7GOSVZTI.js";
|
|
7
|
+
|
|
8
|
+
// src/revision-state.ts
|
|
9
|
+
function openRevision(before, after) {
|
|
10
|
+
const changes = diffBlocks(before, after);
|
|
11
|
+
const decisions = {};
|
|
12
|
+
for (const change of changes) {
|
|
13
|
+
decisions[change.id] = change.kind === "keep" ? "accepted" : "pending";
|
|
14
|
+
}
|
|
15
|
+
return { changes, decisions, pending: countPending(changes, decisions) };
|
|
16
|
+
}
|
|
17
|
+
function accept(state, id) {
|
|
18
|
+
return decide(state, id, "accepted");
|
|
19
|
+
}
|
|
20
|
+
function reject(state, id) {
|
|
21
|
+
return decide(state, id, "rejected");
|
|
22
|
+
}
|
|
23
|
+
function acceptAll(state) {
|
|
24
|
+
return decideAll(state, "accepted");
|
|
25
|
+
}
|
|
26
|
+
function rejectAll(state) {
|
|
27
|
+
return decideAll(state, "rejected");
|
|
28
|
+
}
|
|
29
|
+
function resolvedBlocks(state) {
|
|
30
|
+
return resolved(state).map((entry) => entry.text);
|
|
31
|
+
}
|
|
32
|
+
function resolved(state) {
|
|
33
|
+
const blocks = [];
|
|
34
|
+
let source = 0;
|
|
35
|
+
for (const change of state.changes) {
|
|
36
|
+
const decided = state.decisions[change.id] === "accepted";
|
|
37
|
+
switch (change.kind) {
|
|
38
|
+
case "keep":
|
|
39
|
+
blocks.push({ text: change.after, sourceIndex: source++ });
|
|
40
|
+
break;
|
|
41
|
+
case "insert":
|
|
42
|
+
if (decided) blocks.push({ text: change.after, sourceIndex: -1 });
|
|
43
|
+
break;
|
|
44
|
+
case "delete":
|
|
45
|
+
if (!decided) blocks.push({ text: change.before, sourceIndex: source });
|
|
46
|
+
source++;
|
|
47
|
+
break;
|
|
48
|
+
case "replace":
|
|
49
|
+
blocks.push({ text: decided ? change.after : change.before, sourceIndex: source++ });
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return blocks;
|
|
54
|
+
}
|
|
55
|
+
function decide(state, id, decision) {
|
|
56
|
+
if (!(id in state.decisions) || state.decisions[id] === decision) return state;
|
|
57
|
+
const decisions = { ...state.decisions, [id]: decision };
|
|
58
|
+
return { ...state, decisions, pending: countPending(state.changes, decisions) };
|
|
59
|
+
}
|
|
60
|
+
function decideAll(state, decision) {
|
|
61
|
+
const decisions = {};
|
|
62
|
+
for (const change of state.changes) {
|
|
63
|
+
decisions[change.id] = change.kind === "keep" ? "accepted" : decision;
|
|
64
|
+
}
|
|
65
|
+
return { ...state, decisions, pending: 0 };
|
|
66
|
+
}
|
|
67
|
+
function countPending(changes, decisions) {
|
|
68
|
+
return changes.filter((c) => c.kind !== "keep" && decisions[c.id] === "pending").length;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/useRevisions.ts
|
|
72
|
+
import { useCallback, useMemo, useState } from "react";
|
|
73
|
+
function useRevisions(source) {
|
|
74
|
+
const [state, setState] = useState(null);
|
|
75
|
+
const original = useMemo(() => source ? blocksFromDoc(source) : [], [source]);
|
|
76
|
+
const shapes = useMemo(() => source ? shapesFromDoc(source) : [], [source]);
|
|
77
|
+
const open = useCallback(
|
|
78
|
+
(revised) => setState(openRevision(original, revised)),
|
|
79
|
+
[original]
|
|
80
|
+
);
|
|
81
|
+
const doc = useMemo(() => {
|
|
82
|
+
if (!state) return null;
|
|
83
|
+
const entries = resolved(state);
|
|
84
|
+
return docFromBlocks(
|
|
85
|
+
entries.map((entry) => entry.text),
|
|
86
|
+
entries.map((entry) => shapes[entry.sourceIndex] ?? { type: "paragraph" })
|
|
87
|
+
);
|
|
88
|
+
}, [state, shapes]);
|
|
89
|
+
return {
|
|
90
|
+
state,
|
|
91
|
+
doc,
|
|
92
|
+
pending: state?.pending ?? 0,
|
|
93
|
+
open,
|
|
94
|
+
accept: useCallback((id) => setState((s) => s ? accept(s, id) : s), []),
|
|
95
|
+
reject: useCallback((id) => setState((s) => s ? reject(s, id) : s), []),
|
|
96
|
+
acceptAll: useCallback(() => setState((s) => s ? acceptAll(s) : s), []),
|
|
97
|
+
rejectAll: useCallback(() => setState((s) => s ? rejectAll(s) : s), []),
|
|
98
|
+
close: useCallback(() => setState(null), [])
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export {
|
|
102
|
+
accept,
|
|
103
|
+
acceptAll,
|
|
104
|
+
openRevision,
|
|
105
|
+
reject,
|
|
106
|
+
rejectAll,
|
|
107
|
+
resolved,
|
|
108
|
+
resolvedBlocks,
|
|
109
|
+
useRevisions
|
|
110
|
+
};
|
|
111
|
+
//# sourceMappingURL=revisions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/revision-state.ts","../src/useRevisions.ts"],"sourcesContent":["import { diffBlocks, type BlockChange, type ChangeKind } from \"./diff\";\n\nexport type { BlockChange, ChangeKind };\n\nexport type Decision = \"pending\" | \"accepted\" | \"rejected\";\n\nexport interface RevisionState {\n changes: BlockChange[];\n decisions: Record<string, Decision>;\n pending: number;\n}\n\n/**\n * openRevision starts a review of what the model returned.\n *\n * Nothing is applied yet: the document the user sees keeps reading as it did,\n * with the proposals shown beside it, until they decide one at a time.\n */\nexport function openRevision(before: string[], after: string[]): RevisionState {\n const changes = diffBlocks(before, after);\n const decisions: Record<string, Decision> = {};\n for (const change of changes) {\n decisions[change.id] = change.kind === \"keep\" ? \"accepted\" : \"pending\";\n }\n return { changes, decisions, pending: countPending(changes, decisions) };\n}\n\nexport function accept(state: RevisionState, id: string): RevisionState {\n return decide(state, id, \"accepted\");\n}\n\nexport function reject(state: RevisionState, id: string): RevisionState {\n return decide(state, id, \"rejected\");\n}\n\nexport function acceptAll(state: RevisionState): RevisionState {\n return decideAll(state, \"accepted\");\n}\n\nexport function rejectAll(state: RevisionState): RevisionState {\n return decideAll(state, \"rejected\");\n}\n\n/**\n * resolvedBlocks is the document as it currently stands, counting every\n * decision made so far and leaving undecided changes at their original text.\n *\n * It is always a valid document, so the editor can save mid-review rather than\n * forcing the user to finish before they are allowed to keep their work.\n */\nexport function resolvedBlocks(state: RevisionState): string[] {\n return resolved(state).map((entry) => entry.text);\n}\n\n/**\n * resolved is resolvedBlocks with the origin of each block.\n *\n * `sourceIndex` is where the block sat in the document under review, or -1 for\n * one the model added. A caller restoring block shapes needs it: accepting an\n * insertion shifts every later block, so positions in the result no longer\n * line up with positions in the source.\n */\nexport function resolved(state: RevisionState): Array<{ text: string; sourceIndex: number }> {\n const blocks: Array<{ text: string; sourceIndex: number }> = [];\n let source = 0;\n for (const change of state.changes) {\n const decided = state.decisions[change.id] === \"accepted\";\n switch (change.kind) {\n case \"keep\":\n blocks.push({ text: change.after, sourceIndex: source++ });\n break;\n case \"insert\":\n if (decided) blocks.push({ text: change.after, sourceIndex: -1 });\n break;\n case \"delete\":\n if (!decided) blocks.push({ text: change.before, sourceIndex: source });\n source++;\n break;\n case \"replace\":\n blocks.push({ text: decided ? change.after : change.before, sourceIndex: source++ });\n break;\n }\n }\n return blocks;\n}\n\nfunction decide(state: RevisionState, id: string, decision: Decision): RevisionState {\n if (!(id in state.decisions) || state.decisions[id] === decision) return state;\n\n const decisions = { ...state.decisions, [id]: decision };\n return { ...state, decisions, pending: countPending(state.changes, decisions) };\n}\n\nfunction decideAll(state: RevisionState, decision: Decision): RevisionState {\n const decisions: Record<string, Decision> = {};\n for (const change of state.changes) {\n decisions[change.id] = change.kind === \"keep\" ? \"accepted\" : decision;\n }\n return { ...state, decisions, pending: 0 };\n}\n\nfunction countPending(changes: BlockChange[], decisions: Record<string, Decision>): number {\n return changes.filter((c) => c.kind !== \"keep\" && decisions[c.id] === \"pending\").length;\n}\n","import { useCallback, useMemo, useState } from \"react\";\n\nimport { blocksFromDoc, docFromBlocks, shapesFromDoc, type RichDoc } from \"./document\";\nimport {\n accept,\n acceptAll,\n openRevision,\n reject,\n rejectAll,\n resolved,\n type RevisionState,\n} from \"./revision-state\";\n\nexport interface UseRevisions {\n state: RevisionState | null;\n /** The document as it stands, counting the decisions made so far. */\n doc: RichDoc | null;\n pending: number;\n open: (revised: string[]) => void;\n accept: (id: string) => void;\n reject: (id: string) => void;\n acceptAll: () => void;\n rejectAll: () => void;\n /** Ends the review, keeping whatever `doc` currently reads as. */\n close: () => void;\n}\n\n/**\n * useRevisions holds one review of what a model proposed.\n *\n * The suggestions never enter the document: they live here, beside it, so the\n * editor's own content stays valid and saveable at every point of the review.\n */\nexport function useRevisions(source: RichDoc | null): UseRevisions {\n const [state, setState] = useState<RevisionState | null>(null);\n\n const original = useMemo(() => (source ? blocksFromDoc(source) : []), [source]);\n const shapes = useMemo(() => (source ? shapesFromDoc(source) : []), [source]);\n\n const open = useCallback(\n (revised: string[]) => setState(openRevision(original, revised)),\n [original],\n );\n\n const doc = useMemo(() => {\n if (!state) return null;\n const entries = resolved(state);\n return docFromBlocks(\n entries.map((entry) => entry.text),\n entries.map((entry) => shapes[entry.sourceIndex] ?? { type: \"paragraph\" as const }),\n );\n }, [state, shapes]);\n\n return {\n state,\n doc,\n pending: state?.pending ?? 0,\n open,\n accept: useCallback((id: string) => setState((s) => (s ? accept(s, id) : s)), []),\n reject: useCallback((id: string) => setState((s) => (s ? reject(s, id) : s)), []),\n acceptAll: useCallback(() => setState((s) => (s ? acceptAll(s) : s)), []),\n rejectAll: useCallback(() => setState((s) => (s ? rejectAll(s) : s)), []),\n close: useCallback(() => setState(null), []),\n };\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lalternative/editor",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared TipTap editor for L'Alternative apps: rich text, format menu, selection toolbar and inline AI revisions",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"./revisions": {
|
|
14
|
+
"import": "./dist/revisions.js",
|
|
15
|
+
"types": "./dist/revisions.d.ts"
|
|
16
|
+
},
|
|
17
|
+
"./styles.css": "./dist/editor.css"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsup",
|
|
24
|
+
"dev": "tsup --watch",
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"test": "vitest run"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@tiptap/core": ">=3.0.0",
|
|
30
|
+
"@tiptap/pm": ">=3.0.0",
|
|
31
|
+
"@tiptap/react": ">=3.0.0",
|
|
32
|
+
"@tiptap/starter-kit": ">=3.0.0",
|
|
33
|
+
"react": ">=18.0.0",
|
|
34
|
+
"react-dom": ">=18.0.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@tiptap/core": "^3.29.2",
|
|
38
|
+
"@tiptap/pm": "^3.29.2",
|
|
39
|
+
"@tiptap/react": "^3.29.2",
|
|
40
|
+
"@tiptap/starter-kit": "^3.29.2",
|
|
41
|
+
"@types/react": "^19.0.0",
|
|
42
|
+
"@types/react-dom": "^19.0.0",
|
|
43
|
+
"react": "^19.0.0",
|
|
44
|
+
"react-dom": "^19.0.0",
|
|
45
|
+
"tsup": "^8.5.1",
|
|
46
|
+
"typescript": "^5.7.0",
|
|
47
|
+
"vitest": "^3.0.0"
|
|
48
|
+
},
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"registry": "https://registry.npmjs.org",
|
|
51
|
+
"access": "public"
|
|
52
|
+
},
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "https://github.com/lalternative/packages.git",
|
|
56
|
+
"directory": "editor"
|
|
57
|
+
}
|
|
58
|
+
}
|