Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions frontend/e2e/chat.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ test("manages settings, permanent memory, and model pull progress", async ({ pag
});

test("renders a structured workbench transcript with markdown and code blocks", async ({ page }) => {
await page.setViewportSize({ width: 764, height: 787 });
const content = [
"# Result",
"",
Expand All @@ -195,9 +196,10 @@ test("renders a structured workbench transcript with markdown and code blocks",
"- Input was validated",
"- Calculation was run locally",
"",
"| Input | Output |",
"| --- | ---: |",
"| 6 × 7 | 42 |",
"| Aspect | What It Is | Key Points |",
"| --- | --- | --- |",
"| Shape | Almost spherical, but a bit flattened at the poles. | About 384000 km away; that is roughly one quarter the distance from Earth to the Sun. |",
"| Why it matters | It provides a testing ground for lunar missions. | Its geology helps us study planetary evolution. |",
"",
"```python",
"value = 6 * 7",
Expand Down Expand Up @@ -243,7 +245,20 @@ test("renders a structured workbench transcript with markdown and code blocks",
await expect(page.getByRole("article", { name: "Cortex message" }).getByText("Cortex")).toBeVisible();
await expect(page.getByRole("article", { name: "Your message" }).getByText("You")).toBeVisible();
await expect(page.getByRole("heading", { name: "Result", level: 1 })).toBeVisible();
await expect(page.getByRole("cell", { name: "42" })).toBeVisible();
await expect(page.getByRole("cell", { name: "Aspect" })).toBeVisible();
await expect(page.getByRole("cell", { name: "Shape" })).toBeVisible();
const tableLayout = await page.locator(".markdown-table-wrap table").evaluate((node) => {
const firstRow = node.querySelector("tr");
const firstCell = firstRow?.firstElementChild;
return {
firstColumnWidth: firstCell?.getBoundingClientRect().width ?? 0,
tableWidth: node.getBoundingClientRect().width,
wrapperWidth: node.parentElement?.getBoundingClientRect().width ?? 0,
wrapperScrollWidth: node.parentElement?.scrollWidth ?? 0,
};
});
expect(tableLayout.firstColumnWidth).toBeGreaterThan(120);
expect(tableLayout.wrapperScrollWidth).toBeGreaterThanOrEqual(tableLayout.wrapperWidth);
await expect(page.getByRole("button", { name: "Copy python code" })).toBeVisible();
await expect(page.getByText("Reasoning")).toBeVisible();
await expect(page.getByText("Sources")).toBeVisible();
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/components/SafeMarkdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ function CodeCopyButton({ value, language }: { value: string; language: string }
return <button className="code-copy" type="button" aria-label={`Copy ${language} code`} onClick={() => void copy()}>{copied ? "Copied" : "Copy"}</button>;
}

function Table({ children, ...props }: ComponentProps<"table">) {
return (
<div className="markdown-table-wrap">
<table {...props}>{children}</table>
</div>
);
}

export function SafeMarkdown({ content }: { content: string }) {
return (
<ReactMarkdown
Expand All @@ -60,6 +68,7 @@ export function SafeMarkdown({ content }: { content: string }) {
a: Link,
code: Code,
img: () => null,
table: Table,
}}
>
{content}
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/styles/tokens.css
Original file line number Diff line number Diff line change
Expand Up @@ -304,8 +304,10 @@ a { color: inherit; text-decoration: none; }
.markdown-body a { color: var(--accent); text-decoration: underline; text-decoration-color: color-mix(in srgb, var(--accent) 42%, transparent); text-underline-offset: 3px; }
.message-user .markdown-body a { color: #fff; }
.markdown-body blockquote { margin: 14px 0; padding: 2px 0 2px 15px; border-left: 3px solid var(--accent); color: var(--text-muted); }
.markdown-body table { width: 100%; margin: 15px 0; border-collapse: collapse; font-size: 0.86rem; }
.markdown-body th, .markdown-body td { border: 1px solid var(--line); padding: 8px 10px; text-align: left; vertical-align: top; }
.markdown-table-wrap { max-width: 100%; margin: 15px 0; overflow-x: auto; overscroll-behavior-x: contain; scrollbar-gutter: stable; }
.markdown-body table { width: 100%; min-width: 520px; margin: 0; border-collapse: collapse; font-size: 0.86rem; table-layout: auto; }
.markdown-body th, .markdown-body td { border: 1px solid var(--line); padding: 8px 10px; text-align: left; vertical-align: top; overflow-wrap: break-word; word-break: normal; }
.markdown-body th:first-child, .markdown-body td:first-child { width: 18%; min-width: 10rem; }
.markdown-body th { background: var(--surface-soft); color: var(--text); font-weight: 720; }
.markdown-body pre { position: relative; overflow: auto; margin: 18px 0; padding: 0; border: 1px solid var(--line-strong); border-radius: 10px; background: color-mix(in srgb, var(--bg) 88%, var(--surface)); color: var(--text); }
.markdown-body code { font-family: "Cascadia Code", "SFMono-Regular", Consolas, monospace; font-size: 0.82rem; }
Expand Down
Loading