fix(canvas): 创作输入栏 - 分离模型价格展示并收敛录音与附件入口
This commit is contained in:
1 parent
0330e64857
commit
41d99374ca
5 files changed
+29
-44
No files matched your search
@@ -108,7 +108,7 @@ export function VoiceRecordingInline({ onTranscribed, onCancel }: VoiceRecording
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<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 }}
|
style={{ background: theme.toolbar.panel, borderColor: theme.toolbar.border }}
|
||||||
>
|
>
|
||||||
{displayError ? (
|
{displayError ? (
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type ModelPickerProps = {
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
onMissingConfig?: () => void;
|
onMissingConfig?: () => void;
|
||||||
showSelectedPrice?: boolean;
|
showSelectedPrice?: boolean;
|
||||||
|
showOptionPrices?: boolean;
|
||||||
variant?: "default" | "creation";
|
variant?: "default" | "creation";
|
||||||
requirements?: ModelRequirements;
|
requirements?: ModelRequirements;
|
||||||
showConfiguredModelName?: boolean;
|
showConfiguredModelName?: boolean;
|
||||||
@@ -40,6 +41,7 @@ export function ModelPicker({
|
|||||||
placeholder = "选择模型",
|
placeholder = "选择模型",
|
||||||
onMissingConfig,
|
onMissingConfig,
|
||||||
showSelectedPrice = true,
|
showSelectedPrice = true,
|
||||||
|
showOptionPrices = showSelectedPrice,
|
||||||
variant = "default",
|
variant = "default",
|
||||||
requirements,
|
requirements,
|
||||||
showConfiguredModelName = false,
|
showConfiguredModelName = false,
|
||||||
@@ -226,7 +228,7 @@ export function ModelPicker({
|
|||||||
theme={theme}
|
theme={theme}
|
||||||
creationVariant={creationVariant}
|
creationVariant={creationVariant}
|
||||||
showConfiguredModelName={showConfiguredModelName}
|
showConfiguredModelName={showConfiguredModelName}
|
||||||
showPrice={showSelectedPrice && creditsEnabled}
|
showPrice={showOptionPrices && creditsEnabled}
|
||||||
disabledReason={disabledReason}
|
disabledReason={disabledReason}
|
||||||
/>
|
/>
|
||||||
{selected ? <Check className="canvas-model-picker-option-check" style={{ color: theme.node.activeStroke }} /> : null}
|
{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 { App, Button, Drawer, Modal, Popover, Spin, Tooltip } from "antd";
|
||||||
import { Reorder } from "motion/react";
|
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 { Link } from "react-router";
|
||||||
|
|
||||||
import { AIMessageMarkdown } from "@/components/ai/ai-message-markdown";
|
import { AIMessageMarkdown } from "@/components/ai/ai-message-markdown";
|
||||||
@@ -171,7 +171,6 @@ export default function CreatePage() {
|
|||||||
const [libraryOpen, setLibraryOpen] = useState(false);
|
const [libraryOpen, setLibraryOpen] = useState(false);
|
||||||
const externalAssetSources = useExternalAssetSources(libraryOpen);
|
const externalAssetSources = useExternalAssetSources(libraryOpen);
|
||||||
const abortRef = useRef<AbortController | null>(null);
|
const abortRef = useRef<AbortController | null>(null);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
const composerFocusRef = useRef<HTMLTextAreaElement>(null);
|
const composerFocusRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const threadScrollRef = useRef<HTMLElement>(null);
|
const threadScrollRef = useRef<HTMLElement>(null);
|
||||||
const followLatestMessageRef = useRef(true);
|
const followLatestMessageRef = useRef(true);
|
||||||
@@ -418,27 +417,6 @@ export default function CreatePage() {
|
|||||||
attachment: creationAttachmentFromImage(file, uploaded),
|
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 uploadLibraryAssets = async (files: FileList | File[]) => {
|
||||||
const next = Array.from(files).filter((file) => creationFileAccepted(mode, file));
|
const next = Array.from(files).filter((file) => creationFileAccepted(mode, file));
|
||||||
if (!next.length) return [];
|
if (!next.length) return [];
|
||||||
@@ -453,11 +431,6 @@ export default function CreatePage() {
|
|||||||
return assetIds;
|
return assetIds;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
if (event.target.files) addAttachments(event.target.files);
|
|
||||||
event.target.value = "";
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLibrarySelect = (selectedIds: string[]) => {
|
const handleLibrarySelect = (selectedIds: string[]) => {
|
||||||
const next = selectedIds.flatMap((id): CreationAttachment[] => {
|
const next = selectedIds.flatMap((id): CreationAttachment[] => {
|
||||||
const asset = assets.find((item) => item.id === id);
|
const asset = assets.find((item) => item.id === id);
|
||||||
@@ -893,8 +866,6 @@ export default function CreatePage() {
|
|||||||
onReplaceAttachment: replaceReferenceFromTrack,
|
onReplaceAttachment: replaceReferenceFromTrack,
|
||||||
onReplaceReferenceFiles: replaceReferenceFromFiles,
|
onReplaceReferenceFiles: replaceReferenceFromFiles,
|
||||||
onOpenLibrary: () => setLibraryOpen(true),
|
onOpenLibrary: () => setLibraryOpen(true),
|
||||||
fileInputRef,
|
|
||||||
onFileChange: handleFileChange,
|
|
||||||
onModeChange: selectMode,
|
onModeChange: selectMode,
|
||||||
model: selectedModel,
|
model: selectedModel,
|
||||||
modelRequirements,
|
modelRequirements,
|
||||||
@@ -1164,8 +1135,6 @@ type ComposerProps = {
|
|||||||
onReplaceAttachment: (targetAttachmentId: string, replacement: CreationAttachment) => void;
|
onReplaceAttachment: (targetAttachmentId: string, replacement: CreationAttachment) => void;
|
||||||
onReplaceReferenceFiles: (targetAttachmentId: string, files: File[]) => void;
|
onReplaceReferenceFiles: (targetAttachmentId: string, files: File[]) => void;
|
||||||
onOpenLibrary: () => void;
|
onOpenLibrary: () => void;
|
||||||
fileInputRef: RefObject<HTMLInputElement | null>;
|
|
||||||
onFileChange: (event: ChangeEvent<HTMLInputElement>) => void;
|
|
||||||
onModeChange: (mode: CreationMode) => void;
|
onModeChange: (mode: CreationMode) => void;
|
||||||
model: string;
|
model: string;
|
||||||
modelRequirements: ModelRequirements;
|
modelRequirements: ModelRequirements;
|
||||||
@@ -1327,7 +1296,6 @@ function CreationComposer(props: ComposerProps) {
|
|||||||
};
|
};
|
||||||
const composer = <section className={`creation-chat-composer is-${props.variant}`}>
|
const composer = <section className={`creation-chat-composer is-${props.variant}`}>
|
||||||
<div className="creation-chat-writing-surface">
|
<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">
|
<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); }} />
|
<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}>
|
{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>
|
<span>优化</span>
|
||||||
</button>
|
</button>
|
||||||
</Tooltip> : null}
|
</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} showOptionPrices 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} variant="creation" />
|
|
||||||
{props.mode === "video" || (props.mode === "image" && imageSettingsSupported) ? <GenerationSettingsMenu {...props} /> : null}
|
{props.mode === "video" || (props.mode === "image" && imageSettingsSupported) ? <GenerationSettingsMenu {...props} /> : null}
|
||||||
{props.mode === "video" ? <DurationMenu profile={props.videoProfile} seconds={props.seconds} onChange={props.setSeconds} /> : null}
|
{props.mode === "video" ? <DurationMenu profile={props.videoProfile} seconds={props.seconds} onChange={props.setSeconds} /> : null}
|
||||||
</div>
|
</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-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; }
|
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 {
|
.creation-chat-intro h1 > .creation-intro-emphasis {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0 4px;
|
margin: 0 4px;
|
||||||
|
|||||||
@@ -18,14 +18,13 @@ describe("creation library button", () => {
|
|||||||
expect(dockEnd).toBeGreaterThan(dockStart);
|
expect(dockEnd).toBeGreaterThan(dockStart);
|
||||||
const dockSource = compactSource(source.slice(dockStart, dockEnd));
|
const dockSource = compactSource(source.slice(dockStart, dockEnd));
|
||||||
const modePickerIndex = dockSource.indexOf("<ModePicker mode={props.mode}");
|
const modePickerIndex = dockSource.indexOf("<ModePicker mode={props.mode}");
|
||||||
const attachmentIndex = dockSource.indexOf('aria-label="从本机上传附件"');
|
|
||||||
|
|
||||||
expect(modePickerIndex).toBeGreaterThanOrEqual(0);
|
expect(modePickerIndex).toBeGreaterThanOrEqual(0);
|
||||||
expect(attachmentIndex).toBeGreaterThan(modePickerIndex);
|
|
||||||
expect(dockSource).not.toContain('aria-label="打开素材库选择参考内容"');
|
expect(dockSource).not.toContain('aria-label="打开素材库选择参考内容"');
|
||||||
|
expect(dockSource).not.toContain('aria-label="从本机上传附件"');
|
||||||
expect(source).toContain("onClick={props.onOpenLibrary}");
|
expect(source).toContain("onClick={props.onOpenLibrary}");
|
||||||
expect(source).toContain("creation-reference-add-button");
|
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");
|
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 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 pickerSource = readFileSync(resolve(import.meta.dir, "../src/components/assets/asset-library-picker-modal.tsx"), "utf8");
|
||||||
const uploadStart = source.indexOf("const uploadLibraryAssets = async");
|
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(uploadStart).toBeGreaterThanOrEqual(0);
|
||||||
expect(uploadEnd).toBeGreaterThan(uploadStart);
|
expect(uploadEnd).toBeGreaterThan(uploadStart);
|
||||||
@@ -128,12 +127,12 @@ describe("creation library button", () => {
|
|||||||
expect(removeCreationAttachment(attachments, "last").map((item) => item.id)).toEqual(["first", "middle"]);
|
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"));
|
const source = compactSource(readFileSync(resolve(import.meta.dir, "../src/pages/create/index.tsx"), "utf8"));
|
||||||
|
|
||||||
expect(source).toContain("onPointerDownCapture={(event) => event.stopPropagation()}");
|
expect(source).toContain("onPointerDownCapture={(event) => event.stopPropagation()}");
|
||||||
expect(source).toContain("onRemove(item.id)");
|
expect(source).toContain("onRemove(item.id)");
|
||||||
expect(source).toContain("onClick={() => props.fileInputRef.current?.click()}");
|
|
||||||
expect(source).toContain("onClick={props.onOpenLibrary}");
|
expect(source).toContain("onClick={props.onOpenLibrary}");
|
||||||
|
expect(source).not.toContain("onClick={() => props.fileInputRef.current?.click()}");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user