Skip to content

Commit 8042603

Browse files
committed
fragmentUtil
1 parent 39a3fd5 commit 8042603

File tree

4 files changed

+112
-32
lines changed

4 files changed

+112
-32
lines changed

packages/xl-ai/src/prosemirror/agent.ts

Lines changed: 7 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { UnreachableCaseError } from "@blocknote/core";
2-
import { Fragment, Node, Schema, Slice } from "prosemirror-model";
3-
import { AllSelection, TextSelection, Transaction } from "prosemirror-state";
2+
import { Node, Schema, Slice } from "prosemirror-model";
3+
import { TextSelection, Transaction } from "prosemirror-state";
44
import {
55
ReplaceAroundStep,
66
ReplaceStep,
77
Step,
88
Transform,
99
} from "prosemirror-transform";
10+
import { getFirstChar } from "./fragmentUtil.js";
1011

1112
export type AgentStep = {
1213
prosemirrorSteps: Step[];
@@ -235,39 +236,14 @@ export function getStepsAsAgent(doc: Node, pmSchema: Schema, steps: Step[]) {
235236
return agentSteps;
236237
}
237238

238-
/**
239-
* helper method to get the index of the first character of a fragment
240-
*/
241-
function getFirstChar(fragment: Fragment) {
242-
let index = 0;
243-
for (const content of fragment.content) {
244-
if (content.isText) {
245-
return index;
246-
}
247-
const sel = TextSelection.atStart(content);
248-
if (sel instanceof AllSelection) {
249-
// no text position found
250-
index += content.nodeSize;
251-
continue;
252-
}
253-
index += sel.head;
254-
if (!content.isLeaf) {
255-
// for regular nodes, add 1 position for the node opening
256-
// (annoyingly TextSelection.atStart doesn't account for this)
257-
index += 1;
258-
}
259-
return index;
260-
}
261-
return undefined;
262-
}
263-
264239
export async function delayAgentStep(step: AgentStep) {
240+
const jitter = Math.random() * 0.3 + 0.85; // Random between 0.85 and 1.15
265241
if (step.type === "select") {
266-
await new Promise((resolve) => setTimeout(resolve, 100));
242+
await new Promise((resolve) => setTimeout(resolve, 100 * jitter));
267243
} else if (step.type === "insert") {
268-
await new Promise((resolve) => setTimeout(resolve, 10));
244+
await new Promise((resolve) => setTimeout(resolve, 10 * jitter));
269245
} else if (step.type === "replace") {
270-
await new Promise((resolve) => setTimeout(resolve, 200));
246+
await new Promise((resolve) => setTimeout(resolve, 200 * jitter));
271247
} else {
272248
throw new UnreachableCaseError(step.type);
273249
}

packages/xl-ai/src/prosemirror/changeset.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,6 @@ const createEncoder = (doc: Node, updatedDoc: Node) => {
162162
if (tableCells.has(str)) {
163163
return str;
164164
}
165-
// return Math.random();
166165
return -1;
167166
}
168167
return -1;
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { Fragment, Schema } from "prosemirror-model";
2+
import { describe, expect, it } from "vitest";
3+
import { getFirstChar } from "./fragmentUtil.js";
4+
5+
describe("fragmentUtil", () => {
6+
describe("getFirstChar", () => {
7+
// Create a more complex schema for testing
8+
const schema = new Schema({
9+
nodes: {
10+
doc: { content: "block+" },
11+
paragraph: { content: "inline*", group: "block" },
12+
heading: {
13+
content: "inline*",
14+
group: "block",
15+
attrs: { level: { default: 1 } },
16+
},
17+
blockcontainer: { content: "block+", group: "block" },
18+
text: { group: "inline" },
19+
},
20+
});
21+
22+
it("should return 0 for a text fragment", () => {
23+
const fragment = Fragment.from(schema.text("Hello"));
24+
expect(getFirstChar(fragment)).toBe(0);
25+
});
26+
27+
it("should return correct index for a paragraph with text", () => {
28+
const paragraph = schema.node("paragraph", null, [schema.text("Hello")]);
29+
const fragment = Fragment.from(paragraph);
30+
expect(getFirstChar(fragment)).toBe(1);
31+
});
32+
33+
it("should return undefined for an empty fragment", () => {
34+
const fragment = Fragment.empty;
35+
expect(getFirstChar(fragment)).toBe(undefined);
36+
});
37+
38+
it("should handle nested nodes correctly", () => {
39+
const paragraph = schema.node("paragraph", null, [
40+
schema.text("Hello"),
41+
schema.text(" World"),
42+
]);
43+
const fragment = Fragment.from(paragraph);
44+
expect(getFirstChar(fragment)).toBe(1);
45+
});
46+
47+
it("should handle blockcontainer with nested paragraph", () => {
48+
const paragraph = schema.node("paragraph", null, [schema.text("Hello")]);
49+
const blockcontainer = schema.node("blockcontainer", null, [paragraph]);
50+
const fragment = Fragment.from(blockcontainer);
51+
// Blockquote opening (1) + paragraph opening (1) = 2
52+
expect(getFirstChar(fragment)).toBe(2);
53+
});
54+
55+
it("should handle heading with text", () => {
56+
const heading = schema.node("heading", { level: 2 }, [
57+
schema.text("Title"),
58+
]);
59+
const fragment = Fragment.from(heading);
60+
expect(getFirstChar(fragment)).toBe(1);
61+
});
62+
63+
it("should handle multiple block nodes", () => {
64+
const paragraph1 = schema.node("paragraph", null, [schema.text("First")]);
65+
const paragraph2 = schema.node("paragraph", null, [
66+
schema.text("Second"),
67+
]);
68+
const fragment = Fragment.from([paragraph1, paragraph2]);
69+
expect(getFirstChar(fragment)).toBe(1);
70+
});
71+
72+
it("should handle deeply nested structure", () => {
73+
const text = schema.text("Deep text");
74+
const paragraph = schema.node("paragraph", null, [text]);
75+
const blockcontainer = schema.node("blockcontainer", null, [paragraph]);
76+
const blockcontainer2 = schema.node("blockcontainer", null, [
77+
blockcontainer,
78+
]);
79+
80+
const fragment = Fragment.from(blockcontainer2);
81+
// blockcontainer + blockcontainer + paragraph (1) = 3
82+
expect(getFirstChar(fragment)).toBe(3);
83+
});
84+
});
85+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Fragment } from "prosemirror-model";
2+
3+
/**
4+
* helper method to get the index of the first character of a fragment
5+
*/
6+
export function getFirstChar(fragment: Fragment) {
7+
let index: number | undefined = undefined;
8+
let found = false;
9+
fragment.descendants((n, pos) => {
10+
if (found) {
11+
return false;
12+
}
13+
if (n.isText) {
14+
found = true;
15+
index = pos;
16+
}
17+
return true;
18+
});
19+
return index;
20+
}

0 commit comments

Comments
 (0)