fix(ui): improve knowledge reading and add isolated formatting examples

This commit is contained in:
Codex
2026-09-13 16:52:53 +02:00
parent 2953f6b608
commit 3535fda958
14 changed files with 362 additions and 19 deletions
@@ -0,0 +1,32 @@
import { render } from "@testing-library/react";
import { MarkdownView } from "./MarkdownView";
test("readable paragraphs preserve text, inline code, links and authored source", () => {
const source = `${"Una regola dimostrativa conserva sempre i dati originali. ".repeat(5)}Si usa \`flag = FALSE; valore >= 0.4\` senza cambiare il codice; ${"Questa parte spiega il criterio senza aggiungere nuove informazioni. ".repeat(5)}Leggere **tutto** e il [documento](https://example.com/a.b).`;
const normal = render(<MarkdownView source={source} />);
const expected = normal.container.textContent;
normal.unmount();
const view = render(<MarkdownView source={source} readable />);
expect(view.container.querySelectorAll(".markdown-view > p").length).toBeGreaterThan(2);
// HTML paragraph boundaries add whitespace, but never change the wording.
expect(view.container.textContent?.replace(/\s+/g, " ").trim()).toBe(expected?.replace(/\s+/g, " ").trim());
expect(view.container.querySelector("code")).toHaveTextContent("flag = FALSE; valore >= 0.4");
expect(view.container.querySelector("strong")).toHaveTextContent("tutto");
expect(view.container.querySelector("a")).toHaveAttribute("href", "https://example.com/a.b");
expect(view.container.querySelector(".markdown-view")).toHaveClass("thot-knowledge-prose");
});
test("leaves existing paragraphs, lists and fenced SQL unchanged", () => {
const sql = `SELECT '${"valore; con punteggiatura. ".repeat(20)}' AS esempio;`;
const source = `Primo paragrafo.\n\nSecondo paragrafo.\n\n- Una voce\n- Altra voce\n\n\`\`\`sql\n${sql}\n\`\`\``;
const view = render(<MarkdownView source={source} readable />);
expect(view.container.querySelectorAll(".markdown-view > p")).toHaveLength(2);
expect(view.container.querySelectorAll("li")).toHaveLength(2);
expect(view.container.querySelector("pre code")?.textContent).toBe(sql + "\n");
});
test("does not enable long-paragraph reflow for ordinary document viewers", () => {
const view = render(<MarkdownView source={"A normal document keeps its authored paragraph. ".repeat(20)} />);
expect(view.container.querySelectorAll("p")).toHaveLength(1);
expect(view.container.querySelector(".thot-knowledge-prose")).toBeNull();
});
+4 -3
View File
@@ -4,6 +4,7 @@ import remarkGfm from "remark-gfm";
import type { Components } from "react-markdown";
import { renderMermaid } from "./mermaid";
import { useShell } from "../shell/host/ShellProvider";
import { readableParagraphs } from "./readableParagraphs";
function MermaidBlock({ code }: { code: string }) {
const { theme } = useShell();
@@ -45,10 +46,10 @@ const components: Components = {
},
};
export function MarkdownView({ source }: { source: string }) {
export function MarkdownView({ source, readable = false }: { source: string; readable?: boolean }) {
return (
<div className="markdown-view thot-prose">
<ReactMarkdown remarkPlugins={[remarkGfm]} components={components}>
<div className={`markdown-view thot-prose${readable ? " thot-knowledge-prose" : ""}`}>
<ReactMarkdown remarkPlugins={readable ? [remarkGfm, readableParagraphs] : [remarkGfm]} components={components}>
{source}
</ReactMarkdown>
</div>
@@ -0,0 +1,41 @@
import type { Paragraph, PhrasingContent, Root, RootContent } from "mdast";
// Display-only: preserve every inline node and character. Never rewrite source
// files, SQL, links, existing lists or short authored paragraphs.
export function readableParagraphs() {
return (tree: Root) => {
tree.children = tree.children.flatMap<RootContent>(node => {
if (node.type !== "paragraph") return [node];
const length = node.children.reduce((total, child) => total + ("value" in child ? String(child.value).length : 0), 0);
if (length < 360) return [node];
const paragraphs: Paragraph[] = [];
let children: PhrasingContent[] = [];
let size = 0;
for (const child of node.children) {
if (child.type !== "text") {
children.push(child);
if ("value" in child) size += String(child.value).length;
continue;
}
// Boundaries must be outside inline code/emphasis/links. Decimal points
// and identifiers cannot match because they have no following space.
let start = 0;
for (const match of child.value.matchAll(/;\s+|[.!?]\s+(?=[A-ZÀ-Ý])/g)) {
const end = match.index! + match[0].length;
if (size + end - start < 180) continue;
children.push({ type: "text", value: child.value.slice(start, end) });
paragraphs.push({ type: "paragraph", children });
children = [];
size = 0;
start = end;
}
if (start < child.value.length) {
children.push({ type: "text", value: child.value.slice(start) });
size += child.value.length - start;
}
}
if (children.length) paragraphs.push({ type: "paragraph", children });
return paragraphs;
});
};
}