Match LibTV added module node cards
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-04-18 17:27:19 +08:00
parent 46cd4a5899
commit d2199975a5

View File

@ -19,6 +19,7 @@ import {
Scissors,
Settings2,
Share2,
Sparkles,
Trash2,
Upload,
UserRound,
@ -216,6 +217,15 @@ const ADD_NODE_MODULES: Array<{
{ kind: "script", label: "脚本", description: "创意脚本、生成故事板", icon: Layers, badge: "Beta" },
];
const TARGET_MODULE_CENTER_OFFSETS: Record<AddModuleKind, { x: number; y: number }> = {
audio: { x: 705, y: -257 },
image: { x: 53, y: -257 },
script: { x: 556, y: 253 },
text: { x: -742, y: -257 },
video: { x: -574, y: 253 },
"video-clip": { x: 80, y: 253 },
};
const STARTER_CARDS = [
{
title: "故事脚本生成",
@ -655,13 +665,13 @@ function createTargetModuleNode(kind: AddModuleKind, x: number, y: number, index
return {
id: makeId("script"),
kind: "script",
title: `脚本 ${index}`,
title: "脚本生成器",
subtitle: "脚本生成",
inputType: "text",
x,
y,
width: 520,
height: 360,
width: 438,
height: 470,
accent: "#f0d06b",
draftValues: {
action: "SCRIPT_GENERATE",
@ -683,8 +693,8 @@ function createTargetModuleNode(kind: AddModuleKind, x: number, y: number, index
inputType: "video",
x,
y,
width: 210,
height: 116,
width: 436,
height: 470,
accent: "#ffb8c8",
draftValues: {
action: "VIDEO_CLIP_RESOURCE",
@ -737,6 +747,7 @@ function createTargetModuleNode(kind: AddModuleKind, x: number, y: number, index
},
};
const item = meta[kind];
const isWideModule = item.nodeType === "image" || item.nodeType === "video";
return {
id: makeId(kind),
kind: item.kind,
@ -745,8 +756,8 @@ function createTargetModuleNode(kind: AddModuleKind, x: number, y: number, index
inputType: item.nodeType,
x,
y,
width: item.nodeType === "text" ? 210 : 194,
height: item.nodeType === "text" ? 122 : 116,
width: isWideModule ? 773 : 436,
height: 470,
accent: item.accent,
draftValues: {
action: item.action,
@ -814,6 +825,22 @@ function getPresetMode(node: CloneNode | null | undefined): ActiveStarterScene {
return null;
}
function getTargetModuleKind(node: CloneNode): AddModuleKind | null {
if (node.kind === "media" || getPresetMode(node)) return null;
const nodeType = getDraftString(node, "nodeType");
if (
nodeType === "text" ||
nodeType === "image" ||
nodeType === "video" ||
nodeType === "video-clip" ||
nodeType === "audio" ||
nodeType === "script"
) {
return nodeType;
}
return null;
}
function createPresetMediaNode({
action,
generated = false,
@ -1156,7 +1183,13 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
const addTargetModule = useCallback(
(kind: AddModuleKind, point?: { x: number; y: number }) => {
const center = point || toCanvasPoint(window.innerWidth / 2, window.innerHeight / 2);
const viewportCenter = toCanvasPoint(window.innerWidth / 2, window.innerHeight / 2);
const offset = TARGET_MODULE_CENTER_OFFSETS[kind];
const sameKindCount = nodes.filter((node) => getTargetModuleKind(node) === kind).length;
const center = point || {
x: viewportCenter.x + offset.x + sameKindCount * 28,
y: viewportCenter.y + offset.y + sameKindCount * 28,
};
const created = createTargetModuleNode(kind, 0, 0, nodes.length + 1);
const node = {
...created,
@ -3376,6 +3409,15 @@ function CloneCanvasNode({
onPointerDown: (event: ReactPointerEvent<HTMLDivElement>) => void;
selected: boolean;
}) {
const targetModuleKind = getTargetModuleKind(node);
if (targetModuleKind) {
return (
<CanvasNodeShell node={node} onContextMenu={onContextMenu} onPointerDown={onPointerDown} selected={selected}>
<TargetModuleCanvasNode kind={targetModuleKind} node={node} selected={selected} />
</CanvasNodeShell>
);
}
if (node.kind === "media") {
return (
<CanvasNodeShell node={node} onContextMenu={onContextMenu} onPointerDown={onPointerDown} selected={selected}>
@ -3451,6 +3493,133 @@ function CloneCanvasNode({
);
}
function TargetModuleCanvasNode({
kind,
node,
selected,
}: {
kind: AddModuleKind;
node: CloneNode;
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 }>> = {
audio: [{ icon: Play, label: "音频生视频" }],
image: [
{ icon: Upload, label: "图生图" },
{ icon: "hd", label: "图片高清" },
],
script: [
{ icon: FileText, label: "脚本生成分镜脚本" },
{ icon: Video, label: "视频参考生成分镜脚本" },
{ icon: UserRound, label: "角色生成分镜脚本" },
],
text: [
{ icon: FileText, label: "自己编写内容" },
{ icon: Play, label: "文生视频" },
{ icon: ImageIcon, label: "图片反推提示词" },
{ icon: Music2, label: "文字生音乐" },
],
video: [
{ icon: Layers, label: "首尾帧生成视频" },
{ icon: Sparkles, label: "首帧生成视频" },
],
"video-clip": [],
};
const actionTop =
kind === "text"
? "top-[166px]"
: kind === "image" || kind === "video"
? "top-[220px]"
: kind === "script"
? "top-[190px]"
: "top-[190px]";
return (
<div className="h-full w-full">
<div className="mb-[10px] flex h-7 items-center gap-2 text-[22px] font-semibold leading-none text-white/44">
<HeaderIcon className="h-[22px] w-[22px]" />
<span>{node.title}</span>
</div>
<div
className={cn(
"relative h-[432px] overflow-hidden rounded-[8px] border border-white/8 bg-[#252525] shadow-[0_16px_38px_rgba(0,0,0,0.24)]",
selected && "outline outline-1 outline-white/22"
)}
>
<div className={cn("absolute inset-x-0 flex justify-center text-white/18", kind === "text" || kind === "script" ? "top-[62px]" : "top-[116px]")}>
<TargetModuleHeroGraphic kind={kind} />
</div>
{kind === "video-clip" ? (
<div className="absolute inset-x-0 top-[254px] text-center text-[16px] text-white/34">
空空如也,请连接多个视频节点后操作
</div>
) : (
<div className={cn("absolute left-8", actionTop)}>
<div className="mb-5 text-[17px] text-white/42">尝试:</div>
<div className="space-y-5">
{actions[kind].map((action) => (
<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} 接口待接入。`)}
onPointerDown={(event) => event.stopPropagation()}
type="button"
>
<ModuleActionIcon icon={action.icon} />
<span>{action.label}</span>
</button>
))}
</div>
</div>
)}
</div>
</div>
);
}
function TargetModuleHeroGraphic({ kind }: { kind: AddModuleKind }) {
if (kind === "text" || kind === "script") {
return (
<div className="flex w-[92px] flex-col gap-[9px]">
{[70, 70, 70, 70, 43].map((width, index) => (
<span className="h-[7px] rounded-full bg-white/16" key={`${width}-${index}`} style={{ width }} />
))}
</div>
);
}
if (kind === "image") {
return <ImageIcon className="h-[92px] w-[92px]" strokeWidth={1.5} />;
}
if (kind === "audio") {
return <Music2 className="h-[86px] w-[86px]" strokeWidth={1.5} />;
}
if (kind === "video") {
return (
<span className="flex h-20 w-20 items-center justify-center rounded-[8px] bg-white/18 text-[#242424]">
<Play className="ml-1 h-11 w-11 fill-current" strokeWidth={0} />
</span>
);
}
return <Scissors className="h-[92px] w-[92px]" strokeWidth={1.4} />;
}
function ModuleActionIcon({ icon }: { icon: LucideIcon | "hd" }) {
if (icon === "hd") {
return (
<span className="flex h-[18px] min-w-[24px] items-center justify-center rounded-[3px] border border-white/72 px-1 text-[10px] font-bold leading-none text-white/86">
HD
</span>
);
}
const Icon = icon;
return <Icon className="h-[18px] w-[18px] shrink-0 text-white/86" strokeWidth={2} />;
}
function CanvasNodeShell({
children,
node,
@ -3464,10 +3633,12 @@ function CanvasNodeShell({
onPointerDown: (event: ReactPointerEvent<HTMLDivElement>) => void;
selected: boolean;
}) {
const targetModuleKind = getTargetModuleKind(node);
return (
<div
className={cn(
"absolute cursor-grab select-none shadow-[0_14px_34px_rgba(0,0,0,0.35)] active:cursor-grabbing",
"absolute cursor-grab select-none active:cursor-grabbing",
targetModuleKind ? "shadow-none" : "shadow-[0_14px_34px_rgba(0,0,0,0.35)]",
selected && "z-20"
)}
data-canvas-node="true"
@ -3476,8 +3647,21 @@ function CanvasNodeShell({
onPointerDown={onPointerDown}
style={{ height: node.height, left: node.x, top: node.y, width: node.width }}
>
<span className="absolute left-[-10px] top-1/2 z-10 h-5 w-5 -translate-y-1/2 rounded-full border border-white/30 bg-white" />
<span className="absolute right-[-10px] top-1/2 z-10 h-5 w-5 -translate-y-1/2 rounded-full border border-white/30 bg-white" />
{targetModuleKind ? (
<>
<span className="absolute left-[-13px] top-1/2 z-10 mt-[14px] flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-full border border-white/18 bg-[#242424] text-[20px] leading-none text-white/50">
+
</span>
<span className="absolute right-[-13px] top-1/2 z-10 mt-[14px] flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-full border border-white/18 bg-[#242424] text-[20px] leading-none text-white/50">
+
</span>
</>
) : (
<>
<span className="absolute left-[-10px] top-1/2 z-10 h-5 w-5 -translate-y-1/2 rounded-full border border-white/30 bg-white" />
<span className="absolute right-[-10px] top-1/2 z-10 h-5 w-5 -translate-y-1/2 rounded-full border border-white/30 bg-white" />
</>
)}
{children}
</div>
);