feat: peg AI inline suggestions to granular text spans, add autosave revision history with restore, fix focus mode, and make PS logo background transparent
This commit is contained in:
@@ -10,7 +10,7 @@ import { useProjectStore } from "../../store/project-store.js";
|
||||
import { WritingView } from "../../components/WritingView.js";
|
||||
import { AISidebar } from "./AISidebar.js";
|
||||
|
||||
describe("AISidebar + WritingView accept suggestion flow", () => {
|
||||
describe("AISidebar + WritingView granular text-pegged suggestion flow", () => {
|
||||
beforeEach(() => {
|
||||
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
Element.prototype.scrollIntoView = () => {};
|
||||
@@ -31,16 +31,21 @@ describe("AISidebar + WritingView accept suggestion flow", () => {
|
||||
title: "Chapter One",
|
||||
order: 0,
|
||||
rev: 1,
|
||||
meta: { wordCount: 10, scenes: [], updatedAt: new Date().toISOString() },
|
||||
meta: { wordCount: 25, scenes: [], updatedAt: new Date().toISOString() },
|
||||
doc: {
|
||||
type: "doc",
|
||||
content: [
|
||||
// Blank line at the top to ensure blank lines are never targeted or misaligned
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [],
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "The ferry that carried Mara Vell to the Glass Harbor left the mainland before dawn.",
|
||||
text: "The ferry that carried Mara Vell to the Glass Harbor left the mainland before dawn, its iron hull shuddering against the black chop. By the time it came to rest against the pilings, the light had turned the sea the color of a bruised plum.",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -53,15 +58,6 @@ describe("AISidebar + WritingView accept suggestion flow", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Three years since she had last seen it, and yet it seemed to have shrunk.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -75,12 +71,28 @@ describe("AISidebar + WritingView accept suggestion flow", () => {
|
||||
chapters: [ch],
|
||||
activeChapterId: "ch-1",
|
||||
highlightedLine: null,
|
||||
highlightedText: null,
|
||||
highlightSeq: 0,
|
||||
docExternalRev: 0,
|
||||
revisionsByChapter: {
|
||||
"ch-1": [
|
||||
{
|
||||
id: "rev-initial",
|
||||
chapterId: "ch-1",
|
||||
chapterTitle: "Chapter One",
|
||||
rev: 1,
|
||||
wordCount: 42,
|
||||
savedAt: new Date().toISOString(),
|
||||
label: "Baseline",
|
||||
doc: JSON.parse(JSON.stringify(ch.doc)),
|
||||
},
|
||||
],
|
||||
},
|
||||
status: "ready",
|
||||
});
|
||||
});
|
||||
|
||||
it("generates line suggestions, highlights line on click, and accepts one without crashing", async () => {
|
||||
it("applies multiple granular suggestions sequentially without deleting surrounding sentences, skips blank lines, and supports revision restore", async () => {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
@@ -112,21 +124,47 @@ describe("AISidebar + WritingView accept suggestion flow", () => {
|
||||
await new Promise((r) => setTimeout(r, 1600));
|
||||
});
|
||||
|
||||
const acceptBtn = Array.from(container.querySelectorAll("button")).find((b) =>
|
||||
// Ensure the blank 1st paragraph was skipped and the 2nd DOM child (index 2) is highlighted
|
||||
const edAttr = container.querySelector(".ps-editor")?.getAttribute("data-highlight-line");
|
||||
expect(edAttr).toBe("2");
|
||||
|
||||
// Accept Suggestion #1 (clause 1 of paragraph 2)
|
||||
const acceptBtn1 = Array.from(container.querySelectorAll("button")).find((b) =>
|
||||
b.textContent?.includes("Accept"),
|
||||
);
|
||||
if (!acceptBtn) {
|
||||
console.log("BUTTONS:", Array.from(container.querySelectorAll("button")).map((b) => b.textContent));
|
||||
}
|
||||
expect(acceptBtn).toBeDefined();
|
||||
expect(acceptBtn1).toBeDefined();
|
||||
|
||||
await act(async () => {
|
||||
acceptBtn!.click();
|
||||
await new Promise((r) => setTimeout(r, 150));
|
||||
acceptBtn1!.click();
|
||||
await new Promise((r) => setTimeout(r, 120));
|
||||
});
|
||||
|
||||
const pm = container.querySelector(".ProseMirror");
|
||||
expect(pm?.textContent).toContain("sharp and unmistakable");
|
||||
// Accept Suggestion #2 (clause 2 of the SAME paragraph 2)
|
||||
const acceptBtn2 = Array.from(container.querySelectorAll("button")).find((b) =>
|
||||
b.textContent?.includes("Accept"),
|
||||
);
|
||||
expect(acceptBtn2).toBeDefined();
|
||||
|
||||
await act(async () => {
|
||||
acceptBtn2!.click();
|
||||
await new Promise((r) => setTimeout(r, 120));
|
||||
});
|
||||
|
||||
// Verify BOTH granular clauses in paragraph 2 were updated AND the rest of the paragraph was NOT deleted!
|
||||
const pmText = container.querySelector(".ProseMirror")?.textContent ?? "";
|
||||
expect(pmText).toContain("The ferry that carried Mara Vell to the Glass Harbor cleared the mainland before dawn");
|
||||
expect(pmText).toContain("By the time it came to rest against the pilings, first light had stained the swell the dark violet of a bruised plum.");
|
||||
|
||||
// Verify revisions were recorded and restoring rev #1 brings back original text
|
||||
const revs = useProjectStore.getState().revisionsByChapter["ch-1"] ?? [];
|
||||
expect(revs.length).toBeGreaterThanOrEqual(3);
|
||||
|
||||
await act(async () => {
|
||||
await useProjectStore.getState().restoreRevision("ch-1", "rev-initial");
|
||||
});
|
||||
|
||||
const restoredText = container.querySelector(".ProseMirror")?.textContent ?? "";
|
||||
expect(restoredText).toContain("left the mainland before dawn");
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
|
||||
Reference in New Issue
Block a user