fix(canvas): 创作输入栏 - 分离模型价格展示并收敛录音与附件入口

This commit is contained in:
ddCat
2026-09-01 20:01:52 +08:00
parent 0330e64857
commit 41d99374ca
5 changed files with 29 additions and 44 deletions

View File

@@ -108,7 +108,7 @@ export function VoiceRecordingInline({ onTranscribed, onCancel }: VoiceRecording
return (
<div
className="flex min-w-0 flex-1 items-center gap-2 rounded-lg border px-2 py-1.5"
className="voice-recording-inline flex min-w-0 flex-1 items-center gap-2 rounded-lg border px-2 py-1.5"
style={{ background: theme.toolbar.panel, borderColor: theme.toolbar.border }}
>
{displayError ? (

View File

@@ -24,6 +24,7 @@ type ModelPickerProps = {
placeholder?: string;
onMissingConfig?: () => void;
showSelectedPrice?: boolean;
showOptionPrices?: boolean;
variant?: "default" | "creation";
requirements?: ModelRequirements;
showConfiguredModelName?: boolean;
@@ -40,6 +41,7 @@ export function ModelPicker({
placeholder = "选择模型",
onMissingConfig,
showSelectedPrice = true,
showOptionPrices = showSelectedPrice,
variant = "default",
requirements,
showConfiguredModelName = false,
@@ -226,7 +228,7 @@ export function ModelPicker({
theme={theme}
creationVariant={creationVariant}
showConfiguredModelName={showConfiguredModelName}
showPrice={showSelectedPrice && creditsEnabled}
showPrice={showOptionPrices && creditsEnabled}
disabledReason={disabledReason}
/>
{selected ? <Check className="canvas-model-picker-option-check" style={{ color: theme.node.activeStroke }} /> : null}

View File

@@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type CSSProperties, type PointerEvent, type ReactNode, type RefObject } from "react";
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type PointerEvent, type ReactNode, type RefObject } from "react";
import { App, Button, Drawer, Modal, Popover, Spin, Tooltip } from "antd";
import { Reorder } from "motion/react";
import { ArrowDown, ArrowUp, Check, ChevronDown, ChevronLeft, ChevronRight, Clapperboard, Clock3, Copy, Download, FileText, Film, History, Image as ImageIcon, LoaderCircle, Maximize2, MessageSquareText, Minimize2, Music2, Paperclip, Plus, RefreshCw, Search, SlidersHorizontal, Sparkles, Trash2, WandSparkles, X } from "lucide-react";
import { ArrowDown, ArrowUp, Check, ChevronDown, ChevronLeft, ChevronRight, Clapperboard, Clock3, Copy, Download, FileText, Film, History, Image as ImageIcon, LoaderCircle, Maximize2, MessageSquareText, Minimize2, Music2, Plus, RefreshCw, Search, SlidersHorizontal, Sparkles, Trash2, WandSparkles, X } from "lucide-react";
import { Link } from "react-router";
import { AIMessageMarkdown } from "@/components/ai/ai-message-markdown";
@@ -171,7 +171,6 @@ export default function CreatePage() {
const [libraryOpen, setLibraryOpen] = useState(false);
const externalAssetSources = useExternalAssetSources(libraryOpen);
const abortRef = useRef<AbortController | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const composerFocusRef = useRef<HTMLTextAreaElement>(null);
const threadScrollRef = useRef<HTMLElement>(null);
const followLatestMessageRef = useRef(true);
@@ -418,27 +417,6 @@ export default function CreatePage() {
attachment: creationAttachmentFromImage(file, uploaded),
};
};
const addAttachments = (files: FileList | File[]) => {
if ((mode === "image" || mode === "video") && maxReferences === 0) {
toast.warning(mode === "image" ? "当前图片模型不支持参考图" : "当前模型不支持图生视频");
return;
}
const next = Array.from(files)
.filter((file) => creationFileAccepted(mode, file))
.slice(0, Math.max(0, maxReferences - attachments.length));
if (!next.length) return;
void Promise.allSettled(next.map(async (file) => {
const { asset, attachment } = await uploadCreationAsset(file);
if (asset) addAsset(asset);
return attachment;
})).then((settled) => {
const items = settled.flatMap((entry) => entry.status === "fulfilled" ? [entry.value] : []);
const failed = settled.filter((entry) => entry.status === "rejected");
if (items.length) setAttachments((current) => [...current, ...items].slice(0, maxReferences));
if (failed.length) toast.error(`${failed.length} 个参考素材上传失败,请重试`);
});
};
const uploadLibraryAssets = async (files: FileList | File[]) => {
const next = Array.from(files).filter((file) => creationFileAccepted(mode, file));
if (!next.length) return [];
@@ -453,11 +431,6 @@ export default function CreatePage() {
return assetIds;
};
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
if (event.target.files) addAttachments(event.target.files);
event.target.value = "";
};
const handleLibrarySelect = (selectedIds: string[]) => {
const next = selectedIds.flatMap((id): CreationAttachment[] => {
const asset = assets.find((item) => item.id === id);
@@ -893,8 +866,6 @@ export default function CreatePage() {
onReplaceAttachment: replaceReferenceFromTrack,
onReplaceReferenceFiles: replaceReferenceFromFiles,
onOpenLibrary: () => setLibraryOpen(true),
fileInputRef,
onFileChange: handleFileChange,
onModeChange: selectMode,
model: selectedModel,
modelRequirements,
@@ -1164,8 +1135,6 @@ type ComposerProps = {
onReplaceAttachment: (targetAttachmentId: string, replacement: CreationAttachment) => void;
onReplaceReferenceFiles: (targetAttachmentId: string, files: File[]) => void;
onOpenLibrary: () => void;
fileInputRef: RefObject<HTMLInputElement | null>;
onFileChange: (event: ChangeEvent<HTMLInputElement>) => void;
onModeChange: (mode: CreationMode) => void;
model: string;
modelRequirements: ModelRequirements;
@@ -1327,7 +1296,6 @@ function CreationComposer(props: ComposerProps) {
};
const composer = <section className={`creation-chat-composer is-${props.variant}`}>
<div className="creation-chat-writing-surface">
<input ref={props.fileInputRef} type="file" hidden accept={creationUploadAccept(props.mode)} multiple onChange={props.onFileChange} />
<div className="creation-chat-editor">
<CanvasResourceMentionTextarea ref={props.composerFocusRef} value={props.prompt} references={props.references} mentionMenuWidth={400} sendOnEnter={false} 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="创作提示词,可使用 @ 引用当前参考内容或技能" 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); }} />
{props.attachments.length || referencesSupported ? <div className={`creation-reference-panel${trackState.isExpanded ? " is-expanded" : ""}`} aria-busy={interactionBusy}>
@@ -1416,8 +1384,7 @@ function CreationComposer(props: ComposerProps) {
<span>优化</span>
</button>
</Tooltip> : null}
<Tooltip title="从本机上传附件"><button type="button" className="creation-chat-control" onClick={() => props.fileInputRef.current?.click()} disabled={interactionBusy || !referencesSupported} aria-label="从本机上传附件"><Paperclip /><span>附件</span></button></Tooltip>
<ModelPicker config={props.config} value={props.model} onChange={props.onModelChange} capability={props.mode} requirements={props.modelRequirements} className="creation-model-picker" placeholder={`选择${modeLabels[props.mode]}模型`} showSelectedPrice={false} variant="creation" />
<ModelPicker config={props.config} value={props.model} onChange={props.onModelChange} capability={props.mode} requirements={props.modelRequirements} className="creation-model-picker" placeholder={`选择${modeLabels[props.mode]}模型`} showSelectedPrice={false} showOptionPrices variant="creation" />
{props.mode === "video" || (props.mode === "image" && imageSettingsSupported) ? <GenerationSettingsMenu {...props} /> : null}
{props.mode === "video" ? <DurationMenu profile={props.videoProfile} seconds={props.seconds} onChange={props.setSeconds} /> : null}
</div>

View File

@@ -10816,6 +10816,23 @@ html:not(.dark) .creation-home .creation-chat-control:hover,
html:not(.dark) .creation-home .creation-chat-control[aria-expanded="true"],
html:not(.dark) .creation-home .creation-chat-composer .creation-model-picker.canvas-composer-model-picker:hover { background: rgba(28, 32, 37, .06) !important; }
.creation-home .creation-chat-controls > .voice-recording-inline {
width: min(236px, calc(100vw - 120px));
max-width: 236px;
flex: 0 1 236px;
min-height: 30px;
height: 30px;
overflow: hidden;
border: 0;
border-radius: 7px;
background: transparent !important;
padding: 0 2px;
}
.creation-home .creation-chat-controls > .voice-recording-inline > svg {
flex: 0 1 160px;
max-width: 160px;
}
.creation-chat-intro h1 > .creation-intro-emphasis {
display: inline-block;
margin: 0 4px;