diff --git a/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace.css b/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace.css index 0af36c0a02..e85f6c2ef0 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace.css +++ b/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace.css @@ -551,8 +551,6 @@ button.personal-execution-chip:focus-visible { outline: 2px solid #0070f3; outli .personal-md-list { display: grid; gap: 3px; margin: 0; padding-left: 20px; } .personal-md-list li::marker { color: var(--pw-faint); } .personal-md-code { padding: 1px 5px; border-radius: 5px; background: #f0efe9; color: #4d5361; font-family: var(--font-mono); font-size: var(--text-caption); line-height: var(--leading-caption); } -.personal-md-pre { margin: 0; padding: 12px 14px; border-radius: 10px; background: #23262e; color: #e8ecf3; font: var(--text-body)/var(--leading-body) var(--font-mono); overflow-x: auto; white-space: pre; } -.personal-md-pre code { font: inherit; } .personal-md-link { color: var(--pw-blue-ink); text-decoration: underline; text-underline-offset: 2px; word-break: break-all; } /* Agent persona card */ @@ -1483,8 +1481,8 @@ button.personal-execution-chip:focus-visible { outline: 2px solid #0070f3; outli .personal-md-list li { padding-left: 2px; overflow-wrap: anywhere; } .personal-md-list li::marker { color: var(--pw-faint); } .personal-md-code { padding: 1px 5px; border: 1px solid var(--pw-line); border-radius: 6px; background: #f5f4f1; font: var(--text-body)/var(--leading-body) var(--font-mono); overflow-wrap: anywhere; } -.personal-md-pre { margin: 0; padding: 12px 14px; border: 1px solid var(--pw-line); border-radius: 11px; background: #f7f6f3; overflow-x: auto; } -.personal-md-pre code { font: var(--text-body)/var(--leading-body) var(--font-mono); color: #333943; white-space: pre; } +.personal-md-pre { margin: 0; padding: 12px 14px; border: 1px solid var(--pw-line); border-radius: 11px; background: #f7f6f3; color: var(--pw-text); font: var(--text-body)/var(--leading-body) var(--font-mono); overflow-x: auto; white-space: pre; } +.personal-md-pre code { font: var(--text-body)/var(--leading-body) var(--font-mono); color: inherit; white-space: pre; } .personal-md-link { color: var(--pw-blue-ink); text-decoration: none; border-bottom: 1px solid #c3d3f9; } .personal-md-link:hover { border-bottom-color: var(--pw-blue-ink); } diff --git a/examples/personal-workspace-browser/answer-presentation.mjs b/examples/personal-workspace-browser/answer-presentation.mjs index 3da1fe0026..a69ec0710c 100644 --- a/examples/personal-workspace-browser/answer-presentation.mjs +++ b/examples/personal-workspace-browser/answer-presentation.mjs @@ -2,6 +2,7 @@ // steward and Goal Chat, without turning a short factual answer into a report. import { resolve } from "node:path"; +import { writeFile } from "node:fs/promises"; import { outputDir } from "./fixture.mjs"; import { openWorkspacePage } from "./scenario-context.mjs"; @@ -57,6 +58,24 @@ async function assertAnswerTypography(answer) { } } +// Compare the actual rendered foreground/background, not a particular theme hex. +async function codePresentation(answer) { + return answer.evaluate(node => { + const luminance = color => color.match(/[\d.]+/g).slice(0, 3) + .map(value => Number(value) / 255) + .map(value => value <= .04045 ? value / 12.92 : ((value + .055) / 1.055) ** 2.4) + .reduce((sum, value, index) => sum + value * [.2126, .7152, .0722][index], 0); + return ['.personal-md-pre code', '.personal-md-code'].map(selector => { + const code = node.querySelector(selector), style = getComputedStyle(code); + const background = getComputedStyle(code.closest('pre') ?? code).backgroundColor; + const levels = [luminance(style.color), luminance(background)].sort((a, b) => a - b); + return {selector, foreground: style.color, background, + contrast: (levels[1] + .05) / (levels[0] + .05), + fontSize: parseFloat(style.fontSize), selectable: style.userSelect !== 'none'}; + }); + }); +} + async function assertConversationFits(page) { const layout = await page.locator(".personal-channel-scroll").evaluate(node => ({ width: node.clientWidth, scrollWidth: node.scrollWidth, @@ -99,7 +118,7 @@ export const answerPresentationScenario = { throw new Error("Steward answer lost its ending or executed model HTML"); } await answer.getByRole("link", { name: "单独阅读完整答复" }).waitFor({ state: "visible", timeout: 15_000 }); - await answer.scrollIntoViewIfNeeded(); + await answer.locator(".personal-md-pre").scrollIntoViewIfNeeded(); await page.screenshot({ path: resolve(outputDir, "answer-presentation-desktop.png"), fullPage: false, animations: "disabled" }); await page.setViewportSize({ width: 390, height: 844 }); @@ -116,7 +135,38 @@ export const answerPresentationScenario = { if (overflow.card || !overflow.table) { throw new Error(`The mobile card must fit while the wide table scrolls: ${JSON.stringify(overflow)}`); } + await code.scrollIntoViewIfNeeded(); await page.screenshot({ path: resolve(outputDir, "answer-presentation-mobile.png"), fullPage: false, animations: "disabled" }); + const measurements = []; + for (const theme of ["loopx", "paper", "brutal"]) { + await page.evaluate(value => localStorage.setItem("loopx-pw-theme", value), theme); + await page.reload({ waitUntil: "networkidle" }); + await page.getByRole("navigation", {name:"管家视图"}) + .getByRole("button", {name:/^(Chat|对话)$/, exact:true}).click(); + const themed = page.locator(".personal-channel-timeline .personal-message.is-assistant", { + hasText:"建议先验证方案 A", + }); + await themed.locator(".personal-md-pre code").waitFor(); + for (const width of [1512, 390]) { + await page.setViewportSize({width, height:width === 390 ? 844 : 982}); + await assertConversationFits(page); + const colors = await codePresentation(themed); + measurements.push({theme, width, colors}); + await writeFile(resolve(outputDir, "code-contrast-measurements.json"), JSON.stringify(measurements, null, 2)); + if (colors.some(value => value.contrast < 4.5 || value.fontSize < 14 || !value.selectable)) { + throw new Error(`Code must meet AA contrast without shrinking or disabling selection: ${JSON.stringify({theme, width, colors})}`); + } + const block = themed.locator(".personal-md-pre"); + if (!await block.evaluate(node => node.scrollWidth > node.clientWidth) + || await block.innerText() !== `unwrapped-command ${"exact-argument-".repeat(80)}`) { + throw new Error("Theme switching must preserve complete code and its own horizontal scroll"); + } + await block.scrollIntoViewIfNeeded(); + await page.evaluate(async () => {await document.fonts.ready; await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));}); + await page.screenshot({path:resolve(outputDir, `code-contrast-${theme}-${width}.png`), animations:"disabled"}); + } + } + await page.evaluate(() => localStorage.setItem("loopx-pw-theme", "loopx")); await page.setViewportSize({ width: 1512, height: 982 }); await page.reload({ waitUntil: "networkidle" }); diff --git a/examples/personal-workspace-browser/evidence/code-contrast/after-brutal-1512.png b/examples/personal-workspace-browser/evidence/code-contrast/after-brutal-1512.png new file mode 100644 index 0000000000..7173b76008 Binary files /dev/null and b/examples/personal-workspace-browser/evidence/code-contrast/after-brutal-1512.png differ diff --git a/examples/personal-workspace-browser/evidence/code-contrast/after-brutal-390.png b/examples/personal-workspace-browser/evidence/code-contrast/after-brutal-390.png new file mode 100644 index 0000000000..513f9e0edf Binary files /dev/null and b/examples/personal-workspace-browser/evidence/code-contrast/after-brutal-390.png differ diff --git a/examples/personal-workspace-browser/evidence/code-contrast/after-loopx-1512.png b/examples/personal-workspace-browser/evidence/code-contrast/after-loopx-1512.png new file mode 100644 index 0000000000..a25df898b6 Binary files /dev/null and b/examples/personal-workspace-browser/evidence/code-contrast/after-loopx-1512.png differ diff --git a/examples/personal-workspace-browser/evidence/code-contrast/after-loopx-390.png b/examples/personal-workspace-browser/evidence/code-contrast/after-loopx-390.png new file mode 100644 index 0000000000..e247f1adc5 Binary files /dev/null and b/examples/personal-workspace-browser/evidence/code-contrast/after-loopx-390.png differ diff --git a/examples/personal-workspace-browser/evidence/code-contrast/after-paper-1512.png b/examples/personal-workspace-browser/evidence/code-contrast/after-paper-1512.png new file mode 100644 index 0000000000..dd3f073e73 Binary files /dev/null and b/examples/personal-workspace-browser/evidence/code-contrast/after-paper-1512.png differ diff --git a/examples/personal-workspace-browser/evidence/code-contrast/after-paper-390.png b/examples/personal-workspace-browser/evidence/code-contrast/after-paper-390.png new file mode 100644 index 0000000000..33bab64fe0 Binary files /dev/null and b/examples/personal-workspace-browser/evidence/code-contrast/after-paper-390.png differ diff --git a/examples/personal-workspace-browser/evidence/code-contrast/before-desktop.png b/examples/personal-workspace-browser/evidence/code-contrast/before-desktop.png new file mode 100644 index 0000000000..5a8560aecb Binary files /dev/null and b/examples/personal-workspace-browser/evidence/code-contrast/before-desktop.png differ diff --git a/examples/personal-workspace-browser/evidence/code-contrast/before-narrow.png b/examples/personal-workspace-browser/evidence/code-contrast/before-narrow.png new file mode 100644 index 0000000000..49e2fde837 Binary files /dev/null and b/examples/personal-workspace-browser/evidence/code-contrast/before-narrow.png differ diff --git a/examples/personal-workspace-browser/evidence/code-contrast/report-desktop.png b/examples/personal-workspace-browser/evidence/code-contrast/report-desktop.png new file mode 100644 index 0000000000..b241d37b66 Binary files /dev/null and b/examples/personal-workspace-browser/evidence/code-contrast/report-desktop.png differ diff --git a/examples/personal-workspace-browser/evidence/code-contrast/report-narrow.png b/examples/personal-workspace-browser/evidence/code-contrast/report-narrow.png new file mode 100644 index 0000000000..4cf938fbeb Binary files /dev/null and b/examples/personal-workspace-browser/evidence/code-contrast/report-narrow.png differ