feat(ai): render chart-json blocks as interactive charts
This commit is contained in:
1 parent
852ad1c808
commit
29c6d1fbed
22 files changed
+1214
-9
No files matched your search
+39
@@ -204,4 +204,43 @@ describe("AI SQL dialect prompt", () => {
|
||||
expect(prompt).toContain("db.collection.findOne({})");
|
||||
expect(prompt).not.toContain("get_sample_data");
|
||||
});
|
||||
|
||||
it("injects the rich-content chart protocol into the normal database system prompt", () => {
|
||||
const prompt = buildSystemPrompt("generate", context(), "ask");
|
||||
expect(prompt).toContain("chart-json");
|
||||
expect(prompt).toContain("at most one chart per reply");
|
||||
expect(prompt).toContain('"version":1,"type":"line"');
|
||||
expect(prompt).toContain('"version":1,"type":"pie"');
|
||||
expect(prompt).toContain('"xAxis":{"values":["Jan","Feb","Mar"]}');
|
||||
expect(prompt).toContain("grounded in actual available data");
|
||||
expect(prompt).toContain("Do not emit ```html code blocks unless the user explicitly asks for them.");
|
||||
});
|
||||
|
||||
it("injects the rich-content chart protocol into the vector database system prompt", () => {
|
||||
const prompt = buildSystemPrompt("general", context({ databaseType: "qdrant", connectionName: "Qdrant", database: "vec" }), "ask");
|
||||
expect(prompt).toContain("chart-json");
|
||||
expect(prompt).toContain("at most one chart per reply");
|
||||
expect(prompt).toContain('"version":1,"type":"pie"');
|
||||
expect(prompt).toContain("Do not emit ```html code blocks unless the user explicitly asks for them.");
|
||||
});
|
||||
|
||||
it("injects the zh rich-content protocol into both normal and vector prompts", async () => {
|
||||
await setLocale("zh-CN");
|
||||
try {
|
||||
const normal = buildSystemPrompt("generate", context(), "ask");
|
||||
expect(normal).toContain("chart-json");
|
||||
expect(normal).toContain("一条回复最多一个");
|
||||
expect(normal).toContain('"version":1,"type":"line"');
|
||||
expect(normal).toContain("不得编造");
|
||||
expect(normal).toContain("不要输出 ```html 代码块,除非用户明确要求");
|
||||
|
||||
const vector = buildSystemPrompt("general", context({ databaseType: "milvus", connectionName: "Milvus", database: "vec" }), "ask");
|
||||
expect(vector).toContain("chart-json");
|
||||
expect(vector).toContain("一条回复最多一个");
|
||||
expect(vector).toContain('"version":1,"type":"pie"');
|
||||
expect(vector).toContain("不要输出 ```html 代码块,除非用户明确要求");
|
||||
} finally {
|
||||
await setLocale("en");
|
||||
}
|
||||
});
|
||||
});
|
||||
+56
-1
@@ -1,7 +1,6 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createAiMessageRenderer, splitStreamingTextBlocks } from "@/lib/ai/aiMessageRender";
|
||||
import { formatAiInlineMarkdown } from "@/lib/ai/aiMarkdown";
|
||||
|
||||
describe("createAiMessageRenderer", () => {
|
||||
it("caches completed short messages", () => {
|
||||
const markdown = vi.fn((text: string) => `<p>${text}</p>`);
|
||||
@@ -205,6 +204,62 @@ describe("createAiMessageRenderer", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("createAiMessageRenderer rich chart segments", () => {
|
||||
const markdown = (text: string) => `<p>${text}</p>`;
|
||||
const chartJson = JSON.stringify({ version: 1, type: "bar", xAxis: { values: ["Jan", "Feb"] }, series: [{ name: "Revenue", data: [120, 200] }] });
|
||||
|
||||
it("routes a closed chart-json fence to a chart segment", () => {
|
||||
const renderer = createAiMessageRenderer({ markdown });
|
||||
const segments = renderer.render("```chart-json\n" + chartJson + "\n```");
|
||||
const seg = segments[0];
|
||||
expect(seg.type).toBe("chart");
|
||||
if (seg.type !== "chart") return;
|
||||
expect(seg.content).toBe(chartJson);
|
||||
expect(seg.spec.type).toBe("bar");
|
||||
});
|
||||
|
||||
it("keeps an unfinished chart-json fence as a plain code segment while streaming", () => {
|
||||
const renderer = createAiMessageRenderer({ markdown });
|
||||
const [seg] = renderer.render(`\`\`\`chart-json\n${chartJson.slice(0, 20)}`, { streaming: true });
|
||||
expect(seg.type).toBe("code");
|
||||
if (seg.type !== "code") return;
|
||||
expect(seg.pending).toBe(true);
|
||||
});
|
||||
|
||||
it("switches an open chart-json fence to a chart segment once the closing fence arrives", () => {
|
||||
const renderer = createAiMessageRenderer({ markdown });
|
||||
const open = `\`\`\`chart-json\n${chartJson.slice(0, 20)}`;
|
||||
const closed = `\`\`\`chart-json\n${chartJson}\n\`\`\``;
|
||||
expect(renderer.render(open, { streaming: true })[0].type).toBe("code");
|
||||
expect(renderer.render(closed, { streaming: true })[0].type).toBe("chart");
|
||||
});
|
||||
|
||||
it("falls back to a code segment when a closed chart-json fence fails validation", () => {
|
||||
const renderer = createAiMessageRenderer({ markdown });
|
||||
const [seg] = renderer.render("```chart-json\n{ not json\n```");
|
||||
expect(seg.type).toBe("code");
|
||||
if (seg.type !== "code") return;
|
||||
expect(seg.lang).toBe("CHART-JSON");
|
||||
expect(seg.isSql).toBe(false);
|
||||
expect(seg.pending).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps sql/bash/json fences on the plain code path", () => {
|
||||
const renderer = createAiMessageRenderer({ markdown });
|
||||
const [sql] = renderer.render("```sql\nSELECT 1\n```");
|
||||
expect(sql.type).toBe("code");
|
||||
if (sql.type !== "code") return;
|
||||
expect(sql.lang).toBe("SQL");
|
||||
expect(sql.isSql).toBe(true);
|
||||
|
||||
const [bash] = renderer.render("```bash\necho hi\n```");
|
||||
expect(bash.type).toBe("code");
|
||||
if (bash.type !== "code") return;
|
||||
expect(bash.lang).toBe("BASH");
|
||||
expect(bash.isSql).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("splitStreamingTextBlocks", () => {
|
||||
const long = "内容".repeat(200);
|
||||
|
||||
|
||||
Reference in new issue
Block a user