Fix text module composer replica
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-19 09:04:55 +08:00
parent 37b7e58f2a
commit 193b03d677

View File

@ -387,6 +387,8 @@ const OFFICIAL_ZOOM_DOCK_SCALE = 0.5;
const PROMPT_COMPOSER_BASE_WIDTH = 895;
const PROMPT_COMPOSER_BASE_HEIGHT = 286;
const PRESET_COMPOSER_BASE_HEIGHT = 238;
const TEXT_MODULE_COMPOSER_WIDTH = 1274;
const TEXT_MODULE_COMPOSER_HEIGHT = 270;
const MULTI_VIEW_NINE_GRID_PROMPT =
"以选中图片为角色/主体参考,生成多视角 3x3 九宫格分镜图。保持主体一致,九个镜头包含远景、全身、侧身、背面、半身、近景、局部细节等视角,使用清晰黑色分割线分隔每一格,白色或干净背景。";
const NINE_GRID_CELL_VISUALS: GridCellVisual[] = [
@ -2080,23 +2082,15 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
switch (actionLabel) {
case "自己编写内容": {
anchorUpdates = {
kind: "script",
title: "文本",
subtitle: "文本资源",
inputType: "text",
width: OFFICIAL_TEXT_RESOURCE_WIDTH,
height: OFFICIAL_TEXT_RESOURCE_HEIGHT,
};
draftUpdates = {
action: "TEXT_RESOURCE",
content: [],
action: "TEXT_GENERATE",
content: Array.isArray(anchor.draftValues?.content) ? anchor.draftValues?.content : [],
manualEditing: true,
manualTextComposer: true,
nodeType: "text",
scriptRole: "resource",
viewMode: undefined,
};
toast.success("已切换为文本编写节点");
selectedId = anchor.id;
toast.success("已打开文本编写对话框");
break;
}
case "文生视频": {
@ -3321,6 +3315,8 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
const selectedPresetControlNode = isOfficialImageControlNode(selectedNode) ? selectedNode : null;
const selectedPresetMode = getPresetMode(selectedPresetControlNode);
const selectedTargetModuleKind = selectedNode ? getTargetModuleKind(selectedNode) : null;
const selectedTextResourceNode =
selectedNode && getDraftString(selectedNode, "action") === "TEXT_RESOURCE" ? selectedNode : null;
const selectedPresetAnchor = useMemo<PresetControlAnchor | null>(() => {
if (!selectedPresetControlNode) return null;
return {
@ -3330,6 +3326,15 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
zoom: viewport.zoom,
};
}, [selectedPresetControlNode, viewport.x, viewport.y, viewport.zoom]);
const selectedTargetAnchor = useMemo<PresetControlAnchor | null>(() => {
if (!selectedNode || !selectedTargetModuleKind) return null;
return {
bottomY: Math.round(viewport.y + (selectedNode.y + selectedNode.height) * viewport.zoom),
centerX: Math.round(viewport.x + (selectedNode.x + selectedNode.width / 2) * viewport.zoom),
topY: Math.round(viewport.y + selectedNode.y * viewport.zoom),
zoom: viewport.zoom,
};
}, [selectedNode, selectedTargetModuleKind, viewport.x, viewport.y, viewport.zoom]);
return (
<div
@ -3459,6 +3464,35 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
/>
) : null}
{selectedNode && selectedTargetModuleKind === "text" && selectedTargetAnchor ? (
<TextTargetModuleComposer
anchor={selectedTargetAnchor}
node={selectedNode}
onSubmit={(prompt) => {
setNodes((current) =>
current.map((node) =>
node.id === selectedNode.id
? {
...node,
draftValues: {
...(node.draftValues || {}),
content: prompt ? [prompt] : [],
manualEditing: true,
manualTextComposer: true,
params: {
...((node.draftValues?.params as Record<string, unknown> | undefined) || {}),
prompt,
},
},
}
: node
)
);
toast.success("文本内容已写入当前模块");
}}
/>
) : null}
<TopBar
draftTitle={draftTitle}
isTitleEditing={isTitleEditing}
@ -3514,7 +3548,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
/>
) : null}
{selectedNode && !selectedPresetControlNode && !selectedTargetModuleKind ? (
{selectedNode && !selectedPresetControlNode && !selectedTargetModuleKind && !selectedTextResourceNode ? (
<InspectorPanel
node={selectedNode}
onChangeProperty={updateNodeDraft}
@ -4242,6 +4276,67 @@ function PresetWorkflowControls({
);
}
function TextTargetModuleComposer({
anchor,
node,
onSubmit,
}: {
anchor: PresetControlAnchor;
node: CloneNode;
onSubmit: (prompt: string) => void;
}) {
const contentValues = node.draftValues?.content;
const initialPrompt =
Array.isArray(contentValues) && typeof contentValues[0] === "string" ? contentValues[0] : "";
const [prompt, setPrompt] = useState(initialPrompt);
useEffect(() => {
setPrompt(initialPrompt);
}, [initialPrompt, node.id]);
return (
<div
className="pointer-events-auto absolute z-30 overflow-visible"
data-canvas-overlay="true"
style={{
height: TEXT_MODULE_COMPOSER_HEIGHT,
left: anchor.centerX,
top: anchor.bottomY + 24,
transform: "translateX(-50%)",
width: TEXT_MODULE_COMPOSER_WIDTH,
}}
>
<div className="relative h-[270px] w-[1274px] rounded-[16px] border border-white/8 bg-[#262626]/98 p-6 text-white shadow-[0_24px_84px_rgba(0,0,0,0.48)]">
<textarea
className="h-[148px] w-full resize-none bg-transparent text-[24px] leading-[34px] text-white/74 outline-none placeholder:text-white/40"
onChange={(event) => setPrompt(event.target.value)}
onPointerDown={(event) => event.stopPropagation()}
placeholder="写下你想讲的故事、场景或角色设定。例如:一个来自未来的机器人,在城市屋顶看星星。"
value={prompt}
/>
<div className="absolute bottom-5 left-6 right-[86px] flex items-center gap-8 overflow-hidden text-[24px] text-white/82">
<ComposerControl
label="GVLM 3.1"
icon="⌘"
dropdown
onClick={() => toast.info("文本模型选择后端待接入。")}
/>
<div className="min-w-0 flex-1" />
<ComposerControl label="文" onClick={() => toast.info("翻译接口待接入。")} />
<ComposerControl label="6" icon="⚡" muted onClick={() => toast.info("积分估算接入本地积分系统。")} />
</div>
<button
className="absolute bottom-5 right-5 flex h-14 w-14 items-center justify-center rounded-[14px] bg-white/76 text-[34px] leading-none text-black transition hover:bg-white"
onClick={() => onSubmit(prompt)}
type="button"
>
↑
</button>
</div>
</div>
);
}
function CharacterPresetToolbar({
anchor,
onToolAction,