修改全能创作发现问题

This commit is contained in:
Azmat@qq.com
2026-09-03 19:00:59 +08:00
parent e7122b78fb
commit 3e904479d9
23 changed files with 1325 additions and 433 deletions
+62 -149
View File
@@ -2,14 +2,12 @@ import { useEffect, useMemo, useRef, useState } from "react";
import {
ArrowLeft,
Box,
ChevronDown,
ChevronRight,
FolderOpen,
History,
Image as ImageIcon,
Play,
Plus,
SlidersHorizontal,
Sparkles,
Trash2,
Upload,
@@ -20,7 +18,7 @@ import {
X,
} from "lucide-react";
import { api } from "../api";
import { CustomSelect } from "../components/custom-select";
import { OmniParamBar } from "../components/omni-param-bar";
import { ConfirmModal } from "../components/overlays";
import type { CreationConversation, CreationRef } from "../types";
import type { NavigateFn } from "./route-config";
@@ -37,8 +35,6 @@ type PresetItem = {
cover: string;
};
type Attachment = { name: string; type: string; url?: string; source: "local" | "library" };
const VIDEO_PRESETS: PresetItem[] = [
{ name: "剧情反转带货", category: "story", mode: "video", title: "剧情反转带货", desc: "用人物冲突与意外反转建立记忆点,商品承担剧情中的关键作用。", starter: "创作一条有前后反转的剧情带货视频,让商品自然成为解决问题的关键。", cover: "/assets/prototype/photo-1529139574466-a303027c1d8b.jpg" },
{ name: "商品拟人广告", category: "commerce", mode: "video", title: "商品拟人广告", desc: "让商品成为故事主角,通过个性、动作和情绪完成趣味表达。", starter: "把商品塑造成有性格的拟人角色,创作一条轻快、有趣的短广告。", cover: "/assets/prototype/photo-1608248543803-ba4f8c70ae0b.jpg" },
@@ -75,12 +71,6 @@ const IMAGE_FILTERS = [
{ key: "style", label: "风格" },
];
const VIDEO_MODELS = ["Seedance 2.5", "Seedance 2.0", "Seedance 2.0 Fast", "Seedance 2.0 Mini"];
const IMAGE_MODELS = ["Seedream5.0", "YQ image2"];
const VIDEO_DURATIONS = ["4 秒", "5 秒", "6 秒", "7 秒", "8 秒", "9 秒", "10 秒", "11 秒", "12 秒", "13 秒", "14 秒", "15 秒", "30 秒"];
const IMAGE_COUNTS = ["1 张", "2 张", "4 张", "8 张"];
const RATIOS = ["9:16", "16:9", "1:1", "4:3", "3:4"];
const MENTION_REF_LIMIT = 5;
const MENTION_TABS: Array<{ type: CreationRef["type"]; label: string; Icon: typeof ImageIcon }> = [
@@ -91,6 +81,14 @@ const MENTION_TABS: Array<{ type: CreationRef["type"]; label: string; Icon: type
{ type: "scene", label: "场景", Icon: FolderOpen },
];
function mergeSessionUploads(results: CreationRef[], uploads: CreationRef[], query: string, type: CreationRef["type"]) {
if (type !== "asset") return results;
const q = query.trim().toLowerCase();
const extras = uploads.filter((item) => !q || item.name.toLowerCase().includes(q));
const seen = new Set(extras.map((item) => item.id));
return [...extras, ...results.filter((item) => !seen.has(item.id))];
}
function formatRelativeTime(iso: string) {
const then = new Date(iso).getTime();
if (Number.isNaN(then)) return "";
@@ -103,10 +101,6 @@ function formatRelativeTime(iso: string) {
return days < 30 ? `${days} 天前` : new Date(then).toLocaleDateString("zh-CN");
}
function toOptions(values: string[]) {
return values.map((value) => ({ value, label: value }));
}
export function OmniCreatePage({
navigate,
onNotify,
@@ -117,13 +111,12 @@ export function OmniCreatePage({
const [outputMode, setOutputMode] = useState<OutputMode>("video");
const [selectedCase, setSelectedCase] = useState<PresetItem | null>(null);
const [prompt, setPrompt] = useState("");
const [attachments, setAttachments] = useState<Attachment[]>([]);
const [sessionUploads, setSessionUploads] = useState<CreationRef[]>([]);
const [uploading, setUploading] = useState(false);
const [model, setModel] = useState("Seedance 2.5");
const [resolution, setResolution] = useState("1080p");
const [ratio, setRatio] = useState("9:16");
const [duration, setDuration] = useState("智能时长");
const [customDurationOn, setCustomDurationOn] = useState(false);
const [durationMenuOpen, setDurationMenuOpen] = useState(false);
const [uploadMenuOpen, setUploadMenuOpen] = useState(false);
const [mentionMenuOpen, setMentionMenuOpen] = useState(false);
const [mentionTab, setMentionTab] = useState<CreationRef["type"]>("asset");
@@ -143,16 +136,13 @@ export function OmniCreatePage({
setResolution("模型默认");
setRatio("1:1");
setDuration("1 张");
setCustomDurationOn(true);
} else {
setModel("Seedance 2.5");
setResolution("1080p");
setRatio("9:16");
setDuration("智能时长");
setCustomDurationOn(false);
}
setActiveCategory("all");
setDurationMenuOpen(false);
setSelectedCase((prev) => (prev && prev.mode !== outputMode ? null : prev));
}, [outputMode]);
@@ -162,8 +152,7 @@ export function OmniCreatePage({
if (toolsRef.current?.contains(target)) return;
setUploadMenuOpen(false);
setMentionMenuOpen(false);
setDurationMenuOpen(false);
};
};
document.addEventListener("mousedown", onDown);
return () => document.removeEventListener("mousedown", onDown);
}, []);
@@ -186,12 +175,11 @@ export function OmniCreatePage({
setMentionTab(type);
setMentionMenuOpen(true);
setUploadMenuOpen(false);
setDurationMenuOpen(false);
setMentionLoading(true);
setMentionResults([]);
try {
const res = await api.searchMentions({ q: query, types: [type], limit: 12 });
setMentionResults(res.results);
setMentionResults(mergeSessionUploads(res.results, sessionUploads, query, type));
setTypeLabels(res.type_labels);
} catch (error) {
onNotify?.("error", (error as Error).message);
@@ -215,20 +203,39 @@ export function OmniCreatePage({
setMentionMenuOpen(false);
};
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(event.target.files || []);
if (!files.length) return;
setAttachments((prev) => [
...prev,
...files.map((file) => ({
name: file.name,
type: file.type,
url: URL.createObjectURL(file),
source: "local" as const,
})),
]);
setUploadMenuOpen(false);
event.target.value = "";
setUploadMenuOpen(false);
if (!files.length) return;
const images = files.filter((file) => file.type.startsWith("image/"));
if (images.length !== files.length) {
onNotify?.("info", "全能创作仅支持上传图片");
}
if (!images.length) return;
setUploading(true);
try {
for (const file of images) {
const form = new FormData();
form.append("file", file);
const data = await api.uploadFreeVideoRef(form);
const ref: CreationRef = {
type: "asset",
id: data.asset_id,
name: data.name || file.name,
cover: data.thumb_url || data.url,
};
setSessionUploads((prev) => (prev.some((item) => item.id === ref.id) ? prev : [...prev, ref]));
setPendingRefs((prev) => {
if (prev.some((item) => item.id === ref.id) || prev.length >= MENTION_REF_LIMIT) return prev;
return [...prev, ref];
});
}
} catch (error) {
onNotify?.("error", (error as Error).message);
} finally {
setUploading(false);
}
};
return (
@@ -273,24 +280,11 @@ export function OmniCreatePage({
<X />
</button>
</span>
))}{attachments.map((file, index) => (
<span className="omni-attachment-chip" key={`${file.name}-${index}`}>
{file.type.startsWith("video") ? <Video /> : <ImageIcon />}
<span>{file.name}</span>
<button
type="button"
className="omni-attachment-remove"
aria-label={`删除 ${file.name}`}
onClick={() => setAttachments((prev) => prev.filter((_, i) => i !== index))}
>
<X />
</button>
</span>
))}</div>
<textarea
id="omniStartPrompt"
rows={3}
placeholder="描述你想制作的内容,@ 可引用商品、模特、场景或已有素材……"
placeholder="描述你想制作的内容,@ 可引用商品、模特、场景、素材或刚上传的图片……"
value={prompt}
onChange={(event) => {
const value = event.target.value;
@@ -309,7 +303,6 @@ export function OmniCreatePage({
onClick={() => {
setUploadMenuOpen((open) => !open);
setMentionMenuOpen(false);
setDurationMenuOpen(false);
}}
>
<Plus />
@@ -328,14 +321,14 @@ export function OmniCreatePage({
</button>
<button type="button" onClick={() => fileInputRef.current?.click()}>
<Upload />
<span>本地上传<small>添加电脑中的图片或视频</small></span>
<span>本地上传<small>添加电脑中的图片</small></span>
</button>
</div>
</div>
<input
ref={fileInputRef}
type="file"
accept="image/*,video/*"
accept="image/*"
multiple
hidden
onChange={handleFileChange}
@@ -349,7 +342,6 @@ export function OmniCreatePage({
if (mentionMenuOpen) setMentionMenuOpen(false);
else void openMentions("", mentionTab);
setUploadMenuOpen(false);
setDurationMenuOpen(false);
}}
>
@
@@ -393,100 +385,25 @@ export function OmniCreatePage({
</div>
</div>
<label className="omni-parameter omni-parameter-model">
<CustomSelect
fill
size="sm"
aria-label={outputMode === "video" ? "视频模型" : "图片模型"}
value={model}
onChange={setModel}
options={toOptions(outputMode === "video" ? VIDEO_MODELS : IMAGE_MODELS)}
/>
</label>
<label className={`omni-parameter${outputMode === "image" ? " is-hidden" : ""}`}>
<CustomSelect
fill
size="sm"
aria-label="分辨率"
value={resolution}
onChange={setResolution}
options={toOptions(["1080p", "720p", "480p"])}
/>
</label>
<label className="omni-parameter">
<CustomSelect
fill
size="sm"
aria-label="画面比例"
value={ratio}
onChange={setRatio}
options={toOptions(RATIOS)}
/>
</label>
<div className={`omni-duration-control${outputMode === "image" ? " is-image-count" : ""}`}>
<button
type="button"
className="omni-duration-trigger"
aria-expanded={durationMenuOpen}
onClick={() => {
setDurationMenuOpen((open) => !open);
setUploadMenuOpen(false);
setMentionMenuOpen(false);
}}
>
<span>{duration}</span>
<ChevronDown />
</button>
<div className="omni-duration-menu" hidden={!durationMenuOpen}>
<span className="omni-duration-title">{outputMode === "image" ? "生成张数" : "时长"}</span>
<div className="omni-duration-modes">
<button
type="button"
className={!customDurationOn ? "active" : ""}
onClick={() => {
setCustomDurationOn(false);
setDuration("智能时长");
setDurationMenuOpen(false);
}}
>
<Sparkles />
<span>智能时长</span>
</button>
<button
type="button"
className={customDurationOn ? "active" : ""}
onClick={() => setCustomDurationOn(true)}
>
<SlidersHorizontal />
<span>自定义时长</span>
</button>
</div>
<div className="omni-duration-values" hidden={outputMode === "video" && !customDurationOn}>
{(outputMode === "image" ? IMAGE_COUNTS : VIDEO_DURATIONS).map((value) => (
<button
type="button"
key={value}
className={duration === value ? "active" : ""}
onClick={() => {
setDuration(value);
setCustomDurationOn(true);
setDurationMenuOpen(false);
}}
>
{value}
</button>
))}
</div>
</div>
</div>
<OmniParamBar
isVideo={outputMode === "video"}
model={model}
resolution={resolution}
ratio={ratio}
duration={duration}
onModel={setModel}
onResolution={setResolution}
onRatio={setRatio}
onDuration={setDuration}
/>
</div>
<button
type="button"
className="omni-start-generate"
disabled={starting}
disabled={starting || uploading}
onClick={() => {
const text = prompt.trim();
if (!text && !selectedCase && attachments.length === 0) {
if (!text && !selectedCase && pendingRefs.length === 0) {
onNotify?.("info", "请先输入你想制作的内容,或选择一个创作预设");
return;
}
@@ -508,7 +425,7 @@ export function OmniCreatePage({
})
.then((conversation) => {
// 首条消息交给对话页发,避免这里再复制一份 SSE 消费逻辑
navigate("omniSession", { conversationId: conversation.id, firstMessage: text, firstRefs: pendingRefs });
navigate("omniSession", { conversationId: conversation.id, firstMessage: text, firstRefs: pendingRefs, firstUploads: sessionUploads });
})
.catch((error) => {
onNotify?.("error", (error as Error).message);
@@ -516,7 +433,7 @@ export function OmniCreatePage({
});
}}
>
{starting ? "正在创建…" : "开始创作"}
{uploading ? "图片上传中…" : starting ? "正在创建…" : "开始创作"}
</button>
</div>
</section>
@@ -678,10 +595,6 @@ export function OmniHistoryPage({
</button>
))}
</div>
<button type="button" className="omni-history-new" onClick={() => navigate("omniCreate")}>
<Plus />
新建会话
</button>
</div>
</header>
<div className="omni-history-list">