feat(create): 对话页布局 - 加宽输入区并修正参数弹窗

开始对话后工具条进顶栏、模式切换横排到录音左侧,输入区加宽并去掉黑底;参数弹窗改用工作台表面色,去掉细线描边。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ddCat
2026-09-17 23:32:38 +08:00
parent 7143d54b64
commit 707b6494da
7 changed files with 627 additions and 234 deletions

View File

@@ -54,6 +54,7 @@
.app-product-workspace .creation-home .creation-submit.ant-btn:not(:disabled):hover .creation-submit-action::after { animation: creation-submit-sheen 640ms cubic-bezier(.16, 1, .3, 1); }
.app-product-workspace .creation-home .creation-submit.ant-btn:not(:disabled):hover .creation-submit-action svg { transform: translateY(-1px); }
.app-product-workspace .creation-home .creation-submit.ant-btn:not(:disabled):active .creation-submit-action { transform: scale(.97); }
.app-product-workspace .creation-home .creation-chat-composer.is-thread .creation-submit.ant-btn .creation-submit-action { height: 34px; padding: 0 14px; }
}
.creation-inspiration-filters { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 20px; }
.creation-inspiration-filters button { display: inline-flex; align-items: center; gap: 8px; padding: 8px 14px; min-height: 36px; border: 0; border-radius: 999px; color: var(--user-ink-muted); background: transparent; font-size: 13px; transition: background var(--user-motion), color var(--user-motion); }
@@ -120,3 +121,154 @@
.app-user-workspace .creation-submit-action { transition: none !important; transform: none !important; }
.app-product-workspace .creation-home .creation-submit.ant-btn:not(:disabled):hover .creation-submit-action::after { animation: none; }
}
/* 参数弹窗挂在 body overlays,必须直接用用户表面 token;描边由 Ant inner 的 1px border 造成。 */
.app-user-overlays .creation-control-popover,
.app-product-overlays .creation-control-popover {
--popover: var(--user-surface-raised);
--border: transparent;
--creation-text: var(--user-ink);
--creation-muted: var(--user-ink-muted);
--creation-faint: var(--user-ink-soft);
--control-selected-bg: var(--user-control-pressed);
--control-selected-fg: var(--user-ink);
}
.app-user-overlays .creation-control-popover .ant-popover-arrow,
.app-product-overlays .creation-control-popover .ant-popover-arrow { display: none; }
.app-user-overlays .creation-control-popover.ant-popover,
.app-user-overlays .creation-control-popover .ant-popover-container,
.app-user-overlays .creation-control-popover .ant-popover-inner,
.app-user-overlays .creation-control-popover-surface,
.app-product-overlays .creation-control-popover.ant-popover,
.app-product-overlays .creation-control-popover .ant-popover-container,
.app-product-overlays .creation-control-popover .ant-popover-inner,
.app-product-overlays .creation-control-popover-surface {
border: 0 !important;
outline: 0 !important;
background: var(--user-surface-raised) !important;
background-image: none !important;
box-shadow: 0 18px 40px rgba(0, 0, 0, .36) !important;
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
border-radius: 16px !important;
}
.app-user-overlays .creation-control-popover .ant-popover-inner,
.app-product-overlays .creation-control-popover .ant-popover-inner {
overflow: hidden;
padding: 0 !important;
}
.app-user-overlays .creation-parameter-menu,
.app-product-overlays .creation-parameter-menu { padding: 12px !important; }
.app-user-overlays .creation-parameter-section h3,
.app-user-overlays .creation-duration-heading > span,
.app-product-overlays .creation-parameter-section h3,
.app-product-overlays .creation-duration-heading > span { color: var(--user-ink-muted) !important; }
.app-user-overlays .creation-parameter-section > header > span,
.app-user-overlays .creation-duration-heading strong,
.app-product-overlays .creation-parameter-section > header > span,
.app-product-overlays .creation-duration-heading strong { color: var(--user-ink) !important; }
.app-user-overlays .creation-choice-grid,
.app-product-overlays .creation-choice-grid { gap: 6px !important; }
.app-user-overlays .creation-choice-grid button,
.app-user-overlays .creation-duration-choices button,
.app-product-overlays .creation-choice-grid button,
.app-product-overlays .creation-duration-choices button {
min-height: 40px !important;
border: 0 !important;
border-radius: 10px !important;
background: transparent !important;
box-shadow: none !important;
color: var(--user-ink-muted) !important;
}
.app-user-overlays .creation-choice-grid.is-resolution button,
.app-user-overlays .creation-choice-grid.is-quality button,
.app-user-overlays .creation-choice-grid.is-count button,
.app-user-overlays .creation-duration-choices button,
.app-product-overlays .creation-choice-grid.is-resolution button,
.app-product-overlays .creation-choice-grid.is-quality button,
.app-product-overlays .creation-choice-grid.is-count button,
.app-product-overlays .creation-duration-choices button { min-height: 34px !important; }
.app-user-overlays .creation-choice-grid button:hover,
.app-user-overlays .creation-duration-choices button:hover,
.app-product-overlays .creation-choice-grid button:hover,
.app-product-overlays .creation-duration-choices button:hover {
background: var(--user-surface-hover) !important;
color: var(--user-ink) !important;
}
.app-user-overlays .creation-choice-grid button.is-selected,
.app-user-overlays .creation-duration-choices button.is-selected,
.app-product-overlays .creation-choice-grid button.is-selected,
.app-product-overlays .creation-duration-choices button.is-selected {
background: var(--user-control-pressed) !important;
color: var(--user-ink) !important;
}
.app-user-overlays .creation-choice-grid .creation-ratio-preview > span,
.app-product-overlays .creation-choice-grid .creation-ratio-preview > span {
border: 0 !important;
border-radius: 3px !important;
background: currentColor;
opacity: .55;
}
.app-user-overlays .creation-choice-grid button.is-selected .creation-ratio-preview > span,
.app-product-overlays .creation-choice-grid button.is-selected .creation-ratio-preview > span { opacity: .92; }
.app-user-overlays .creation-custom-value,
.app-product-overlays .creation-custom-value {
border: 0 !important;
background: var(--user-surface-muted) !important;
color: var(--user-ink-muted) !important;
}
.app-user-overlays .creation-parameter-menu .image-size-tiers button,
.app-user-overlays .creation-parameter-menu .image-size-ratios button,
.app-user-overlays .creation-parameter-menu .image-size-auto,
.app-product-overlays .creation-parameter-menu .image-size-tiers button,
.app-product-overlays .creation-parameter-menu .image-size-ratios button,
.app-product-overlays .creation-parameter-menu .image-size-auto {
border: 0 !important;
background: var(--user-surface-muted) !important;
box-shadow: none !important;
color: var(--user-ink-muted) !important;
}
.app-user-overlays .creation-parameter-menu .image-size-tiers button[aria-pressed="true"],
.app-user-overlays .creation-parameter-menu .image-size-ratios button[aria-pressed="true"],
.app-user-overlays .creation-parameter-menu .image-size-auto[aria-pressed="true"],
.app-product-overlays .creation-parameter-menu .image-size-tiers button[aria-pressed="true"],
.app-product-overlays .creation-parameter-menu .image-size-ratios button[aria-pressed="true"],
.app-product-overlays .creation-parameter-menu .image-size-auto[aria-pressed="true"] {
background: var(--user-control-pressed) !important;
box-shadow: none !important;
color: var(--user-ink) !important;
}
.app-user-overlays .creation-parameter-menu .image-size-ratio-icon,
.app-product-overlays .creation-parameter-menu .image-size-ratio-icon {
border: 0 !important;
background: currentColor;
opacity: .55;
}
html:not(.dark) .app-user-overlays .creation-control-popover.ant-popover,
html:not(.dark) .app-user-overlays .creation-control-popover .ant-popover-container,
html:not(.dark) .app-user-overlays .creation-control-popover .ant-popover-inner,
html:not(.dark) .app-user-overlays .creation-control-popover-surface,
html:not(.dark) .app-product-overlays .creation-control-popover.ant-popover,
html:not(.dark) .app-product-overlays .creation-control-popover .ant-popover-container,
html:not(.dark) .app-product-overlays .creation-control-popover .ant-popover-inner,
html:not(.dark) .app-product-overlays .creation-control-popover-surface {
box-shadow: 0 16px 40px rgba(25, 25, 30, .12) !important;
}

View File

@@ -35,6 +35,7 @@ export type CreationMessage = {
export type CreationConversation = { id: string; title: string; updatedAt: string; canvasId?: string; messages: CreationMessage[] };
export const modeLabels: Record<CreationMode, string> = { text: "文本", image: "图片", video: "视频" };
export const defaultCreationMode: CreationMode = "image";
export const shotScriptLabels: Record<CreationMode, string> = { text: "创作思路", image: "画面指令", video: "镜头脚本" };
export const ratioOptions = [
{ value: "1:1", label: "方形" },

View File

@@ -1,9 +1,11 @@
import { ImageSizePicker } from "@/components/image-size-picker";
import { imageResolutionUsesQuality } from "@/lib/image-size-presets";
import { createPortal } from "react-dom";
import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type PointerEvent, type ReactNode, type RefObject } from "react";
import { App, Button, Dropdown, Popover } from "antd";
import { AppDrawer } from "@/components/ui/product/app-drawer";
import { AppModal } from "@/components/ui/product/app-modal";
import { useWorkspaceTopBarMount } from "@/components/layout/workspace-top-bar-extension";
import { Tooltip } from "@/components/ui/base/tooltip";
import { Reorder, LayoutGroup, motion, useReducedMotion } from "motion/react";
import { ArrowDown, ArrowUp, Brain, ChevronDown, ChevronLeft, ChevronRight, Clapperboard, Clock3, Copy, Download, FileText, Film, History, Image as ImageIcon, LoaderCircle, Maximize2, MessageSquareText, Minimize2, MoreHorizontal, Music2, Pencil, Plus, RefreshCw, Search, SlidersHorizontal, Sparkles, Trash2, UserRound, WandSparkles, Waves, X } from "lucide-react";
@@ -174,7 +176,8 @@ export function CreationWorkspaceToolbar({ shots, onJumpToShot, onNewConversatio
window.addEventListener("mousedown", onPointerDown);
return () => window.removeEventListener("mousedown", onPointerDown);
}, [railOpen]);
return <header className="creation-thread-toolbar">
const mount = useWorkspaceTopBarMount();
const toolbar = <header className="creation-thread-toolbar">
<div className="creation-toolbar-shots" ref={railRef}>
<button type="button" className="creation-rail-trigger" aria-expanded={railOpen} aria-haspopup="listbox" onClick={() => setRailOpen((open) => !open)}><Clapperboard />镜头时间线{shots.length > 0 ? <em className="creation-rail-count">{shots.length}</em> : null}</button>
{railOpen ? <div className="creation-rail-pop" role="listbox" aria-label="镜头时间线">
@@ -196,6 +199,9 @@ export function CreationWorkspaceToolbar({ shots, onJumpToShot, onNewConversatio
<Tooltip title="历史对话"><button type="button" aria-label="查看历史对话" className="creation-toolbar-action" onClick={onOpenHistory}><History /></button></Tooltip>
</div>
</header>;
if (mount) return createPortal(toolbar, mount);
if (mount === null) return null;
return toolbar;
}
export function CreationMessageView({ item, shotNumber, onRetryFailure, onCreateVariant, onEditUserMessage, onContinueCanvas, openingCanvas }: { item: CreationMessage; shotNumber: number; onRetryFailure: () => void; onCreateVariant: () => void; onEditUserMessage: (text: string) => void; onContinueCanvas: (ids?: string[]) => void; openingCanvas: boolean }) {
@@ -492,7 +498,6 @@ export function CreationComposer(props: ComposerProps) {
spotlightColor="color-mix(in srgb, var(--user-ink) 12%, transparent)"
spotlightRadius={280}
>
{props.variant === "thread" ? <div className="creation-composer-mode-row"><ModePicker mode={props.mode} onModeChange={props.onModeChange} /></div> : null}
<div className="creation-chat-writing-surface">
<div className="creation-chat-editor">
<CanvasResourceMentionTextarea ref={props.composerFocusRef} value={props.prompt} references={props.references} mentionMenuWidth={400} sendOnEnter onFocus={props.onPromptFocus} onChange={props.setPrompt} onSubmit={props.onSubmit} containerClassName="creation-chat-mention-container" className="creation-chat-mention-editor creation-scrollbar" style={{ color: "var(--creation-text)" }} placeholder={props.placeholderOverride || (props.variant === "empty" ? emptyPlaceholder : placeholder)} aria-label="创作提示词,可使用 @ 引用当前参考内容或技能;回车发送,Shift+回车换行" spellCheck disabled={interactionBusy} activeDropReferenceId={dropTargetReferenceId} onReferenceFilesDrop={(reference, files) => { const target = props.references.find((item) => item.id === reference.id); if (target?.attachmentId) props.onReplaceReferenceFiles(target.attachmentId, files); }} />
@@ -564,6 +569,7 @@ export function CreationComposer(props: ComposerProps) {
</div>
<footer className="creation-chat-dock">
<div className="creation-chat-controls">
{props.variant === "thread" ? <ModePicker mode={props.mode} onModeChange={props.onModeChange} /> : null}
<VoiceRecordingButton
className="creation-voice-trigger"
disabled={interactionBusy}
@@ -663,7 +669,7 @@ export function CreationModeTabs({ mode, onModeChange, agentActive = false, onAg
}
function ModePicker({ mode, onModeChange }: { mode: CreationMode; onModeChange: (mode: CreationMode) => void }) {
return <CreationModeTabs mode={mode} onModeChange={onModeChange} orientation="vertical" />;
return <CreationModeTabs mode={mode} onModeChange={onModeChange} />;
}
function GenerationSettingsMenu(props: ComposerProps) {

View File

@@ -29,7 +29,7 @@ import { createPluginHostContext } from "@/services/plugin-host";
import { usePluginStore } from "@/stores/use-plugin-store";
import { buildCreationMentionReferences, expandCreationPrompt, reconcileCreationAttachmentLimit, removeCreationReferenceTokens, replaceCreationAttachmentReference, selectedCreationReferences, type CreationReference } from "./creation-references";
import { creationAttachmentFromAsset, creationAttachmentFromAudio, creationAttachmentFromAudioAsset, creationAttachmentFromDocument, creationAttachmentFromExternalAsset, creationAttachmentFromImage, creationAttachmentFromVideo, creationAttachmentFromVideoAsset, creationAttachmentKind, creationAudioAsset, creationFileAccepted, creationImageAsset, creationMediaAspectRatio, creationUploadAccept, creationVideoAsset, removeCreationAttachment, splitCreationAttachments, type CreationAttachment } from "./creation-assets";
import { modeLabels, type CreationConversation, type CreationMessage, type CreationMode, type CreationRetryContext, type CreationSettings, type CreationShotRailEntry, type CreationStatus } from "./creation-types";
import { defaultCreationMode, modeLabels, type CreationConversation, type CreationMessage, type CreationMode, type CreationRetryContext, type CreationSettings, type CreationShotRailEntry, type CreationStatus } from "./creation-types";
import { attachCreationTaskContexts, completedCreationGenerationTask, conversationTimestamp, creationShotRail, creationVideoShotOrdinal, isImageAttachment, isVideoAttachment, materializeCreationTaskResults, newConversation, newMessage, reconcileCreationTaskMessages } from "./creation-conversations";
import { CreationComposer, CreationEmptySuggest, CreationFeaturedWorks, CreationHistoryDrawer, CreationMessageView, CreationModeTabs, CreationWorkspaceToolbar, creationAssetCategoryLabels } from "./creation-workspace";
import { CreationAgentEntry } from "./creation-agent-entry";
@@ -83,7 +83,7 @@ export default function CreatePage() {
const [activeId, setActiveId] = useState("");
const activeIdRef = useRef("");
const [hydrated, setHydrated] = useState(false);
const [mode, setMode] = useState<CreationMode>(() => initialComposerPreferences.mode || "video");
const [mode, setMode] = useState<CreationMode>(() => initialComposerPreferences.mode || defaultCreationMode);
const [prompt, setPrompt] = useState("");
const [attachments, setAttachments] = useState<CreationAttachment[]>([]);
const promptRef = useRef(prompt);
@@ -178,7 +178,7 @@ export default function CreatePage() {
useEffect(() => {
if (!composerPreferencesHydrated || composerPreferencesInitialized) return;
const saved = useCreationPreferencesStore.getState().preferences;
const nextMode = saved.mode || "video";
const nextMode = saved.mode || defaultCreationMode;
setMode(nextMode);
if (nextMode === "image" && saved.image) {
if (saved.image.ratio) setRatio(saved.image.ratio);