Restore LibTV module action workflows
This commit is contained in:
parent
5ba8900438
commit
3278776e7f
@ -190,6 +190,19 @@ type ComposerSettings = {
|
||||
|
||||
type AddModuleKind = "text" | "image" | "video" | "video-clip" | "audio" | "script";
|
||||
type CanvasCreateKind = AddModuleKind;
|
||||
type TargetModuleActionLabel =
|
||||
| "自己编写内容"
|
||||
| "文生视频"
|
||||
| "图片反推提示词"
|
||||
| "文字生音乐"
|
||||
| "图生图"
|
||||
| "图片高清"
|
||||
| "音频生视频"
|
||||
| "首尾帧生成视频"
|
||||
| "首帧生成视频"
|
||||
| "脚本生成分镜脚本"
|
||||
| "视频参考生成分镜脚本"
|
||||
| "角色生成分镜脚本";
|
||||
|
||||
type LiblibVideoCanvasReplicaProps = {
|
||||
canvasId: string;
|
||||
@ -394,6 +407,31 @@ const CREDIT_PACKS = [
|
||||
const STORY_SCRIPT_PROMPT = "根据我上传的剧本生成一个完整的故事脚本";
|
||||
const AUDIO_TO_VIDEO_PROMPT =
|
||||
"根据上传的音频生成对应场景画面,镜头语言、节奏、音乐匹配情绪变化,电影级质感。";
|
||||
const TEXT_TO_VIDEO_PROMPT =
|
||||
"爆款情绪短片\n傍晚的城市天台,一个年轻人迎着晚风张开双手,镜头从远景缓慢推进到中景,头发被风吹起,夕阳逆光,光晕明显,空气中有漂浮微尘,电影级色彩分级,浅景深,情绪张力强,慢动作";
|
||||
const TEXT_TO_VIDEO_GENERATE_PROMPT = "根据文字描述生成视频。";
|
||||
const TEXT_TO_MUSIC_PROMPT =
|
||||
"生成一首现代品牌电子音乐,干净有力的低频贝斯,清晰电子鼓点,整体风格高级、未来感强。";
|
||||
const IMAGE_TO_PROMPT_PROMPT = "根据图片生成结构化中文提示词,包括主体描述、环境、光影、镜头语言、风格关键词。";
|
||||
const HD_IMAGE_SOURCE_URL =
|
||||
"https://liblibai-online.liblib.cloud/web/ea95cb51f3d94addbb92dc003cd33b49/5d40e509-6182-4d5d-b202-8e130b44a66c.png";
|
||||
const VIDEO_FIRST_FRAME_SOURCE_URL =
|
||||
"https://liblibai-online.liblib.cloud/web/28aa80df5944499487381983a8f9bbdd/604bb8d4-e8d1-439d-80b4-9867a579d0e7.png";
|
||||
const VIDEO_FIRST_FRAME_PROMPT = "以当前图为首帧生成视频。";
|
||||
const VIDEO_FIRST_LAST_FIRST_URL =
|
||||
"https://liblibai-online.liblib.cloud/web/ea95cb51f3d94addbb92dc003cd33b49/3dbb2a8e-47a7-4480-9de0-293adea8fbb4.png";
|
||||
const VIDEO_FIRST_LAST_LAST_URL =
|
||||
"https://liblibai-online.liblib.cloud/web/ea95cb51f3d94addbb92dc003cd33b49/cea9eb03-e93f-45ab-b1c3-42d749d95321.png";
|
||||
const VIDEO_FIRST_LAST_PROMPT =
|
||||
"手从白帆布包中取出青柠气泡水罐的特写,过渡到短发女生在户外草地闭眼仰头喝同款气泡水的中景,日系清新治愈风格,明亮自然光,柔和暖调,胶片质感,动作流畅自然";
|
||||
const SCRIPT_VIDEO_REFERENCE_URL =
|
||||
"https://liblibai-online.liblib.cloud/web/ea95cb51f3d94addbb92dc003cd33b49/7a21a9b9-503b-4168-b9c2-be0a3770f215.mp4";
|
||||
const SCRIPT_CHARACTER_ONE_URL =
|
||||
"https://liblibai-online.liblib.cloud/web/ea95cb51f3d94addbb92dc003cd33b49/b6989b27-e725-4958-a062-e8dfb3312db3.png";
|
||||
const SCRIPT_CHARACTER_TWO_URL =
|
||||
"https://liblibai-online.liblib.cloud/web/ea95cb51f3d94addbb92dc003cd33b49/fe840bb1-649e-4664-a23d-30f5ecf8e36c.png";
|
||||
const SCRIPT_CHARACTER_PROMPT =
|
||||
"根据角色参考图生成分镜脚本,围绕旧城区、机械修复、暴雨停电和被重新点亮的街道展开,突出人物关系、镜头画面和声音设计。";
|
||||
const SLASH_IMAGE_ITEMS = [
|
||||
"剧情推演四宫格",
|
||||
"25宫格连贯分镜",
|
||||
@ -828,6 +866,89 @@ function createUploadedMediaNode(file: File, mediaUrl: string, x: number, y: num
|
||||
};
|
||||
}
|
||||
|
||||
function createMediaResourceNode({
|
||||
action,
|
||||
height,
|
||||
mediaType,
|
||||
mediaUrl,
|
||||
title,
|
||||
width,
|
||||
x,
|
||||
y,
|
||||
prompt,
|
||||
}: {
|
||||
action?: string;
|
||||
height?: number;
|
||||
mediaType: NonNullable<CloneNode["inputType"]>;
|
||||
mediaUrl: string;
|
||||
prompt?: string;
|
||||
title: string;
|
||||
width?: number;
|
||||
x: number;
|
||||
y: number;
|
||||
}): CloneNode {
|
||||
const label = mediaType === "video" ? "视频" : mediaType === "audio" ? "音频" : mediaType === "text" ? "文本" : "图片";
|
||||
return {
|
||||
id: makeId(mediaType),
|
||||
kind: "media",
|
||||
title,
|
||||
subtitle: `${label}资源`,
|
||||
inputType: mediaType,
|
||||
x,
|
||||
y,
|
||||
width: width ?? (mediaType === "audio" ? 430 : mediaType === "video" ? 560 : 420),
|
||||
height: height ?? (mediaType === "audio" ? 170 : mediaType === "video" ? 315 : 280),
|
||||
accent: mediaType === "audio" ? "#9ee6bb" : mediaType === "video" ? "#8b96ff" : "#8790ff",
|
||||
draftValues: {
|
||||
action: action ?? `${mediaType.toUpperCase()}_RESOURCE`,
|
||||
alt: title,
|
||||
generated: false,
|
||||
mediaType,
|
||||
mediaUrl,
|
||||
nodeType: mediaType,
|
||||
params: prompt ? { prompt } : undefined,
|
||||
prompt: prompt || "",
|
||||
url: [mediaUrl],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createTextResourceNode({
|
||||
content,
|
||||
height = 430,
|
||||
title,
|
||||
width = 560,
|
||||
x,
|
||||
y,
|
||||
}: {
|
||||
content: string;
|
||||
height?: number;
|
||||
title: string;
|
||||
width?: number;
|
||||
x: number;
|
||||
y: number;
|
||||
}): CloneNode {
|
||||
return {
|
||||
id: makeId("text"),
|
||||
kind: "script",
|
||||
title,
|
||||
subtitle: "文本资源",
|
||||
inputType: "text",
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
accent: "#f0d06b",
|
||||
draftValues: {
|
||||
action: "TEXT_RESOURCE",
|
||||
content: [content],
|
||||
nodeType: "text",
|
||||
scriptRole: "resource",
|
||||
viewMode: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getDraftString(node: CloneNode | null | undefined, key: string) {
|
||||
const value = node?.draftValues?.[key];
|
||||
return typeof value === "string" ? value : "";
|
||||
@ -848,6 +969,9 @@ function getPresetMode(node: CloneNode | null | undefined): ActiveStarterScene {
|
||||
|
||||
function getTargetModuleKind(node: CloneNode): AddModuleKind | null {
|
||||
if (node.kind === "media" || getPresetMode(node)) return null;
|
||||
if (getDraftString(node, "action") === "TEXT_RESOURCE" || getDraftString(node, "scriptRole") === "resource") {
|
||||
return null;
|
||||
}
|
||||
const nodeType = getDraftString(node, "nodeType");
|
||||
if (
|
||||
nodeType === "text" ||
|
||||
@ -1347,6 +1471,275 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
[addNodes, connectionMenu, nodes]
|
||||
);
|
||||
|
||||
const runTargetModuleAction = useCallback(
|
||||
(nodeId: string, actionLabel: TargetModuleActionLabel) => {
|
||||
const anchor = nodes.find((node) => node.id === nodeId);
|
||||
if (!anchor) return;
|
||||
|
||||
const leftX = Math.round(anchor.x - 560 - 120);
|
||||
const rightX = Math.round(anchor.x + anchor.width + 120);
|
||||
const centerY = (height: number) => Math.round(anchor.y + anchor.height / 2 - height / 2);
|
||||
const nextNodes: CloneNode[] = [];
|
||||
const nextEdges: CloneEdge[] = [];
|
||||
let selectedId = anchor.id;
|
||||
let anchorUpdates: Partial<CloneNode> = {};
|
||||
let draftUpdates: Record<string, unknown> = {};
|
||||
|
||||
const connectToAnchor = (node: CloneNode) => {
|
||||
nextNodes.push(node);
|
||||
nextEdges.push({ id: makeId("edge"), source: node.id, target: anchor.id });
|
||||
selectedId = node.id;
|
||||
};
|
||||
|
||||
const connectFromAnchor = (node: CloneNode) => {
|
||||
nextNodes.push(node);
|
||||
nextEdges.push({ id: makeId("edge"), source: anchor.id, target: node.id });
|
||||
selectedId = node.id;
|
||||
};
|
||||
|
||||
const makeGenerated = (kind: Exclude<AddModuleKind, "audio"> | "audio", title: string, x: number, updates?: Record<string, unknown>) => {
|
||||
const created = createTargetModuleNode(kind, x, 0, nodes.length + nextNodes.length + 1);
|
||||
return {
|
||||
...created,
|
||||
title,
|
||||
y: centerY(created.height),
|
||||
draftValues: {
|
||||
...(created.draftValues || {}),
|
||||
...(updates || {}),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
switch (actionLabel) {
|
||||
case "自己编写内容": {
|
||||
draftUpdates = { action: "TEXT_RESOURCE", content: [], manualEditing: true };
|
||||
toast.success("已切换为文本编写节点");
|
||||
break;
|
||||
}
|
||||
case "文生视频": {
|
||||
draftUpdates = { action: "TEXT_GENERATE", content: [TEXT_TO_VIDEO_PROMPT] };
|
||||
connectFromAnchor(makeGenerated("video", "视频", rightX, { params: { prompt: TEXT_TO_VIDEO_GENERATE_PROMPT } }));
|
||||
break;
|
||||
}
|
||||
case "图片反推提示词": {
|
||||
const image = createMediaResourceNode({
|
||||
mediaType: "image",
|
||||
mediaUrl: FIRST_FRAME_IMAGE_URL,
|
||||
title: "图片",
|
||||
width: 420,
|
||||
height: 280,
|
||||
x: leftX,
|
||||
y: centerY(280),
|
||||
});
|
||||
draftUpdates = {
|
||||
action: "TEXT_GENERATE",
|
||||
content: [],
|
||||
params: { ...(anchor.draftValues?.params as Record<string, unknown> | undefined), prompt: IMAGE_TO_PROMPT_PROMPT },
|
||||
};
|
||||
connectToAnchor(image);
|
||||
break;
|
||||
}
|
||||
case "文字生音乐": {
|
||||
draftUpdates = { action: "TEXT_GENERATE", content: [TEXT_TO_MUSIC_PROMPT] };
|
||||
connectFromAnchor(makeGenerated("audio", "音频", rightX, { params: { scene: "Music" } }));
|
||||
break;
|
||||
}
|
||||
case "图生图": {
|
||||
anchorUpdates = {
|
||||
kind: "media",
|
||||
title: "图片",
|
||||
subtitle: "图片资源",
|
||||
width: 420,
|
||||
height: 280,
|
||||
inputType: "image",
|
||||
};
|
||||
draftUpdates = {
|
||||
action: "IMAGE_RESOURCE",
|
||||
generated: false,
|
||||
mediaType: "image",
|
||||
mediaUrl: CHARACTER_IMAGE_URL,
|
||||
nodeType: "image",
|
||||
url: [CHARACTER_IMAGE_URL],
|
||||
};
|
||||
connectFromAnchor(makeGenerated("image", "图片生成", rightX));
|
||||
break;
|
||||
}
|
||||
case "图片高清": {
|
||||
anchorUpdates = {
|
||||
kind: "media",
|
||||
title: "图片",
|
||||
subtitle: "图片资源",
|
||||
width: 420,
|
||||
height: 280,
|
||||
inputType: "image",
|
||||
};
|
||||
draftUpdates = {
|
||||
action: "IMAGE_RESOURCE",
|
||||
generated: false,
|
||||
mediaType: "image",
|
||||
mediaUrl: HD_IMAGE_SOURCE_URL,
|
||||
nodeType: "image",
|
||||
url: [HD_IMAGE_SOURCE_URL],
|
||||
};
|
||||
connectFromAnchor(makeGenerated("image", "高清", rightX, { generatorType: "ENHANCE" }));
|
||||
break;
|
||||
}
|
||||
case "音频生视频": {
|
||||
anchorUpdates = {
|
||||
kind: "media",
|
||||
title: "音频",
|
||||
subtitle: "音频资源",
|
||||
width: 430,
|
||||
height: 170,
|
||||
inputType: "audio",
|
||||
};
|
||||
draftUpdates = {
|
||||
action: "AUDIO_RESOURCE",
|
||||
generated: false,
|
||||
mediaType: "audio",
|
||||
mediaUrl: AUDIO_SOURCE_URL,
|
||||
nodeType: "audio",
|
||||
url: [AUDIO_SOURCE_URL],
|
||||
};
|
||||
const video = makeGenerated("video", "视频", rightX, { params: { prompt: AUDIO_TO_VIDEO_PROMPT } });
|
||||
const image = createMediaResourceNode({
|
||||
mediaType: "image",
|
||||
mediaUrl: AUDIO_REFERENCE_IMAGE_URL,
|
||||
title: "图片",
|
||||
width: 430,
|
||||
height: 252,
|
||||
x: anchor.x,
|
||||
y: Math.round(anchor.y + 170 + 120),
|
||||
});
|
||||
nextNodes.push(video, image);
|
||||
nextEdges.push(
|
||||
{ id: makeId("edge"), source: anchor.id, target: video.id },
|
||||
{ id: makeId("edge"), source: image.id, target: video.id }
|
||||
);
|
||||
selectedId = video.id;
|
||||
break;
|
||||
}
|
||||
case "首帧生成视频": {
|
||||
draftUpdates = { action: "VIDEO_GENERATE", params: { prompt: VIDEO_FIRST_FRAME_PROMPT } };
|
||||
connectToAnchor(
|
||||
createMediaResourceNode({
|
||||
mediaType: "image",
|
||||
mediaUrl: VIDEO_FIRST_FRAME_SOURCE_URL,
|
||||
title: "首帧",
|
||||
width: 420,
|
||||
height: 280,
|
||||
x: leftX,
|
||||
y: centerY(280),
|
||||
})
|
||||
);
|
||||
selectedId = anchor.id;
|
||||
break;
|
||||
}
|
||||
case "首尾帧生成视频": {
|
||||
draftUpdates = { action: "VIDEO_GENERATE", params: { prompt: VIDEO_FIRST_LAST_PROMPT } };
|
||||
const first = createMediaResourceNode({
|
||||
mediaType: "image",
|
||||
mediaUrl: VIDEO_FIRST_LAST_FIRST_URL,
|
||||
title: "首帧",
|
||||
width: 420,
|
||||
height: 240,
|
||||
x: leftX,
|
||||
y: Math.round(anchor.y + 18),
|
||||
});
|
||||
const last = createMediaResourceNode({
|
||||
mediaType: "image",
|
||||
mediaUrl: VIDEO_FIRST_LAST_LAST_URL,
|
||||
title: "尾帧",
|
||||
width: 420,
|
||||
height: 240,
|
||||
x: leftX,
|
||||
y: Math.round(anchor.y + 286),
|
||||
});
|
||||
connectToAnchor(first);
|
||||
connectToAnchor(last);
|
||||
selectedId = anchor.id;
|
||||
break;
|
||||
}
|
||||
case "脚本生成分镜脚本": {
|
||||
connectToAnchor(createTextResourceNode({ content: STORY_SCRIPT_TEXT, title: "文本", x: leftX, y: centerY(430) }));
|
||||
selectedId = anchor.id;
|
||||
break;
|
||||
}
|
||||
case "视频参考生成分镜脚本": {
|
||||
draftUpdates = { action: "SCRIPT_GENERATE", params: { prompt: "解析视频内容生成分镜脚本。" } };
|
||||
connectToAnchor(
|
||||
createMediaResourceNode({
|
||||
action: "VIDEO_RESOURCE",
|
||||
mediaType: "video",
|
||||
mediaUrl: SCRIPT_VIDEO_REFERENCE_URL,
|
||||
prompt: "解析视频内容生成分镜脚本。",
|
||||
title: "视频",
|
||||
width: 560,
|
||||
height: 315,
|
||||
x: leftX,
|
||||
y: centerY(315),
|
||||
})
|
||||
);
|
||||
selectedId = anchor.id;
|
||||
break;
|
||||
}
|
||||
case "角色生成分镜脚本": {
|
||||
draftUpdates = { action: "SCRIPT_GENERATE", params: { prompt: SCRIPT_CHARACTER_PROMPT } };
|
||||
const first = createMediaResourceNode({
|
||||
mediaType: "image",
|
||||
mediaUrl: SCRIPT_CHARACTER_ONE_URL,
|
||||
title: "角色 1",
|
||||
width: 300,
|
||||
height: 300,
|
||||
x: leftX,
|
||||
y: Math.round(anchor.y + 10),
|
||||
});
|
||||
const second = createMediaResourceNode({
|
||||
mediaType: "image",
|
||||
mediaUrl: SCRIPT_CHARACTER_TWO_URL,
|
||||
title: "角色 2",
|
||||
width: 300,
|
||||
height: 300,
|
||||
x: leftX,
|
||||
y: Math.round(anchor.y + 330),
|
||||
});
|
||||
connectToAnchor(first);
|
||||
connectToAnchor(second);
|
||||
selectedId = anchor.id;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
setNodes((current) => [
|
||||
...current.map((item) =>
|
||||
item.id === anchor.id
|
||||
? {
|
||||
...item,
|
||||
...anchorUpdates,
|
||||
draftValues: {
|
||||
...(item.draftValues || {}),
|
||||
...draftUpdates,
|
||||
},
|
||||
}
|
||||
: item
|
||||
),
|
||||
...nextNodes,
|
||||
]);
|
||||
if (nextEdges.length > 0) {
|
||||
setEdges((current) => [...current, ...nextEdges]);
|
||||
}
|
||||
setSelectedNodeId(selectedId);
|
||||
setNodeContextMenu(null);
|
||||
setConnectionMenu(null);
|
||||
setCanvasCommand(null);
|
||||
setActivePanel(null);
|
||||
if (actionLabel !== "自己编写内容") toast.success(`已按「${actionLabel}」创建关联节点`);
|
||||
},
|
||||
[nodes]
|
||||
);
|
||||
|
||||
const createFreeNode = useCallback(
|
||||
(kind: CanvasCreateKind) => {
|
||||
if (!canvasCommand) return;
|
||||
@ -1638,6 +2031,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
node={node}
|
||||
selected={node.id === selectedNodeId}
|
||||
onContextMenu={(event) => openNodeContext(event, node)}
|
||||
onModuleAction={(actionLabel) => runTargetModuleAction(node.id, actionLabel)}
|
||||
onOpenConnectionMenu={(event, side) => openConnectionMenu(event, node, side)}
|
||||
onPointerDown={(event) => startNodeDrag(event, node)}
|
||||
/>
|
||||
@ -3565,12 +3959,14 @@ function CloneEdges({ edges, nodes }: { edges: CloneEdge[]; nodes: CloneNode[] }
|
||||
function CloneCanvasNode({
|
||||
node,
|
||||
onContextMenu,
|
||||
onModuleAction,
|
||||
onOpenConnectionMenu,
|
||||
onPointerDown,
|
||||
selected,
|
||||
}: {
|
||||
node: CloneNode;
|
||||
onContextMenu: (event: ReactMouseEvent<HTMLDivElement>) => void;
|
||||
onModuleAction: (actionLabel: TargetModuleActionLabel) => void;
|
||||
onOpenConnectionMenu: (event: ReactMouseEvent<HTMLButtonElement>, side: ConnectionSide) => void;
|
||||
onPointerDown: (event: ReactPointerEvent<HTMLDivElement>) => void;
|
||||
selected: boolean;
|
||||
@ -3579,7 +3975,7 @@ function CloneCanvasNode({
|
||||
if (targetModuleKind) {
|
||||
return (
|
||||
<CanvasNodeShell node={node} onContextMenu={onContextMenu} onOpenConnectionMenu={onOpenConnectionMenu} onPointerDown={onPointerDown} selected={selected}>
|
||||
<TargetModuleCanvasNode kind={targetModuleKind} node={node} selected={selected} />
|
||||
<TargetModuleCanvasNode kind={targetModuleKind} node={node} onAction={onModuleAction} selected={selected} />
|
||||
</CanvasNodeShell>
|
||||
);
|
||||
}
|
||||
@ -3662,14 +4058,16 @@ function CloneCanvasNode({
|
||||
function TargetModuleCanvasNode({
|
||||
kind,
|
||||
node,
|
||||
onAction,
|
||||
selected,
|
||||
}: {
|
||||
kind: AddModuleKind;
|
||||
node: CloneNode;
|
||||
onAction: (actionLabel: TargetModuleActionLabel) => void;
|
||||
selected: boolean;
|
||||
}) {
|
||||
const HeaderIcon = kind === "video-clip" ? Scissors : kind === "audio" ? Music2 : kind === "video" ? Video : kind === "image" ? ImageIcon : FileText;
|
||||
const actions: Record<AddModuleKind, Array<{ icon: LucideIcon | "hd"; label: string }>> = {
|
||||
const actions: Record<AddModuleKind, Array<{ icon: LucideIcon | "hd"; label: TargetModuleActionLabel }>> = {
|
||||
audio: [{ icon: Play, label: "音频生视频" }],
|
||||
image: [
|
||||
{ icon: Upload, label: "图生图" },
|
||||
@ -3728,7 +4126,7 @@ function TargetModuleCanvasNode({
|
||||
<button
|
||||
className="flex items-center gap-3 text-[18px] leading-none text-white/82 hover:text-white"
|
||||
key={action.label}
|
||||
onClick={() => toast.info(`${action.label} 接口待接入。`)}
|
||||
onClick={() => onAction(action.label)}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
type="button"
|
||||
>
|
||||
@ -3918,13 +4316,17 @@ function PresetMediaCanvasNode({ node, selected }: { node: CloneNode; selected:
|
||||
)}
|
||||
>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle,rgba(255,255,255,0.12)_1px,transparent_1px)] bg-[length:18px_18px] opacity-20" />
|
||||
<div className="relative text-center">
|
||||
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-white/8 text-white/70">
|
||||
<Video className="h-7 w-7" />
|
||||
{mediaUrl ? (
|
||||
<video className="relative h-full w-full object-cover" controls playsInline src={mediaUrl} />
|
||||
) : (
|
||||
<div className="relative text-center">
|
||||
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-white/8 text-white/70">
|
||||
<Video className="h-7 w-7" />
|
||||
</div>
|
||||
<div className="mt-4 text-[18px] font-semibold text-white/78">待生成视频</div>
|
||||
<div className="mt-2 text-[13px] text-white/38">生成后将在这里预览</div>
|
||||
</div>
|
||||
<div className="mt-4 text-[18px] font-semibold text-white/78">待生成视频</div>
|
||||
<div className="mt-2 text-[13px] text-white/38">生成后将在这里预览</div>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
className="absolute right-3 top-3 rounded-[8px] bg-white/8 px-3 py-2 text-[12px] text-white/72 hover:bg-white/12"
|
||||
onClick={() => toast.info("视频全屏预览待接入。")}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user