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

This commit is contained in:
ddCat
2026-09-01 20:01:52 +08:00
parent 2832fee9c1
commit 2141367076
5 changed files with 29 additions and 44 deletions

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>