From 9232a6c7c2aa6d7b58b74785df2e7335f870aa41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=94=E9=A6=99=E4=B8=B8=E5=AD=90?= <15518898337@163.com> Date: Sun, 23 Aug 2026 17:13:26 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E4=BC=98=E5=8C=96Agent=E5=AF=B9=E8=AF=9D?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E6=B6=88=E6=81=AF=E6=B0=94=E6=B3=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将用户消息改为右侧气泡显示。 统一项目对话、Agent聊天页和Agent对话弹窗样式。 补充消息宽度、换行、圆角和移动端友好布局。 --- apps/ai-game-creator-shell/src/styles.css | 54 +++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/apps/ai-game-creator-shell/src/styles.css b/apps/ai-game-creator-shell/src/styles.css index 6c0eb6197..db1cc1f89 100644 --- a/apps/ai-game-creator-shell/src/styles.css +++ b/apps/ai-game-creator-shell/src/styles.css @@ -1090,10 +1090,17 @@ textarea { .launcher-agent-chat-messages .message { max-width: min(720px, 88%); margin: 0; + padding: 9px 12px; + border-radius: 14px; + line-height: 1.55; } .launcher-agent-chat-messages .message--user { justify-self: end; + border: 1px solid #d8c4a8; + border-radius: 14px 14px 4px; + background: #fff7ed; + color: #8a4b08; } .launcher-agent-chat-waiting { @@ -4419,6 +4426,27 @@ h2 { border: 1px solid #dde3ee; } +.agent-conversation-list .message { + width: fit-content; + max-width: min(88%, 720px); + margin: 0; + padding: 9px 12px; + border-radius: 14px; + line-height: 1.55; +} + +.agent-conversation-list .message + .message { + margin-top: 10px; +} + +.agent-conversation-list .message--user { + margin-left: auto; + border: 1px solid #d8c4a8; + border-radius: 14px 14px 4px; + background: #fff7ed; + color: #8a4b08; +} + .agent-memory-box { display: grid; gap: 6px; @@ -6219,6 +6247,9 @@ iframe.preview-frame { } .game-workbench-chat .project-supervisor-message-list { + display: flex; + flex-direction: column; + align-items: flex-start; min-height: 96px; max-height: none; overflow-y: auto; @@ -6227,6 +6258,29 @@ iframe.preview-frame { scrollbar-gutter: stable; } +/* Keep the transcript readable while making the user's turns immediately + distinct from the assistant stream, like a conventional chat surface. */ +.game-workbench-chat .project-supervisor-message-list .message { + width: fit-content; + max-width: min(88%, 720px); + margin: 0; + padding: 9px 12px; + border-radius: 14px; + line-height: 1.55; +} + +.game-workbench-chat .project-supervisor-message-list .message + .message { + margin-top: 10px; +} + +.game-workbench-chat .project-supervisor-message-list .message--user { + align-self: flex-end; + border: 1px solid var(--platform-button-primary-border); + border-radius: 14px 14px 4px; + background: var(--platform-warm-bg); + color: var(--platform-warm-text); +} + .game-workbench-chat .agent-runtime-status { max-height: clamp(120px, 24dvh, 240px); min-height: 0; -- 2.52.0 From 5db439b142dd999096d5b8020025dc9057d7d13e Mon Sep 17 00:00:00 2001 From: kdletters Date: Sun, 23 Aug 2026 18:32:38 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E6=80=BB=E6=8E=A7=E5=AF=B9=E8=AF=9D=E6=B0=94=E6=B3=A1=E5=B8=83?= =?UTF-8?q?=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 恢复消息列表块布局,避免执行过程卡收缩 调整用户气泡文字主题色,满足浅色和深色对比度 补充布局契约测试与工作台 PRD 验收口径 --- apps/ai-game-creator-shell/src/styles.css | 7 ++--- .../appSurface/project-development.suite.ts | 31 +++++++++++++++++++ ...AI游戏创作】项目开发工作台PRD-2026-07-20.md | 3 +- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/apps/ai-game-creator-shell/src/styles.css b/apps/ai-game-creator-shell/src/styles.css index db1cc1f89..8a5a44455 100644 --- a/apps/ai-game-creator-shell/src/styles.css +++ b/apps/ai-game-creator-shell/src/styles.css @@ -6247,9 +6247,6 @@ iframe.preview-frame { } .game-workbench-chat .project-supervisor-message-list { - display: flex; - flex-direction: column; - align-items: flex-start; min-height: 96px; max-height: none; overflow-y: auto; @@ -6274,11 +6271,11 @@ iframe.preview-frame { } .game-workbench-chat .project-supervisor-message-list .message--user { - align-self: flex-end; + margin-left: auto; border: 1px solid var(--platform-button-primary-border); border-radius: 14px 14px 4px; background: var(--platform-warm-bg); - color: var(--platform-warm-text); + color: var(--platform-text-base); } .game-workbench-chat .agent-runtime-status { diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts index 7d911c847..f0178131c 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts @@ -3482,6 +3482,37 @@ export function registerProjectWorkbenchFoundationTests() { ); }); + it('keeps workbench chat bubbles aligned without shrinking process cards', () => { + const styles = readFileSync( + resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), + 'utf8', + ); + const messageListRule = + styles.match( + /\.game-workbench-chat \.project-supervisor-message-list\s*\{([^}]*)\}/s, + )?.[1] ?? ''; + const messageRule = + styles.match( + /\.game-workbench-chat \.project-supervisor-message-list \.message\s*\{([^}]*)\}/s, + )?.[1] ?? ''; + const userMessageRule = + styles.match( + /\.game-workbench-chat \.project-supervisor-message-list \.message--user\s*\{([^}]*)\}/s, + )?.[1] ?? ''; + + expect(messageListRule).not.toBe(''); + expect(messageListRule).not.toContain('display: flex;'); + expect(messageListRule).not.toContain('align-items:'); + expect(messageRule).toContain('width: fit-content;'); + expect(messageRule).toContain('max-width: min(88%, 720px);'); + expect(userMessageRule).not.toBe(''); + expect(userMessageRule).toContain('margin-left: auto;'); + expect(userMessageRule).not.toContain('align-self:'); + expect(userMessageRule).toContain('border-radius: 14px 14px 4px;'); + expect(userMessageRule).toContain('background: var(--platform-warm-bg);'); + expect(userMessageRule).toContain('color: var(--platform-text-base);'); + }); + it('enables the run presentation and renders registered images in the resource viewer', async () => { const manifest = createGameCreationAppManifest( 'workbench-runnable', diff --git a/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md b/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md index bf17da26f..1210b16e9 100644 --- a/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md +++ b/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md @@ -1,6 +1,6 @@ # AI 游戏创作项目开发工作台 PRD -更新时间:`2026-08-15`(项目管理页紧凑表格与桌面可用性收口;资源卡预览、分区布局、非破坏性资源编辑及 Godot 双根合同保持不变) +更新时间:`2026-08-23`(右侧 Supervisor 对话气泡、可访问对比度与过程卡布局收口;资源卡预览、分区布局、非破坏性资源编辑及 Godot 双根合同保持不变) ## 1. 产品定位 @@ -108,6 +108,7 @@ - 第一批共享 chrome 固定覆盖画布动作按钮、工具栏、工具分组和分隔符。按钮的默认、悬停、键盘焦点、选中、禁用和主次色语义由共享层表达;宿主只提供图标、文案、事件与业务禁用条件。 - 主站账号、钱包、服务端项目、云端素材库和生成面板仍留在网站宿主;客户端本地项目、manifest、Runtime、审批、草稿、生成与正式提交仍留在 Tauri 宿主。共享视觉组件不得读取这些业务事实。 - 客户端 UI 对齐采用“同视觉、同组件、保留四区布局”,不复制主站整页布局。资源总览、运行视图和 Supervisor 对话继续是 Game Agent 工作台独有语义。 +- 右侧 Supervisor 对话中,用户消息使用右对齐、最大宽度受限的主题暖色气泡,assistant 消息保持左对齐;消息换行不得产生水平溢出,执行过程卡继续占满消息区可用宽度。气泡正文在 light / dark 平台主题下均须满足 WCAG AA 普通文本 `4.5:1` 对比度。 - 客户端正式产品仍只按最小 `1280×720` 横屏合同交付,并保留 `1280×800` 默认窗口与既有基线验收;更窄浏览器样式只负责不崩溃和开发兼容,不改成移动端创作工作台。 - 普通用户界面不默认展示内部错误码或本机绝对路径,也不展示 External Editor Base URL 或 Developer API Key;官方服务地址由客户端构建固定,远端编辑自动使用当前陶泥儿登录态。仅独立 standalone game-chat release 或显式高级自定义模式在隔离配置页维护 External v1 URL/Key,确需诊断的信息进入受控详情或开发模式。 - 素材画布的“素材名称”是用户可编辑的正式输出名称;“资源用途”是 manifest subtype,不向普通用户开放自由文本。新增资源默认“普通游戏美术”,可从普通游戏美术、统一视觉规范、游戏界面原型、核心美术图集四项中选择;精修资源继承源用途且不可改。导出格式继续限定 PNG/JPEG/WebP。工具动作与保存设置分层展示,“保存到项目”在 `1280×800` 和窄容器中都必须完整可见。 -- 2.52.0 From da1bcbd2a7ebbe66bbb80b19ba9f90c200938814 Mon Sep 17 00:00:00 2001 From: kdletters Date: Sun, 23 Aug 2026 19:04:12 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E8=A1=A5=E5=BC=BA=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E6=80=BB=E6=8E=A7=E6=B0=94=E6=B3=A1=E5=9B=9E=E5=BD=92=E5=90=88?= =?UTF-8?q?=E5=90=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 显式锁定执行过程卡占满消息区宽度 验证执行过程卡保持为消息列表直接子项 新增浅色与深色主题气泡 WCAG 对比度测试 --- apps/ai-game-creator-shell/src/styles.css | 6 + .../appSurface/project-development.suite.ts | 9 ++ .../tests/workbenchThemeContrast.test.ts | 148 ++++++++++++++++++ 3 files changed, 163 insertions(+) create mode 100644 apps/ai-game-creator-shell/tests/workbenchThemeContrast.test.ts diff --git a/apps/ai-game-creator-shell/src/styles.css b/apps/ai-game-creator-shell/src/styles.css index 8a5a44455..8fd623592 100644 --- a/apps/ai-game-creator-shell/src/styles.css +++ b/apps/ai-game-creator-shell/src/styles.css @@ -6278,6 +6278,12 @@ iframe.preview-frame { color: var(--platform-text-base); } +.game-workbench-chat + .project-supervisor-message-list + > .project-supervisor-process-card { + width: 100%; +} + .game-workbench-chat .agent-runtime-status { max-height: clamp(120px, 24dvh, 240px); min-height: 0; diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts index f0178131c..9507775bb 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts @@ -3499,6 +3499,12 @@ export function registerProjectWorkbenchFoundationTests() { styles.match( /\.game-workbench-chat \.project-supervisor-message-list \.message--user\s*\{([^}]*)\}/s, )?.[1] ?? ''; + const processCardRules = Array.from( + styles.matchAll( + /\.game-workbench-chat\s+\.project-supervisor-message-list\s*>\s*\.project-supervisor-process-card\s*\{([^}]*)\}/gs, + ), + (match) => match[1], + ); expect(messageListRule).not.toBe(''); expect(messageListRule).not.toContain('display: flex;'); @@ -3511,6 +3517,8 @@ export function registerProjectWorkbenchFoundationTests() { expect(userMessageRule).toContain('border-radius: 14px 14px 4px;'); expect(userMessageRule).toContain('background: var(--platform-warm-bg);'); expect(userMessageRule).toContain('color: var(--platform-text-base);'); + expect(processCardRules).toHaveLength(1); + expect(processCardRules[0]).toContain('width: 100%;'); }); it('enables the run presentation and renders registered images in the resource viewer', async () => { @@ -9310,6 +9318,7 @@ export function registerProjectSupervisorSurfaceTests() { }); const waitingProcessCard = within(directMessageList).getByLabelText('陶泥儿执行过程'); + expect(waitingProcessCard.parentElement).toBe(directMessageList); expect( within(waitingProcessCard).getByText('已发送消息,正在等待陶泥儿回复'), ).not.toBeNull(); diff --git a/apps/ai-game-creator-shell/tests/workbenchThemeContrast.test.ts b/apps/ai-game-creator-shell/tests/workbenchThemeContrast.test.ts new file mode 100644 index 000000000..8968364ec --- /dev/null +++ b/apps/ai-game-creator-shell/tests/workbenchThemeContrast.test.ts @@ -0,0 +1,148 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +type Rgba = readonly [number, number, number, number]; + +const themePath = resolve(process.cwd(), 'packages/shared/src/theme.css'); + +function getCssBlock(source: string, selector: string) { + const selectorIndex = source.indexOf(selector); + expect(selectorIndex, `${selector} should exist`).toBeGreaterThanOrEqual(0); + + const openBraceIndex = source.indexOf('{', selectorIndex); + let depth = 0; + for (let index = openBraceIndex; index < source.length; index += 1) { + const char = source[index]; + if (char === '{') { + depth += 1; + } else if (char === '}') { + depth -= 1; + if (depth === 0) { + return source.slice(openBraceIndex + 1, index); + } + } + } + + throw new Error(`${selector} block is not closed`); +} + +function getCssVariable(block: string, variableName: string) { + const match = block.match(new RegExp(`${variableName}:\\s*([^;]+);`)); + expect(match, `${variableName} should exist`).not.toBeNull(); + return match![1].trim(); +} + +function parseCssColor(source: string): Rgba { + const value = source.trim(); + const hex = value.match(/^#([\da-f]{6})$/i); + if (hex) { + return [ + Number.parseInt(hex[1].slice(0, 2), 16), + Number.parseInt(hex[1].slice(2, 4), 16), + Number.parseInt(hex[1].slice(4, 6), 16), + 1, + ]; + } + + const functional = value.match(/^rgba?\((.*)\)$/i); + if (!functional) { + throw new Error(`unsupported color: ${value}`); + } + const parts = functional[1] + .replace('/', ' ') + .split(/[,\s]+/) + .filter(Boolean); + return [ + Number(parts[0]), + Number(parts[1]), + Number(parts[2]), + parts[3] === undefined ? 1 : Number(parts[3]), + ]; +} + +function compositeColor(foreground: Rgba, background: Rgba): Rgba { + const alpha = foreground[3] + background[3] * (1 - foreground[3]); + return [ + (foreground[0] * foreground[3] + + background[0] * background[3] * (1 - foreground[3])) / + alpha, + (foreground[1] * foreground[3] + + background[1] * background[3] * (1 - foreground[3])) / + alpha, + (foreground[2] * foreground[3] + + background[2] * background[3] * (1 - foreground[3])) / + alpha, + alpha, + ]; +} + +function contrastRatio(first: Rgba, second: Rgba) { + const luminance = ([red, green, blue]: Rgba) => { + const channels = [red, green, blue].map((channel) => { + const value = channel / 255; + return value <= 0.04045 + ? value / 12.92 + : ((value + 0.055) / 1.055) ** 2.4; + }); + return channels[0] * 0.2126 + channels[1] * 0.7152 + channels[2] * 0.0722; + }; + + const firstLuminance = luminance(first); + const secondLuminance = luminance(second); + return ( + (Math.max(firstLuminance, secondLuminance) + 0.05) / + (Math.min(firstLuminance, secondLuminance) + 0.05) + ); +} + +describe('workbench theme contrast', () => { + it('keeps warm user bubbles above WCAG AA text contrast', () => { + const css = readFileSync(themePath, 'utf8'); + const light = getCssBlock(css, '.platform-theme--light'); + const dark = getCssBlock(css, '.platform-theme--dark'); + const bubbleBackground = (theme: string, underlay: Rgba) => + compositeColor( + parseCssColor(getCssVariable(theme, '--platform-warm-bg')), + compositeColor( + parseCssColor(getCssVariable(theme, '--platform-input-fill')), + underlay, + ), + ); + + // Pure black is darker than any real light-theme workbench underlay. + const lightContrast = contrastRatio( + parseCssColor(getCssVariable(light, '--platform-text-base')), + bubbleBackground(light, [0, 0, 0, 1]), + ); + + const darkBodyFill = getCssVariable(dark, '--platform-body-fill'); + const darkPanelFill = getCssVariable(dark, '--platform-desktop-panel-fill'); + expect(darkBodyFill).toContain('rgba(129, 140, 248, 0.2)'); + expect(darkBodyFill).toContain('rgba(59, 130, 246, 0.12)'); + expect(darkBodyFill).toContain('rgba(34, 211, 238, 0.08)'); + expect(darkBodyFill).toContain('#13151c'); + expect(darkPanelFill).toContain('rgba(255, 255, 255, 0.05)'); + // Conservative upper bound: the brightest body stop, every radial peak, + // and the brightest desktop-panel overlay are composed at full strength. + const darkBrightestUnderlay = compositeColor( + [255, 255, 255, 0.05], + compositeColor( + [129, 140, 248, 0.2], + compositeColor( + [59, 130, 246, 0.12], + compositeColor([34, 211, 238, 0.08], [19, 21, 28, 1]), + ), + ), + ); + const darkContrast = contrastRatio( + parseCssColor(getCssVariable(dark, '--platform-text-base')), + bubbleBackground(dark, darkBrightestUnderlay), + ); + + expect(contrastRatio([0, 0, 0, 1], [255, 255, 255, 1])).toBeCloseTo(21); + expect(lightContrast).toBeGreaterThanOrEqual(4.5); + expect(darkContrast).toBeGreaterThanOrEqual(4.5); + }); +}); -- 2.52.0