diff --git a/apps/ai-game-creator-shell/src/components/common/ComposerNoticeToast.tsx b/apps/ai-game-creator-shell/src/components/common/ComposerNoticeToast.tsx new file mode 100644 index 000000000..535c7b336 --- /dev/null +++ b/apps/ai-game-creator-shell/src/components/common/ComposerNoticeToast.tsx @@ -0,0 +1,56 @@ +import { PlatformRuntimeStatusToast } from '@genarrative/shared/components'; +import { useEffect, useRef } from 'react'; +import { createPortal } from 'react-dom'; + +import { COMPOSER_NOTICE_MILLIS, type ComposerNotice } from './composerNotice'; + +/** + * 输入区浮层提示的本体(底部居中 portal、6s 自动收起)。 + * + * 只由 `useComposerNoticeHost` 选中的宿主渲染;`data-composer-model-toast` 是各宿主用例的判据。 + */ +export function ComposerNoticeToast({ + notice, + onDismiss, +}: { + notice: ComposerNotice | null; + onDismiss: () => void; +}) { + /** 收起回调只从 ref 读:宿主每次渲染都换箭头函数,写进依赖会让计时器反复重起。 */ + const onDismissRef = useRef(onDismiss); + onDismissRef.current = onDismiss; + useEffect(() => { + if (!notice) { + return undefined; + } + const timer = window.setTimeout( + () => onDismissRef.current(), + COMPOSER_NOTICE_MILLIS, + ); + return () => { + window.clearTimeout(timer); + }; + }, [notice]); + + if (!notice) { + return null; + } + return createPortal( +
+ + {notice.message} + +
, + document.body, + ); +} diff --git a/apps/ai-game-creator-shell/src/components/common/composerNotice.ts b/apps/ai-game-creator-shell/src/components/common/composerNotice.ts new file mode 100644 index 000000000..201df4333 --- /dev/null +++ b/apps/ai-game-creator-shell/src/components/common/composerNotice.ts @@ -0,0 +1,91 @@ +import { useEffect, useRef, useState } from 'react'; + +/** 浮层提示的停留时长:与外壳(`LauncherNoticeToast`)与模板库提示同一档(6s)。 */ +export const COMPOSER_NOTICE_MILLIS = 6000; + +export type ComposerNotice = { + /** 每次提示自增:同一条文案再出现时也会重新弹一次并重起计时。 */ + id: number; + message: string; + /** + * 语气:失败(模型服务不可用 / 推理档保存失败)用 `error`;状态(模型列表已刷新、 + * 正在刷新模型列表、默认模型已停用等)用 `neutral`。 + */ + tone: 'error' | 'neutral'; +}; + +/** + * 输入区浮层提示的**模块级单例**:模型选择器与推理档控件都往这里发,宿主不必接任何回调。 + * + * 为什么是单例而不是各自 portal: + * 1. 输入区**不留常驻状态行**——那一排控件容不下第二行文字,一旦在控件里排一行提示,整颗 + * 控件会变成两行高、把触发钮顶到行线上方(#600 复核实测的错位); + * 2. 同一个输入区里可能同时挂着模型选择器与推理档控件(首页、策划、工作台都一样),各自 + * portal 会在同一位置叠出两条 toast。这里由**编号最小的那个宿主**渲染,保证同一时刻 + * 只有一条。 + */ +let notice: ComposerNotice | null = null; +let sequence = 0; +let hostSequence = 0; +const hosts = new Map void>(); + +function notifyHosts() { + for (const listener of hosts.values()) { + listener(); + } +} + +/** 发一条提示(每条都会重新弹一次并重起计时)。 */ +export function publishComposerNotice( + message: string, + tone: ComposerNotice['tone'] = 'error', +) { + sequence += 1; + notice = { id: sequence, message, tone }; + notifyHosts(); +} + +/** 收起当前提示(6s 计时结束或宿主主动收)。 */ +export function dismissComposerNotice() { + notice = null; + notifyHosts(); +} + +/** + * 在输入区里挂一个 toast 宿主。同一个输入区里多个调用点只有一个会真正渲染(编号最小的那个), + * 所以模型选择器与推理档控件可以各自调用,页面里也只会出现一条浮层。 + * + * 返回 `notice: null` 表示这次由别的宿主渲染(调用方照样把 `` 挂上即可)。 + */ +export function useComposerNoticeHost(): { + notice: ComposerNotice | null; + dismiss: () => void; +} { + const hostIdRef = useRef(null); + const [, forceRender] = useState(0); + + if (hostIdRef.current === null) { + hostSequence += 1; + hostIdRef.current = hostSequence; + } + + useEffect(() => { + const hostId = hostIdRef.current as number; + const listener = () => forceRender((value) => value + 1); + hosts.set(hostId, listener); + notifyHosts(); + return () => { + hosts.delete(hostId); + if (hosts.size === 0) { + // 输入区整体卸载(切页/测试清理)时把待显示的提示一并丢掉,避免留给下一个宿主。 + notice = null; + } + notifyHosts(); + }; + }, []); + + if (hosts.size === 0 || Math.min(...hosts.keys()) !== hostIdRef.current) { + return { notice: null, dismiss: dismissComposerNotice }; + } + return { notice, dismiss: dismissComposerNotice }; +} diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/ConversationModelSelect.tsx b/apps/ai-game-creator-shell/src/features/project-workspace/ConversationModelSelect.tsx index 040474bf1..e2116c9a8 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/ConversationModelSelect.tsx +++ b/apps/ai-game-creator-shell/src/features/project-workspace/ConversationModelSelect.tsx @@ -1,5 +1,5 @@ import { Check, ChevronDown, Lock, RefreshCcw } from 'lucide-react'; -import type { Ref } from 'react'; +import type { ReactNode, Ref } from 'react'; import { useCallback, useEffect, @@ -10,6 +10,11 @@ import { import { resolveTauriInvoke } from '../../app/tauri'; import type { GameCreatorAppConfigView } from '../../app/types'; +import { + publishComposerNotice, + useComposerNoticeHost, +} from '../../components/common/composerNotice'; +import { ComposerNoticeToast } from '../../components/common/ComposerNoticeToast'; import { cachedLlmModelCatalog, type ClientLlmModel, @@ -70,12 +75,22 @@ export function ConversationModelSelect({ onReady, projectPath, ref, + strengthSection, }: { className?: string; disabled: boolean; onReady?: (ready: boolean) => void; projectPath?: string; ref?: Ref; + /** + * 菜单顶部的附加区(Direct 输入盒用来放「思维强度」滑块)。 + * + * 传了它才会把弹层包一层 `.conversation-model-menu-shell`:外壳负责定位 / 描边 / 底色, + * 强度区在上、模型列表(`role="listbox"`)在下,滑块**不进** listbox 子树 + * (参数控件放进菜单角色里会让读屏进菜单模式后读不到滑块)。 + * 不传时渲染与改动前完全一致,策划输入盒、首页、画布都不受影响。 + */ + strengthSection?: ReactNode; }) { const initialCatalog = cachedLlmModelCatalog(); const [models, setModels] = useState( @@ -91,6 +106,9 @@ export function ConversationModelSelect({ const [busy, setBusy] = useState(!initialCatalog); const [error, setError] = useState(''); const [notice, setNotice] = useState(''); + /** 触发钮上显示的模型名(目录还没读完时用「模型」占位);同时用作触发钮的 `title`。 */ + const triggerLabel = + models.find((model) => model.id === selected)?.displayName ?? '模型'; const [manualRefreshBusy, setManualRefreshBusy] = useState(false); const [open, setOpen] = useState(false); const containerRef = useRef(null); @@ -109,6 +127,41 @@ export function ConversationModelSelect({ onReadyRef.current = onReady; }, [onReady]); + /** + * 失败与状态都走**组件自己的浮层**(`composerNotice` 单例 + toast 宿主):任何宿主都不留 + * 常驻 `role="alert"` / `role="status"` 行——输入区那一排控件容不下第二行文字,一旦排一行 + * 提示,整颗控件会变成两行高,把触发钮顶到行线上方(#600 复核实测的错位)。 + * 两条通道各自去重:同一条只报一次,清空后再出现可以再报。 + */ + const reportedErrorRef = useRef(''); + useEffect(() => { + if (error === '') { + reportedErrorRef.current = ''; + return; + } + if (reportedErrorRef.current === error) { + return; + } + reportedErrorRef.current = error; + publishComposerNotice(error, 'error'); + }, [error]); + + const reportedNoticeRef = useRef(''); + useEffect(() => { + if (notice === '') { + reportedNoticeRef.current = ''; + return; + } + if (reportedNoticeRef.current === notice) { + return; + } + reportedNoticeRef.current = notice; + publishComposerNotice(notice, 'neutral'); + }, [notice]); + + // 这个输入区里的 toast 宿主(同一输入区多个调用点只有一个真正渲染)。 + const composerNotice = useComposerNoticeHost(); + useEffect(() => { mountedRef.current = true; return () => { @@ -407,6 +460,75 @@ export function ConversationModelSelect({ } } + const modelMenu = ( +
+ {busy && models.length === 0 ? ( + 正在读取模型 + ) : null} + {models.map((model) => ( + + ))} + {/* 目录里存在但当前档位不可用的模型:不可点;原因通过 aria-label 与 hover 提示暴露, + 且刻意不进入 Tab 序列,避免键盘用户在不可选项上停留。 */} + {unavailableModels.map((model) => { + const reasonLabel = unavailableModelReasonLabel(model.reason); + return ( +
+ + {model.displayName} + +
+ ); + })} +
+ +
+ ); + return (
- {error ? {error} : null} - {notice ? {notice} : null} + {/* 失败与状态都只走浮层:控件里不再渲染常驻 `role="alert"` / `role="status"` 行。 */} + {open ? ( -
- {busy && models.length === 0 ? ( - - 正在读取模型 - - ) : null} - {models.map((model) => ( - - ))} - {/* 目录里存在但当前档位不可用的模型:不可点;原因通过 aria-label 与 hover 提示暴露, - 且刻意不进入 Tab 序列,避免键盘用户在不可选项上停留。 */} - {unavailableModels.map((model) => { - const reasonLabel = unavailableModelReasonLabel(model.reason); - return ( -
- - {model.displayName} - -
- ); - })} -
- -
+ strengthSection ? ( + /* 合并形态(Direct):强度区在上、模型列表在下,外壳负责定位与描边。 */ +
+ {strengthSection} + {modelMenu} +
+ ) : ( + modelMenu + ) ) : null}
); diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx b/apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx index 149e5f7f7..bf158680d 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx +++ b/apps/ai-game-creator-shell/src/features/project-workspace/ResourceReferenceInput.tsx @@ -114,6 +114,13 @@ type ResourceReferenceInputProps = { submitSuppressed?: boolean; /** 输入区操作排(编辑器右侧那一列)里宿主注入的内容,例如素材选择器的 `@` 触发钮。 */ inputActions?: ReactNode; + /** + * 把「AI 润色 / 恢复原文」这一排**渲染到宿主指定的容器里**(`createPortal`)。 + * + * Direct 输入区把它挪进下方那条工具条,让润色钮与 `@` / `+` / 模型 / 语音 / 发送 同一水平线 + * (否则它会自成一行,窄面板下看起来像"堆叠")。不传时仍然渲染在输入盒内(策划面等保持原样)。 + */ + actionsPortalTarget?: HTMLElement | null; inputRef?: RefObject; /** 输入区容器元素:宿主定位自己的浮层时用它当锚点。 */ containerRef?: RefObject; @@ -431,6 +438,7 @@ function ResourceReferenceEditor({ showPolishAction = true, submitSuppressed = false, inputActions, + actionsPortalTarget, composerRef, rootRef, }: ResourceReferenceInputProps & { @@ -842,48 +850,55 @@ function ResourceReferenceEditor({ return () => form.removeEventListener('submit', handleFormSubmit, true); }, [rootRef]); - return ( - <> -
- {inputActions} - {showPolishAction ? ( - <> + /** + * 「输入区操作排」这一块:宿主注入的 `inputActions` + 内置的「AI 润色 / 恢复原文」。 + * 默认渲染在输入盒里;宿主给了 `actionsPortalTarget` 时改渲染到那个容器(Direct 把它放进 + * 下方工具条,与 `@` / `+` / 模型 / 语音 / 发送 同一行)。 + */ + const inputActionsBlock = ( +
+ {inputActions} + {showPolishAction ? ( + <> + + {polishedOriginalText !== null ? ( - {polishedOriginalText !== null ? ( - - ) : null} - - ) : null} -
+ ) : null} + + ) : null} +
+ ); + + return ( + <> + {actionsPortalTarget + ? createPortal(inputActionsBlock, actionsPortalTarget) + : inputActionsBlock} {/* 提醒面板打开时错误提示只在面板里出现,输入区不重复显示。 */} {/* 「与原文相同 / 已截断」这类提示也要可见:只报失败会让「润色没变化」看起来像按钮坏了。 */} {showPolishAction && diff --git a/apps/ai-game-creator-shell/src/services/clientApi.ts b/apps/ai-game-creator-shell/src/services/clientApi.ts index 78236e2d4..915e6feaf 100644 --- a/apps/ai-game-creator-shell/src/services/clientApi.ts +++ b/apps/ai-game-creator-shell/src/services/clientApi.ts @@ -22,7 +22,7 @@ export type ClientLlmModelCatalog = LlmModelsResponse; export async function loadClientLlmModels(): Promise { const invoke = resolveTauriInvoke(); - if (!invoke) throw new Error('本地模型服务不可用'); + if (!invoke) throw new Error('模型服务不可用'); // Rust typed command 已经把响应解析成 `LlmModelsResponse`(含 unavailableModels)。 return invoke('load_game_creator_llm_models'); } diff --git a/apps/ai-game-creator-shell/src/services/llmModelCatalog.ts b/apps/ai-game-creator-shell/src/services/llmModelCatalog.ts index 0582f7c5e..cc73afc8c 100644 --- a/apps/ai-game-creator-shell/src/services/llmModelCatalog.ts +++ b/apps/ai-game-creator-shell/src/services/llmModelCatalog.ts @@ -30,7 +30,7 @@ export function notifyLlmConfigChanged() { async function loadEffectiveCatalog(epoch: number) { const invoke = resolveTauriInvoke(); if (!invoke) { - throw new LlmModelCatalogConfigError('本地模型服务不可用'); + throw new LlmModelCatalogConfigError('模型服务不可用'); } let config: GameCreatorAppConfigView | undefined; try { diff --git a/apps/ai-game-creator-shell/src/styles.css b/apps/ai-game-creator-shell/src/styles.css index e7776dc08..8c3b2535d 100644 --- a/apps/ai-game-creator-shell/src/styles.css +++ b/apps/ai-game-creator-shell/src/styles.css @@ -10593,14 +10593,9 @@ iframe.preview-frame { flex: 0 0 auto; } - /* 窄屏:输入框左右留白收紧,操作排(`@` / AI 润色 / 恢复原文)保持单行不换行, - 编辑器靠 `minmax(0, 1fr)` 吸收宽度,不会挤出横向滚动条。 */ - .game-workbench-chat - .project-chat-surface.is-direct-codex - .project-chat-composer - .resource-reference-input { - padding: 10px 12px 6px; - } + /* 窄屏输入框的内边距由六类选择器的 + `.… .project-chat-composer.is-direct-codex .resource-reference-input` 统一给(`padding: 0`), + 这里原有一条五类的 `padding: 10px 12px 6px` 恒不生效,已按「删死重」删除。 */ .game-workbench-chat .project-chat-surface, .game-workbench-chat .project-chat-conversation { @@ -11424,29 +11419,331 @@ button.design-workspace-tree__entry:hover, color: var(--platform-text-soft, #6b7280); font-size: 12px; } +/* ============================================================ + 模型菜单的**共享样式面**(任何宿主都拿同一套观感) + ============================================================ + 选择器只用组件自带类名(`.conversation-model-select` 一族),不带宿主祖先链:工作台输入区、 + 首页输入区等**任何** `ConversationModelSelect` 实例都得到同一套浮层观感(#600 复核后的口径)。 + 宿主专属的东西——工作台输入区的锚点、与工具栏的几何关系、强度区布局——留在各自作用域; + 这里只放卡片与列表本身,两边不各抄一份。 + 观感:卡片 16px 圆角 / 12px 内边距 / 极淡描边 / 既有暖色投影 token;列表行 34px、行圆角 9px、 + hover 极淡暖底、选中是轻量 pill;滚动条 6px 且平时透明(悬停控件才显形)。 + 颜色全部取 `--platform-*` token,不引入新色值。 */ + +/* 卡片:合并形态(带强度区)用外壳当卡片,独立形态菜单自己就是卡片——两处同一套值。 + 独立形态的锚点沿用上面那条基础规则(`position: absolute; right: 0; bottom: calc(100% + 8px)`), + 不在这里重复。 */ +.conversation-model-select .conversation-model-menu-shell, +.conversation-model-select .conversation-model-menu { + display: flex; + flex-direction: column; + width: max-content; + min-width: min(260px, calc(100vw - 24px)); + max-width: min(320px, calc(100vw - 24px)); + /* 独立形态:卡片自己就是滚动区,194 = 170 行高 + 上下各 12 内边距,与合并形态同高。 */ + max-height: min(194px, calc(100vh - 24px)); + padding: 12px; + border: 1px solid var(--platform-line-soft); + border-radius: 16px; + background: var(--platform-input-fill); + box-shadow: var(--platform-desktop-hover-shadow); + overflow: auto; + scrollbar-width: thin; + scrollbar-color: var(--platform-subpanel-border) transparent; + pointer-events: auto; +} + +/* 外壳里:强度区与列表之间只留 6px;列表只负责滚动与截断,卡片观感归外壳。 + 里层这几条必须带 `!important`:基础规则里那条 `.conversation-model-menu` 用 + `background: … !important` 声明了菜单底色(它服务的是"菜单自己就是卡片"的旧形态)。 */ +.conversation-model-select .conversation-model-menu-shell { + /* 锚点与独立形态的菜单同一套值(贴锚点右缘、向上展开、距锚点 8px):这是**组件级**的 + 弹层朝向,任何宿主都该一致;宿主只决定"锚点容器"在哪(首页 `.home-input-model-select`、 + 工作台控制排里的 `.conversation-model-select` 都是 `position: relative`)。 */ + position: absolute; + right: 0; + bottom: calc(100% + 8px); + z-index: 20; + gap: 6px; + /* 合并形态的高度由内容决定(强度区 + 列表各自的上下界已经定死), + 不再套用上面那条"卡片自己就是滚动区"的 194px 上限——那会把列表挤矮。 */ + max-height: none; +} + +.conversation-model-select + .conversation-model-menu-shell + .conversation-model-menu { + position: static !important; + width: 100% !important; + min-width: 0 !important; + max-width: none !important; + /* 170 = 5 行 × 34px:不会停在半行上;再长就滚动(细滚动条,悬停显形)。 */ + max-height: 170px !important; + padding: 0 !important; + border: 0 !important; + border-radius: 12px !important; + background: transparent !important; + box-shadow: none !important; +} + +/* 行:34px 高、9px 圆角、hover 极淡暖底,选中是轻量 pill(不做重色块)。 + 基础规则里同属性的旧值用 `!important` 声明,这里按同一档 `!important` 覆盖。 */ +.conversation-model-select .conversation-model-menu button { + min-height: 34px !important; + padding: 0 10px !important; + border-radius: 9px !important; +} + +/* 选项行:名字在左、勾在右(`space-between`);刷新行不是选项,保持**左对齐** + ——图标与「刷新模型列表」文本贴最左成一组,文本左边缘 = 图标右边缘 + 7px(见 + `.conversation-model-menu-refresh`)。这条按 `[role='option']` 限定,避免再波及刷新行。 */ +.conversation-model-select .conversation-model-menu button[role='option'] { + justify-content: space-between !important; +} + +.conversation-model-select .conversation-model-menu button:hover:not(:disabled), +.conversation-model-select .conversation-model-menu button:focus-visible { + background: var(--platform-nav-item-icon-fill) !important; + color: var(--platform-text-strong) !important; +} + +.conversation-model-select + .conversation-model-menu + button[aria-selected='true'] { + background: var(--platform-warm-bg) !important; + color: var(--platform-text-strong) !important; + font-weight: 600 !important; +} + +/* 列表宽度由卡片限定,超长模型名走省略号(旧形态靠 `width: max-content` 不截断)。 */ +.conversation-model-select .conversation-model-menu-option-main { + min-width: 0; + overflow: hidden; +} + +.conversation-model-select .conversation-model-menu-option-main > span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.conversation-model-select .conversation-model-menu::-webkit-scrollbar { + width: 6px; +} + +.conversation-model-select .conversation-model-menu::-webkit-scrollbar-track { + background: transparent; +} + +/* 静止时不画滚动条,鼠标进控件(或键盘焦点在控件内)才显形。 */ +.conversation-model-select .conversation-model-menu::-webkit-scrollbar-thumb { + border-radius: 999px; + background: transparent; +} + +.conversation-model-select:hover + .conversation-model-menu::-webkit-scrollbar-thumb, +.conversation-model-select + .conversation-model-menu:focus-within::-webkit-scrollbar-thumb { + background: var(--platform-subpanel-border); +} + +/* ---- 强度区(组件自带类名;任何宿主共用同一份,没有宿主分支) ---- */ + +/* 强度区:菜单顶部,与列表之间只用留白分开(不画横贯分隔线),标题左右层级分明。 + 左右不留内边距:滑块(轨道 + 圆钮)直接铺满卡片内容宽度,填充段与圆钮两端与轨道两端贴齐 + —— 这条内边距原先只是为档位点排布留位,点删掉后必须归零,否则两端各多 6px 空白。 */ +.project-chat-reasoning-effort.is-section { + display: grid; + /* 单列铺满:`auto` 轨道在这里只按 max-content 定尺,滑块会被压回固有宽度。 */ + grid-template-columns: minmax(0, 1fr); + justify-items: stretch; + width: 100%; + align-self: stretch; + gap: 10px; + padding: 4px 0 2px; +} + +/* 滑块:圆钮(20px)与轨道(6px)叠在一起,命中与按 `step` 吸附由原生 range 负责; + 档位圆点删掉后这里不再需要相对定位的容器(点是唯一的绝对定位子元素)。 */ +.project-chat-composer-strength-slider { + display: grid; + grid-template-columns: minmax(0, 1fr); + width: 100%; +} + +.project-chat-composer-strength-head { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 8px; + min-width: 0; +} + +.project-chat-composer-strength-title { + color: var(--platform-text-strong); + font-size: 12px; + font-weight: 600; +} + +.project-chat-composer-strength-range { + /* 圆钮(input 的 thumb)要压在档位点之上:实心钮不该被它下面那颗点透出来。 */ + position: relative; + z-index: 1; + width: 100%; + height: 26px; + margin: 0; + appearance: none; + border: 0; + background: transparent; + cursor: pointer; +} + +.project-chat-composer-strength-range::-webkit-slider-runnable-track { + height: 6px; + border-radius: 999px; + /* 已选段终点 = **圆钮中心**(`--strength-ratio` 是无单位档位比例;圆钮中心 = + 10px + ratio * (100% - 20px),两端各留一个圆钮半径)。原先终点写成 20px + …(= 圆钮 + **右缘**),亚像素 / 圆角 / 描边一有误差就在圆钮右侧露出一小截橙色。 */ + background: linear-gradient( + to right, + var(--platform-accent) 0 + calc(10px + var(--strength-ratio, 0) * (100% - 20px)), + var(--platform-subpanel-border) + calc(10px + var(--strength-ratio, 0) * (100% - 20px)) 100% + ); +} + +.project-chat-composer-strength-range::-webkit-slider-thumb { + width: 20px; + height: 20px; + margin-top: -7px; + appearance: none; + border: 1px solid var(--platform-subpanel-border); + border-radius: 999px; + /* 实心暖白圆钮:用 `--platform-panel-fill`(两个主题都是不透明面板色)而不是半透明的 + `--platform-input-highlight`——后者浅色主题只有 90% 不透明,下面的已选段会透出来, + 看起来就是个"靶心"。不再画中心点、也不用主色描边成空心环。 */ + background: var(--platform-panel-fill); + /* 圆钮柔影:从品牌主色派生(`color-mix`,不新造 token)。 */ + box-shadow: + 0 2px 6px color-mix(in srgb, var(--platform-accent) 24%, transparent), + 0 1px 2px color-mix(in srgb, var(--platform-accent) 16%, transparent); +} + +.project-chat-composer-strength-range:hover:not( + :disabled + )::-webkit-slider-thumb { + transform: scale(1.06); + /* 悬停把暖影压深一档(同族派生,不新造色值)。 */ + box-shadow: + 0 3px 9px color-mix(in srgb, var(--platform-accent) 30%, transparent), + 0 1px 3px color-mix(in srgb, var(--platform-accent) 20%, transparent); +} + +.project-chat-composer-strength-range:active:not( + :disabled + )::-webkit-slider-thumb { + transform: scale(0.96); +} + +.project-chat-composer-strength-range:focus-visible { + outline: none; +} + +.project-chat-composer-strength-range:focus-visible::-webkit-slider-thumb { + /* 「白钮 + 间隙 + 环」三明治:先铺一圈 2px 卡片底色当间隙,再叠焦点环——否则 3px 环 + 紧贴 20px 白钮会被读成"空心钮/靶心"。间隙色只能用**颜色** token + (`--platform-panel-fill` 是渐变,写进 `box-shadow` 会让整条声明失效、连环和影子一起丢), + 所以取 `--platform-input-fill`;环色与厚度不变,对比度不回退。 */ + box-shadow: + 0 0 0 2px var(--platform-input-fill), + 0 0 0 4px var(--platform-input-focus-ring), + 0 2px 6px color-mix(in srgb, var(--platform-accent) 24%, transparent); +} + +.project-chat-composer-strength-range:disabled { + cursor: not-allowed; + opacity: 0.55; +} + +/* Firefox 的滑块伪元素与 WebKit 不同名,同一套取值再来一份。 */ +.project-chat-composer-strength-range::-moz-range-track { + height: 6px; + border-radius: 999px; + background: var(--platform-subpanel-border); +} + +.project-chat-composer-strength-range::-moz-range-progress { + height: 6px; + border-radius: 999px; + background: var(--platform-accent); +} + +.project-chat-composer-strength-range::-moz-range-thumb { + width: 20px; + height: 20px; + /* 与 WebKit 同一套实心钮:暖白底 + 极淡描边 + 柔和暖影(不用主色描边成空心环)。 */ + border: 1px solid var(--platform-subpanel-border); + border-radius: 999px; + background: var(--platform-panel-fill); + box-shadow: + 0 2px 6px color-mix(in srgb, var(--platform-accent) 24%, transparent), + 0 1px 2px color-mix(in srgb, var(--platform-accent) 16%, transparent); +} + +/* 高对比(forced-colors)模式下 thumb 的 box-shadow 会被系统丢弃,焦点会完全不可见, + 这里回退到系统色 outline;只在 forced-colors 生效,亮色观感不受影响。 */ +@media (forced-colors: active) { + .project-chat-composer-strength-range:focus-visible { + outline: 2px solid Highlight; + outline-offset: 2px; + } +} + +/* Firefox 的焦点:先关掉轨道自带的 focus-outer 描边,再给圆钮同一套「间隙 + 环」。 */ +.project-chat-composer-strength-range::-moz-focus-outer { + border: 0; +} + +.project-chat-composer-strength-range:focus-visible::-moz-range-thumb { + box-shadow: + 0 0 0 2px var(--platform-input-fill), + 0 0 0 4px var(--platform-input-focus-ring), + 0 2px 6px color-mix(in srgb, var(--platform-accent) 24%, transparent); +} + +/* 档位圆点已删除(用户口径「很丑」,第一选择就是删):连同 `space-between` 排布、 + `z-index: 2` 抬层、`.is-active{opacity:0}` 与点的 `color-mix` 底色一起删掉, + 容器上那条「圆钮半径 − 半点宽」的 `padding: 0 7px` 也随之作废(滑块左右不再留白)。 + 历史成因(点被 6px 轨道盖住、与圆钮两端错位 ±12.2px)由 Git 与 pitfalls 追溯。 */ + +.project-chat-composer-strength-value { + color: var(--platform-text-soft); + font-size: 11px; + text-align: right; +} + .game-workbench-chat .project-chat-surface.is-direct-codex .project-chat-composer .conversation-model-select .conversation-model-menu button { + /* 只保留"抵消宿主通用按钮样式"的几何:工作台输入区的 `button` 有 28px 方块一族规则, + 尺寸 / 圆角 / 配色交给上面的共享样式面,避免宿主再抄一份观感。 + 这里**不再**统一写 `justify-content: space-between`——那样会连刷新行一起推到两端 + (图标在左、文案被顶到右)。对齐由共享面的「选项行 space-between / 刷新行 flex-start」 + 两条规则各管各的。 */ position: static !important; display: flex !important; align-items: center !important; - justify-content: space-between !important; width: 100% !important; min-width: 0 !important; - min-height: 32px !important; - height: 32px !important; margin: 0 !important; - padding: 0 9px !important; border: 0 !important; - border-radius: 6px !important; - background: transparent !important; - color: var(--platform-text-base, #374151) !important; box-shadow: none !important; font-size: 12px !important; - font-weight: 500 !important; white-space: nowrap !important; } .game-workbench-chat @@ -11467,8 +11764,6 @@ button.design-workspace-tree__entry:hover, .conversation-model-select .conversation-model-menu button[aria-selected='true'] { - background: #f3f1ee !important; - color: var(--platform-text-strong, #1f2937) !important; outline: none !important; } .conversation-model-menu-divider { @@ -11477,8 +11772,10 @@ button.design-workspace-tree__entry:hover, background: var(--platform-line-soft, #ece9e5); } .conversation-model-menu-refresh { + /* 刷新行不是选项:图标 + 文本贴最左成一组(选项行才用 `space-between`)。 + `gap` 取卡片内既有的 8px 节奏(与工具条列间距同值,不新造数)。 */ justify-content: flex-start !important; - gap: 7px !important; + gap: 8px !important; color: var(--platform-text-soft, #6b7280) !important; } .conversation-model-menu-refresh:hover, @@ -11527,7 +11824,9 @@ button.design-workspace-tree__entry:hover, .project-chat-composer .conversation-model-trigger { min-width: 0; - max-width: 180px; + /* 与下面控制排里那条同档(160px):模型名走省略号,宽度不够时由外层换行让位, + 不再把整组控件推出输入盒。 */ + max-width: 160px; height: 30px; } .project-chat-surface.is-direct-codex @@ -11580,8 +11879,12 @@ button.design-workspace-tree__entry:hover, .project-chat-composer.is-direct-codex { display: grid; grid-template-columns: minmax(0, 1fr); - grid-template-rows: auto auto; - gap: 0; + /* 单行工具条下限 252 内缩在盒内(左右各 12px 内边距 + 各 1px 边框)→ 外壳最小宽度 + 252 + 24 + 2 = **278**:低于它时不换行、不重叠,内容溢出可见(面板不裁切)。 */ + min-width: 278px; + /* 控件行 / 状态层之间靠行间距分开(状态层没有内容时为 0 高,不占行); + `gap` 同时给队列 chip 与输入盒之间留出同一档间距。 */ + gap: 8px; align-items: stretch; position: relative; right: 0; @@ -11606,8 +11909,10 @@ button.design-workspace-tree__entry:hover, .project-chat-composer.is-direct-codex .resource-reference-input { width: 100%; - min-height: 56px; - padding: 0 0 4px; + /* 盒子几何(高度下界 / 内边距)在这一条里定死:后面那条只补网格与行距, + 不再重复声明,避免"同权重靠后写取胜"的顺序依赖。 */ + min-height: 0; + padding: 0; border: 0; border-radius: 0; background: transparent; @@ -11645,11 +11950,8 @@ button.design-workspace-tree__entry:hover, color: var(--platform-text-soft); } -.game-workbench-layout--design .project-chat-composer-controls, -.game-workbench-chat - .project-chat-surface.is-direct-codex - .project-chat-composer.is-direct-codex - .project-chat-composer-controls { +/* 策划控件排:保持改动前的几何(不改策划面的间距与表现)。 */ +.game-workbench-layout--design .project-chat-composer-controls { display: flex; align-items: center; justify-content: space-between; @@ -11659,8 +11961,32 @@ button.design-workspace-tree__entry:hover, pointer-events: auto; } +/* Direct 控件排:**唯一一行控件,永远不换行**(`@` / `+` / AI 润色 / 模型 / 语音 / 发送 平铺)。 + 单行下限 = Σ 控件最小宽度 + 列间距:`@` 28 + `+` 28 + 润色 28 + 模型钮 64(`min-width: 64px`, + 名字走省略号)+ 语音 28 + 发送 28 = 204,加 5 个 8px 列间距 = 244;再留 8px 余量 → **252** + (夹具实测:自然宽度 290.4、下限 244)。低于 252 时不换行、不重叠,内容溢出可见。 */ +.game-workbench-chat + .project-chat-surface.is-direct-codex + .project-chat-composer.is-direct-codex + .project-chat-composer-controls { + display: flex; + flex-wrap: nowrap; + align-items: center; + justify-content: space-between; + gap: 6px 8px; + min-width: 252px; + pointer-events: auto; +} + +/* 策划控件排两侧分组:同样保持改动前的几何。 */ .game-workbench-layout--design .project-chat-composer-controls-left, -.game-workbench-layout--design .project-chat-composer-controls-right, +.game-workbench-layout--design .project-chat-composer-controls-right { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + .game-workbench-chat .project-chat-surface.is-direct-codex .project-chat-composer.is-direct-codex @@ -11673,16 +11999,33 @@ button.design-workspace-tree__entry:hover, .project-chat-composer-controls-right { display: flex; align-items: center; - gap: 8px; + gap: 6px 8px; min-width: 0; } +/* Direct 左侧动作组(`@` 引用 / `+` 附件 / AI 润色钮)同样不换行:工具条只有一行。 */ +.game-workbench-chat + .project-chat-surface.is-direct-codex + .project-chat-composer.is-direct-codex + .project-chat-composer-controls-left { + /* 左组是固定宽度的动作钮(`@` / `+` / 润色),不参与收缩:收缩会让它们溢出、 + 压到右侧的模型钮上;要收缩就由右组的模型钮(名字走省略号)承担。 */ + flex: 0 0 auto; + flex-wrap: nowrap; +} + +/* 右侧那一组是**不可再压的控件**(推理档 / 模型 / 语音 / 发送):它原来写死 + `flex: 0 0 auto`,宽度不够时既不收缩也不换行,整组向右溢出输入盒、和左侧的 + `@` `+` 叠在一起(窄面板下 `推理档` 直接压在 `添加文件` 上)。改成可收缩的一行: + 组内按 `flex-shrink` 让模型钮先让位(模型名自带省略号),一组控件永远保持单行、 + 整组在容器放不下时由外层换到第二行右对齐。 */ .game-workbench-chat .project-chat-surface.is-direct-codex .project-chat-composer.is-direct-codex .project-chat-composer-controls-right { - flex: 0 0 auto; + flex: 1 1 auto; margin-left: auto; + justify-content: flex-end; } /* 左侧引用按钮保留输入盒的内边距,点击区域不向外偏移。 */ @@ -11718,14 +12061,28 @@ button.design-workspace-tree__entry:hover, max-width: 180px; } +/* Direct 侧把模型控件收到 160px(与触发钮同档):模型名、错误提示都在这一档里用 + 省略号 / 折行收住,「@ + · 推理档 · 模型 · 语音 · 发送」在 400px 以上的面板里 + 保持一行。多带一层 `.project-chat-composer-controls-right`(真实父元素)是为了 + 严格高于上面那条共享规则的特异性,不依赖源码顺序;策划面仍走上一条的 180px。 */ +.game-workbench-chat + .project-chat-surface.is-direct-codex + .project-chat-composer.is-direct-codex + .project-chat-composer-controls + .project-chat-composer-controls-right + .conversation-model-select { + max-width: 160px; +} + .game-workbench-chat .project-chat-surface.is-direct-codex .project-chat-composer.is-direct-codex .project-chat-composer-controls .conversation-model-trigger { flex: 0 1 auto; - min-width: 0; - max-width: 180px; + /* 下限只是「别缩到看不见」的兜底:最窄的 280px 面板下模型钮仍有 ~74px。 */ + min-width: 64px; + max-width: 160px; color: var(--platform-text-base) !important; } @@ -11998,18 +12355,11 @@ button.design-workspace-tree__entry:hover, color: #a82f2f; } -.game-workbench-chat - .project-chat-surface.is-direct-codex - .project-chat-composer.is-direct-codex - .project-chat-reasoning-effort { - display: inline-flex; - align-items: center; - flex: 0 1 auto; - min-width: 0; - gap: 6px; -} - -/* 队列 chip:压在输入区上方,横向排布、超出换行。 */ +/* 独立「推理档」控件已经并入模型菜单(`.project-chat-reasoning-effort.is-section`, + 观感见文件上方「模型菜单的共享样式面」);这里那条给它排版的 + `.game-workbench-chat … .project-chat-reasoning-effort` 规则(`display: inline-flex` 一族) + 会以更高特异性盖掉共享面的 `display: grid`,已按「删死重」删除。 + 队列 chip:压在输入区上方,横向排布、超出换行。 */ .game-workbench-chat .project-chat-surface.is-direct-codex .project-chat-composer.is-direct-codex @@ -12018,7 +12368,8 @@ button.design-workspace-tree__entry:hover, flex-wrap: wrap; align-items: center; gap: 6px; - margin: 0 0 6px; + /* 与输入盒之间的间距归 composer 的行间距(`gap: 8px`),这里不再自带 6px。 */ + margin: 0; padding: 0; list-style: none; } @@ -12073,22 +12424,38 @@ button.design-workspace-tree__entry:hover, font-variant-numeric: tabular-nums; } -.game-workbench-layout--design .project-chat-composer-notice { - max-height: 96px; - overflow: auto; - overflow-wrap: anywhere; -} - +/* 提示段落的高度上限与折行:策划面保持改动前的 96px 一档,Direct 侧一并复用 + (原来它没有这条,长提示会把输入盒顶高)。 */ .game-workbench-layout--design .project-chat-composer-notice, .game-workbench-chat .project-chat-surface.is-direct-codex .project-chat-composer.is-direct-codex .project-chat-composer-notice { + max-height: 96px; + overflow: auto; + overflow-wrap: anywhere; +} + +/* 策划面:提示段落自带 4px 上间距(改动前的值,不动)。 */ +.game-workbench-layout--design .project-chat-composer-notice { margin: 4px 0 0; color: var(--platform-text-soft); font-size: 12px; } +/* Direct 侧:提示段落的上间距交给 composer 的 `gap: 8px`,段落自身归零。 + 多带一层 `.project-chat-conversation`(DirectProjectChatView 里的真实祖先)是为了 + 严格高于上面那条共享规则的特异性,不依赖源码顺序。 */ +.game-workbench-chat + .project-chat-surface.is-direct-codex + .project-chat-conversation + .project-chat-composer.is-direct-codex + > .project-chat-composer-notice { + margin: 0; + color: var(--platform-text-soft); + font-size: 12px; +} + /* 消息列表的最终几何,故意放在文件靠后的位置:它与上面那条同选择器同权重 (`.game-workbench-chat .project-chat-surface.is-direct-codex .project-chat-message-list`),按"后写胜出"把下面窄屏块之前可能出现的 @@ -12189,29 +12556,19 @@ button.design-workspace-tree__entry:hover, max-height: 24px; } +/* 输入区仍然生效的两条声明:文字色与可交互性。几何(`display` / `width` / `padding`)、 + 描边与底色全部由更高特异性的 + `.game-workbench-chat .project-chat-surface.is-direct-codex .project-chat-composer.is-direct-codex + .resource-reference-input` 那两条规则决定(六类 > 五类),这里的同属性声明恒不生效, + 已按「删死重」删除。 */ .game-workbench-chat .project-chat-surface.is-direct-codex .project-chat-composer .resource-reference-input { - display: grid; - width: 100%; - padding: 12px 15px 6px; - border-color: var(--platform-surface-border); - border-radius: 14px; - background: var(--platform-input-fill); color: var(--platform-text-base); pointer-events: auto; } -.game-workbench-chat - .project-chat-surface.is-direct-codex - .project-chat-composer - .resource-reference-input:focus-within { - border-color: var(--platform-surface-hover-border); - outline: none; - box-shadow: 0 0 0 3px var(--platform-input-focus-ring); -} - .game-workbench-chat .project-chat-surface.is-direct-codex .project-chat-composer @@ -12224,12 +12581,13 @@ button.design-workspace-tree__entry:hover, line-height: 1.6; } +/* 编辑器的行高与滚动:`min-height` / `max-height` 由更高特异性的 + `.… .project-chat-composer.is-direct-codex .resource-reference-input .resource-reference-input-editor` + 那条决定(六类 + 额外一层 `.resource-reference-input`),这里的旧值恒不生效,已删。 */ .game-workbench-chat .project-chat-surface.is-direct-codex .project-chat-composer .resource-reference-input-editor { - min-height: 96px; - max-height: 140px; overflow-y: auto; padding: 0; font-size: 13px; @@ -12280,7 +12638,9 @@ button.design-workspace-tree__entry:hover, grid-column: 1; align-self: center; min-width: 0; - margin-top: 0; + /* 状态行自带 6px 上间距(输入盒的 `gap` row 分量为 0:没内容时不留幻影行距, + 有内容时由这里给间距)。不要把 `margin-top: 0` 写回来——它会盖掉这一条。 */ + margin-top: 6px; } /* direct-codex 的 `@` 触发钮是下面控制排里的 `.project-chat-reference-trigger`, @@ -12942,33 +13302,55 @@ button.design-workspace-tree__entry:hover, /* ============================================================ 右侧对话面板:输入盒内文字与控件的对齐(2026-09) ============================================================ - 这一组必须放在文件最后:输入区的网格结构与「AI 润色」按钮的位置由更早的 - `.resource-reference-input` / `.resource-reference-input-actions` 规则决定, - 同特异性下只有靠后写的才能盖回去。 + 这一组是「文字 + 区内操作」两行网格的**唯一来源**,盒子几何(内边距 / 高度下界) + 则由前面那条六类 `.… .resource-reference-input` 规则定死:两边不再声明同一个属性, + 所以**不依赖源码先后**(放在文件末尾只是为了集中阅读与就近维护)。 - 输入区是两列网格(文本列 + 操作列),右侧那枚「AI 润色」按钮常年占掉 36px, - 于是文字右边缘比盒内右边缘少 36px——左右留白与上下留白看着就不一致。 - 这里把输入区改成单块并把操作按钮浮到右下角,文字可用满整宽。 */ + 输入区自己就是「文字 + 区内操作」的两行网格: + 第一行是编辑器(横跨整宽,文字用满盒内宽度); + 第二行左边是润色状态(`.resource-reference-input-status`)、右边是 + 「AI 润色 / 恢复原文」操作排(`.resource-reference-input-actions`)。 + 操作排**回到网格行里**,不再绝对定位浮到文本区右下角:它原来 `right: 0; + bottom: 0` 地压在编辑器最后一行文字上(实测按钮 28×24px 与编辑器相交;文字长到 + 贴近右下角时被盖住),并且和下方工具栏里的发送钮同一条竖线,看起来像"叠了两个按钮"。 */ .game-workbench-chat .project-chat-surface.is-direct-codex .project-chat-composer.is-direct-codex .resource-reference-input { - display: block; - position: relative; - min-height: 96px; + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + grid-template-rows: auto auto; + align-content: start; + /* 行间距归 0:状态层没有内容时第二行高度为 0,不留幻影行距;有内容时由状态元素 + 自带的 6px 上间距提供间隔(见 `.resource-reference-input-status` 那两条规则)。 */ + gap: 0 8px; } +/* 编辑器横跨两列:文字不再被右侧 28px 的操作列长期挤窄。 */ .game-workbench-chat .project-chat-surface.is-direct-codex .project-chat-composer.is-direct-codex .resource-reference-input - .resource-reference-input-actions { - position: absolute; - right: 0; - bottom: 0; - display: flex; - align-items: flex-end; - justify-content: flex-end; + .resource-reference-input-editor { + grid-row: 1; + grid-column: 1 / -1; + min-height: 72px; + max-height: 160px; +} + +/* 润色状态与操作排同一行、分列:状态出现/消失都不改变输入盒高度,两者也不会互相压。 */ +.game-workbench-chat + .project-chat-surface.is-direct-codex + .project-chat-composer.is-direct-codex + .resource-reference-input + .resource-reference-input-status { + grid-row: 2; + grid-column: 1; + align-self: center; + min-width: 0; + /* 状态行自带 6px 上间距(输入盒的 `gap` row 分量为 0)。 */ + margin-top: 6px; + overflow-wrap: anywhere; } /* 工具调用折叠块块头改成两行:第一行图标 + 汇总(超长省略),第二行状态 + 用时,箭头右侧跨两行。 @@ -13394,3 +13776,92 @@ details.design-agent-reasoning[open] text-align: center; white-space: pre-wrap; } + +/* ============================================================ + 输入区工具栏与状态提示(2026-10 · issue #600) + ============================================================ + **只作用于工作台 Direct 输入区**(`.game-workbench-chat .project-chat-surface + .is-direct-codex .project-chat-composer.is-direct-codex`)。策划输入区 + (`PlanningChatView` / `.game-workbench-layout--design`)不在本 issue 范围内, + 本次一行都不动:它的控件排几何、下拉宽度、提示段落 margin 全部保持改动前的值。 + 这一组只补前文没有的属性;模型钮的 160px 上限 / 64px 下限、推理档的 + `flex: 0 0 auto`、提示段落 margin 均写在前文各自那条规则里(Direct 侧用多一层 + 真实祖先提高特异性)。**本区块不依赖源码顺序**:这里要么是前文从未声明过的属性, + 要么用更长的选择器链严格盖过前文,把整块挪到文件中间结果也不变。 + ① 工具栏里的下拉控件(推理档 / 模型)要能收缩、模型名要能省略,宽度不够时靠换行 + 让位,而不是把整组推到输入盒外面去和左边的 `@` `+` 重叠; + ② 输入区不留常驻状态行:模型服务的失败走 `ComposerModelNoticeToast` 浮层提示 + (控件排容不下第二行文字,实测会把触发钮顶到行线上方,见下面 ② 段); + ③ 状态提示区(队列 chip、提示段落)与输入盒、工具栏共用同一档行间距; + ④ 思维强度并入模型菜单:底行只留模型触发钮一颗控件,强度区(原生 ``) + 嵌在菜单顶部,与模型列表共用同一张卡(`.conversation-model-menu-shell`)。 */ + +/* 工具条里的「AI 润色 / 恢复原文」挂点(`ResourceReferenceInput` 用 portal 渲染进来): + 位于**右组发送钮之前**——润色钮与发送钮同为圆角矩形按钮,相邻成组;空输入态只是不显示 + 按钮,不占额外行。DOM 顺序即 `模型 → 麦克风 → 润色 → 发送`(Tab / 读屏顺序同视觉)。 */ +.game-workbench-chat + .project-chat-surface.is-direct-codex + .project-chat-composer.is-direct-codex + .project-chat-composer-controls-actions { + display: inline-flex; + /* 不参与收缩:里面的润色钮是 28px 固定方块,槽位被压窄会让它溢出去压到 `+`。 */ + flex: 0 0 auto; + align-items: center; + min-width: 0; +} + +.game-workbench-chat + .project-chat-surface.is-direct-codex + .project-chat-composer.is-direct-codex + .project-chat-composer-controls-actions + .resource-reference-input-actions { + display: flex; + flex: 0 0 auto; + align-items: center; + gap: 6px; +} + +/* ① 下拉控件:可收缩(模型名超长走省略号);整条工具条不换行,控件排只占一行。 */ +.game-workbench-chat + .project-chat-surface.is-direct-codex + .project-chat-composer.is-direct-codex + .project-chat-composer-controls + .conversation-model-select { + flex-wrap: nowrap; + justify-content: flex-end; +} + +.game-workbench-chat + .project-chat-surface.is-direct-codex + .project-chat-composer.is-direct-codex + .project-chat-composer-controls + .conversation-model-trigger-label { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +/* 上面两条给独立「推理档」控件排版的规则(`flex-wrap` / `justify-content` / 提示整行) + 随该控件并入模型菜单一起退役:现役元素是 `.project-chat-reasoning-effort.is-section` + (网格,观感见「模型菜单的共享样式面」),`flex-*` 对它无效。 */ + +/* ② 模型控件的错误 / 状态提示:**不在控件里渲染**。 + 这一段原来把 `.conversation-model-select [role='alert']` 从 `bottom: 36px` 浮层收回 + 控件排内独占一行;实测这样会让整颗模型控件变成两行高,在 `align-items: center` 的 + 工具条里把触发钮顶到行线上方、提示落到行线下方(用户复核反馈的"完全没对齐")。 + 现在失败与状态一律走组件自己的底部浮层(`components/common/composerNotice.ts` + + `ComposerNoticeToast`,任何宿主都一样):控件排内不再有这两颗元素,本条规则恒无命中对象,已删。 */ + +/* ③ 状态提示区(队列 chip、提示段落)与输入盒、工具栏共用同一档行间距: + 队列 chip 与提示段落的 `margin` 都写回前文各自那条规则。 */ + +/* ④ 思维强度**并入模型菜单**:底行只留模型触发钮一颗控件,强度区嵌在菜单顶部。 + 外壳(`.conversation-model-menu-shell`)的锚点与朝向是**组件级**的(见文件上方「模型菜单的 + 共享样式面」:`position: absolute; right: 0; bottom: calc(100% + 8px)`,首页那颗同一套); + 这里不再有工作台专属的浮层 / 卡片 / 强度区规则。 + 强度区里是原生 ``,自带 `role="slider"`、`aria-valuemin/max/now` + 与 ←/→/Home/End,`aria-valuetext` 报中文档位名,点轨道或拖拽按 `step` 吸附到档位。 */ + +/* 强度区的观感(标题 / 滑块 / 档位点)同样已经抬到文件上方「模型菜单的共享样式面」一节, + 选择器只用组件自带类名(`.project-chat-reasoning-effort.is-section` 一族), + 首页与工作台共用同一份;这里不再有工作台专属的强度区规则。 */ diff --git a/apps/ai-game-creator-shell/src/view/home/index.tsx b/apps/ai-game-creator-shell/src/view/home/index.tsx index 64307288d..97dab66c9 100644 --- a/apps/ai-game-creator-shell/src/view/home/index.tsx +++ b/apps/ai-game-creator-shell/src/view/home/index.tsx @@ -16,6 +16,7 @@ import type { ProjectStartMode } from '../../app/types'; import { resolveHomeCreationFailureMessage } from '../../features/app-shell/homeCreationFailure'; import { ConversationModelSelect } from '../../features/project-workspace/ConversationModelSelect'; import type { GameTemplateEntry } from '../../features/template-library/templateLibraryModel'; +import { ComposerReasoningEffortSelect } from '../project-development/chat/components/DirectProjectComposer/ComposerControls'; import RichInputArea, { UploadButton } from './components/RichInputArea'; import { richTextToAttachments, @@ -297,6 +298,14 @@ export default function HomeView({ + } /> + {open ? ( +
+ {options.map((option) => ( + + ))} +
+ ) : null} +
+ ); + + /* + 强度区(Direct 输入盒):嵌在模型菜单顶部,底行因此只留一颗控件。 + 原生 `` 自带 `role="slider"` 与 ←/→/Home/End 键盘行为, + 点轨道(含拖拽)按 `step` 吸附到档位,`aria-valuetext` 报中文档位名; + 浮层的开合与 Esc / 点外部关闭由模型菜单自己负责(本区不再自带触发钮与浮层)。 + */ + const strengthSection = ( + + + 思维强度 + + {`当前:${activeLabel}`} + + + + {/* 原生 range 的 role/值域本来就成立,这里再显式写一遍:`aria-valuetext` 是 + 中文档位名,读屏念的是「最高」而不是「4」。 */} + + {/* 档位圆点已删除(用户口径「很丑」,第一选择就是删):轨道本身就是刻度, + 命中与按 `step` 吸附仍由原生 range 负责,`aria-valuetext` 报中文档位名。 */} + + + ); + return ( - {/* 与模型选择器同一套观感:复用 `conversation-model-*` 的触发钮与浮层样式, - 不再用原生 `` + 5 个档位点),与模型列表共用同一张卡(`.conversation-model-menu-shell`,向上展开、贴触发钮右缘,里层 `.conversation-model-menu` 回到文档流只负责滚动)。 +- **滑块不能放进 `role="listbox"` 子树**:参数控件混进菜单角色会让读屏进菜单模式后读不到它,所以外壳是普通容器(强度区 + listbox 并列),只能靠 `strengthSection` 这个显式 prop 由宿主注入。 +- 档位枚举、默认值、持久化通道(读 `read_game_creator_app_config`、写 `select_game_creator_reasoning_effort`)与旧下拉**完全一致**;`ComposerReasoningEffortSelect` 的 `presentation` 默认 `'dropdown'`(策划输入盒继续用,零语义变化),Direct 用 `'section'`;`ConversationModelSelect` 不传 `strengthSection` 时渲染与改动前逐字一致(首页 / 策划 / 画布不受影响)。 +- 关掉旧的独立触发钮(底行元素 2 → 1);浮层的开合、Esc 与点击外部关闭都由模型菜单既有行为负责,强度区不再自带浮层。 +- **取舍**:强度区随菜单挂载,打开时先显示「默认」再回读成落盘档位(一次 IPC 往返),测试里要 `waitFor`;换掉这一点需要把档位状态提到宿主,暂不做。 +- **判据/取证**:`tests/appSurface/chat-composer.suite.ts`(底行不再有第二颗控件、菜单里改档落同一条持久化通道、Esc/点外部关闭、滑块不在 listbox 内)+ `tests/chatDialogFrameLayout.test.ts`(外壳向上展开 / 强度区网格 / 底行高度不增)+ `tests/appSurface/design-agent.suite.ts`(策划菜单里没有滑块)。 +- **视觉口径**:卡片与滑块的观感照 Codex 参考图,颜色只取我们自己的暖色 token——卡片圆角 16px、内边距 12px、极淡描边(`--platform-line-soft`)+ 既有暖色投影 token `--platform-desktop-hover-shadow`、**不画横贯分隔线**(只靠留白);强度区标题 `--platform-text-strong` 12/600、右侧当前档位 `--platform-text-soft` 11px;轨道 6px 圆头,已选段 `--platform-accent`、未选段 `--platform-subpanel-border`(进度由组件写进来的 `--strength-ratio` 驱动渐变 `calc(20px + var(--strength-ratio) * (100% - 20px))`),圆钮 20px **实心**(`--platform-panel-fill`,不用半透明 token,避免透出已选段变成"靶心")+ `--platform-subpanel-border` 1px 描边 + 从 `--platform-accent` 派生的 `color-mix` 柔影;档位点 7px 绝对定位叠在轨道**之上**(`z-index: 2` > range 的 1)、当前档位那一点隐藏(见本文件顶部 2026-10-05 那条的对齐算式与实测);模型列表行高 34px、行圆角 9px、hover `--platform-nav-item-icon-fill`、选中 `--platform-warm-bg` 轻量 pill + 右侧对勾;列表 `max-height: 170px`(=5×34,不出现半行),滚动条 6px 且平时透明、悬停卡片才显形。 +- **实测(无头 Chromium 夹具,447px / 900px)**:卡片 260×263(圆角 16 / 内边距 12)、强度区 234×61、滑块 222×26、档位点 7px 且中心与轨道中线重合(点 270 / 轨道 269)、列表行高 34、`max-height` 170(内容 315 → 滚动)、默认档进度 0%,按 `End` 到 100% 且文案变「当前:最高」;卡片右缘未溢出视口。 + +- **关联**:`features/project-workspace/ConversationModelSelect.tsx`、`view/project-development/chat/components/DirectProjectComposer/{DirectProjectComposer.tsx,ComposerControls.tsx}`、`src/styles.css`。 + ## 2026-10-03 后台异步回填被「进项目」覆盖:AI 项目名只在兜底名仍成立时改 - **现象**(Issue 599):首页建项把 AI 命名改成「与建项并行、结果后台回填」后,偶尔工作台标题停在兜底名(`GameAgent 项目 <短 id>`),而磁盘 manifest 已经是 AI 名字。 @@ -97,6 +139,15 @@ - **判据/取证**:`npx vitest run apps/ai-game-creator-shell/tests/homeProjectNamingAsync.test.tsx` 的「keeps the AI name when the naming result lands before the project is entered」(去掉等待即复现兜底名覆盖);appSurface 的「syncs the workbench title and recent project list with the AI name after creation」。 - **边界**:条件改名的判据必须由宿主校验(项目 ID + 当前名称仍是兜底名),前端只转述 `expectedProjectId` / `expectedName`;用户已手动改名时宿主返回 `renamed: false`,回填整体跳过、绝不覆盖用户输入。 - **关联**:`apps/ai-game-creator-shell/src/features/app-shell/useHomeProjectCreation.ts`、`apps/ai-game-creator-shell/src-tauri/src/commands.rs`。 + +## 2026-10-03 AGC 输入区控件叠在一起:输入区内的操作排与状态提示不能用绝对定位 + +- **现象**:#600 截图里用户输入区域「元素堆叠」——空草稿时「AI 润色」按钮就压在输入内容区右下角(实测与编辑器相交 28×24px),模型控件的失败提示「本地模型服务不可用」直接画在第 3~4 行文字上(70×36px,长草稿时把整段文字盖住);面板越窄越糟:280px 面板下推理档整颗压在「添加文件」上(28×28px),「插入素材引用」与推理档也相交(20×28px)。 +- **原因**:两条「靠绝对定位把控件挪出流」的旧写法叠在一起:① `.resource-reference-input-actions` 被写成 `position: absolute; right: 0; bottom: 0`,浮到编辑器右下角(同一份样式早前其实是网格第二行,见文件里 11893 行那条注释,被后写的绝对定位规则盖掉);② `ConversationModelSelect` 的 `[role='alert'] / [role='status']` 是 `bottom: 36px` 的浮层,锚点就在输入盒正下方,提示必然落在文字上。另外 `.project-chat-composer-controls-right` 写死 `flex: 0 0 auto`,宽度不够时既不收缩也不换行,整组向右溢出输入盒(360px 面板下溢出 34px、280px 下 114px)。 +- **处理(现行口径)**:`.project-chat-composer.is-direct-codex` 是单列网格(`gap: 8px`);**控件全部集中在唯一一行工具条**——左组 `@` / `+`,右组 模型 / 语音 / **「AI 润色 / 恢复原文」(紧贴发送左侧)** / 发送,六颗 `flex-wrap: nowrap` 平铺(润色钮由 `ResourceReferenceInput` 的 `actionsPortalTarget` portal 进右组发送钮之前的挂点,不再自成一行;它与发送钮同为圆角矩形按钮,相邻成组,DOM / Tab 顺序都是 模型 → 麦克风 → 润色 → 发送);状态/提示层只在有内容时占行(空输入态第二行为 0 高、行间距为 0,状态自己带 6px 上间距)。窄宽度用 `min-width` 兜(控件排 252px / 外壳 278px)+ 横向溢出可见:**不换行、不堆叠、不重叠**,发送钮仍可点击(夹具 `elementFromPoint` 命中)。模型钮封顶 160px 且名字走省略号(收缩由它承担),左组是固定 28px 动作钮、`flex: 0 0 auto` 不参与收缩。**模型失败提示后来改成走浮层**(见本文件顶部 2026-10-04 / 2026-10-05 两条):控件排里既不留常驻红字、也不留常驻状态文字——「控件排内独占一行」那次快照已退役,行高回到由模型触发钮决定。 +- **判据/取证**:`apps/ai-game-creator-shell/tests/chatDialogFrameLayout.test.ts` 用层叠求值钉住单行工具条(240 / 280 / 296 / 320 / 360 / 438 / 900 七个宽度下 `flex-wrap` 都是 `nowrap`、控件排 `min-width: 252px`、外壳 `min-width: 278px`)、输入盒最高高度算式(12 盒内上内边距 + 160 编辑器 + 0 输入区行距 + 0 状态行 + 0 输入区下内边距 + 8 输入盒与工具栏行距 + **30 工具栏行高(模型触发钮 30px 撑起来,发送/语音是 28px)** + 12 盒内下内边距 = **222**),以及组件源码里润色钮渲染在工具条挂点(`actionsPortalTarget={controlsActionsSlot}`、挂点在控件排之后);`tests/appSurface/project-development.suite.ts` 钉住 composer 的 `gap: 8px`。真实渲染对照见 #600 PR:同一夹具在 495/438/360/296/280px 面板 × 空 / 长多行 / 队列+提示 四态下,改前 2~4 处相交(含文字被提示盖住),改后 0 处相交、0 处溢出。 +- **关联**:`apps/ai-game-creator-shell/src/styles.css`(文件末尾「输入区工具栏与状态提示」区块)、`docs/【功能说明】AGC聊天AI润色与发送前提醒-2026-09-10.md`。 + ## 2026-10-03 AGC 随包 plugins 的 feature 档位必须与消费方一致,且门禁会因 build.rs 未重跑而假通过 - **现象**:Windows 本机 `npm run check:generated-bindings`(`npm run lint` 链内,`scripts/check-repository-ci.sh` 的 Repository checks 也走它)在 `build.rs:167:29` panic:`插件随包资源校验失败:随包插件存在未声明文件:.../src-tauri/resources/plugins/agc-godot-editor/native/gdextension/bin/win-x64/agc_godot_editor.dll(目标 x86_64-pc-windows-msvc 与当前 feature 组合不允许;请先执行随包资源准备步骤)`;树上换成 `agc-unity-editor/dotnet/publish/win-x64/Agc.Unity.Attach.exe` 时报同一类错。反向还有更隐蔽的形态:门禁 2 秒就 exit 0 说「通过」,但 tree 上其实带着编辑器产物。