fix(canvas): 创作输入栏 - 分离模型价格展示并收敛录音与附件入口
This commit is contained in:
@@ -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 ? (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -18,14 +18,13 @@ describe("creation library button", () => {
|
||||
expect(dockEnd).toBeGreaterThan(dockStart);
|
||||
const dockSource = compactSource(source.slice(dockStart, dockEnd));
|
||||
const modePickerIndex = dockSource.indexOf("<ModePicker mode={props.mode}");
|
||||
const attachmentIndex = dockSource.indexOf('aria-label="从本机上传附件"');
|
||||
|
||||
expect(modePickerIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(attachmentIndex).toBeGreaterThan(modePickerIndex);
|
||||
expect(dockSource).not.toContain('aria-label="打开素材库选择参考内容"');
|
||||
expect(dockSource).not.toContain('aria-label="从本机上传附件"');
|
||||
expect(source).toContain("onClick={props.onOpenLibrary}");
|
||||
expect(source).toContain("creation-reference-add-button");
|
||||
expect(source).toContain('showSelectedPrice={false} variant="creation"');
|
||||
expect(source).toContain('showSelectedPrice={false} showOptionPrices variant="creation"');
|
||||
expect(source).toContain("canvas-node-composer-submit-cost");
|
||||
});
|
||||
|
||||
@@ -33,7 +32,7 @@ describe("creation library button", () => {
|
||||
const source = readFileSync(resolve(import.meta.dir, "../src/pages/create/index.tsx"), "utf8");
|
||||
const pickerSource = readFileSync(resolve(import.meta.dir, "../src/components/assets/asset-library-picker-modal.tsx"), "utf8");
|
||||
const uploadStart = source.indexOf("const uploadLibraryAssets = async");
|
||||
const uploadEnd = source.indexOf("const handleFileChange", uploadStart);
|
||||
const uploadEnd = source.indexOf("const handleLibrarySelect", uploadStart);
|
||||
|
||||
expect(uploadStart).toBeGreaterThanOrEqual(0);
|
||||
expect(uploadEnd).toBeGreaterThan(uploadStart);
|
||||
@@ -128,12 +127,12 @@ describe("creation library button", () => {
|
||||
expect(removeCreationAttachment(attachments, "last").map((item) => item.id)).toEqual(["first", "middle"]);
|
||||
});
|
||||
|
||||
test("删除按钮在指针按下阶段隔离拖拽,附件与素材库入口职责独立", () => {
|
||||
test("删除按钮在指针按下阶段隔离拖拽,素材库入口职责独立", () => {
|
||||
const source = compactSource(readFileSync(resolve(import.meta.dir, "../src/pages/create/index.tsx"), "utf8"));
|
||||
|
||||
expect(source).toContain("onPointerDownCapture={(event) => event.stopPropagation()}");
|
||||
expect(source).toContain("onRemove(item.id)");
|
||||
expect(source).toContain("onClick={() => props.fileInputRef.current?.click()}");
|
||||
expect(source).toContain("onClick={props.onOpenLibrary}");
|
||||
expect(source).not.toContain("onClick={() => props.fileInputRef.current?.click()}");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user