Skip to content

Commit 5f7ad83

Browse files
committed
feat(web): add chat text annotations
1 parent e60821f commit 5f7ad83

18 files changed

Lines changed: 2076 additions & 80 deletions
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { describe, expect, it } from "vite-plus/test";
2+
3+
import {
4+
appendChatSelectionAnnotationsToPrompt,
5+
collectChatSelectionAnnotationsByMessageId,
6+
countChatSelectionAnnotationsForMessage,
7+
deriveChatSelectionIndicators,
8+
formatChatSelectionAnnotation,
9+
parseChatSelectionMessageSegments,
10+
type ChatSelectionAnnotation,
11+
} from "./chatSelectionAnnotation";
12+
13+
const annotation: ChatSelectionAnnotation = {
14+
id: "selection-1",
15+
selectedText: "Restart <the> adapter",
16+
comment: "Why is this necessary?",
17+
};
18+
19+
describe("chat selection annotations", () => {
20+
it("round-trips selected text and comments without treating markup as tags", () => {
21+
const prompt = appendChatSelectionAnnotationsToPrompt("Explain this", [annotation]);
22+
const segments = parseChatSelectionMessageSegments(prompt);
23+
24+
expect(segments).toHaveLength(2);
25+
expect(segments[0]).toEqual({
26+
kind: "text",
27+
id: "chat-selection-text:0",
28+
text: "Explain this\n\n",
29+
});
30+
expect(segments[1]).toEqual({ kind: "selection", annotation });
31+
});
32+
33+
it("supports multiple annotations", () => {
34+
const second = {
35+
...annotation,
36+
id: "selection-2",
37+
comment: "Compare this",
38+
};
39+
const segments = parseChatSelectionMessageSegments(
40+
appendChatSelectionAnnotationsToPrompt("", [annotation, second]),
41+
);
42+
43+
expect(segments.filter((segment) => segment.kind === "selection")).toHaveLength(2);
44+
45+
expect(countChatSelectionAnnotationsForMessage([annotation, second], "selection-message")).toBe(
46+
0,
47+
);
48+
expect(
49+
countChatSelectionAnnotationsForMessage(
50+
[{ ...annotation, messageId: "selection-message" }, second],
51+
"selection-message",
52+
),
53+
).toBe(1);
54+
});
55+
56+
it("does not emit a block for an empty annotation list", () => {
57+
expect(appendChatSelectionAnnotationsToPrompt("Keep this", [])).toBe("Keep this");
58+
expect(formatChatSelectionAnnotation(annotation)).toContain("<selected_text>");
59+
});
60+
61+
it("persists the source message id in the sent prompt", () => {
62+
const sourceAnnotation = {
63+
...annotation,
64+
messageId: "assistant-1",
65+
sourceStart: 42,
66+
sourceEnd: 63,
67+
};
68+
const prompt = appendChatSelectionAnnotationsToPrompt("Explain this", [sourceAnnotation]);
69+
const segments = parseChatSelectionMessageSegments(prompt);
70+
71+
expect(segments[1]).toEqual({ kind: "selection", annotation: sourceAnnotation });
72+
expect(prompt).toContain('message_id="assistant-1"');
73+
expect(prompt).toContain('source_start="42" source_end="63"');
74+
});
75+
76+
it("groups pending annotations by source message", () => {
77+
const pending = { ...annotation, messageId: "assistant-1" };
78+
const byMessageId = collectChatSelectionAnnotationsByMessageId([pending]);
79+
80+
expect(byMessageId.get("assistant-1")).toEqual([pending]);
81+
expect(deriveChatSelectionIndicators([pending])).toEqual([
82+
{
83+
id: pending.id,
84+
kind: "text-comment",
85+
number: 1,
86+
annotation: pending,
87+
},
88+
]);
89+
});
90+
91+
it("creates one numbered indicator for every annotation in source order", () => {
92+
const second = { ...annotation, id: "selection-2", comment: "" };
93+
94+
expect(deriveChatSelectionIndicators([annotation, second])).toEqual([
95+
{
96+
id: annotation.id,
97+
kind: "text-comment",
98+
number: 1,
99+
annotation,
100+
},
101+
{
102+
id: second.id,
103+
kind: "text-selection",
104+
number: 2,
105+
annotation: second,
106+
},
107+
]);
108+
});
109+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "@t3tools/shared/chatSelectionAnnotation";

0 commit comments

Comments
 (0)