You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
108 lines
5.2 KiB
108 lines
5.2 KiB
"use client";
|
|
|
|
import { useI18n } from "@/i18n";
|
|
import { getNodeOutputMediaType, SINGLE_INPUT_HANDLE_ID } from "@/utils/nodeHandles";
|
|
import type { EaseCurveNodeData, VideoStitchNodeData, WorkflowEdge, WorkflowNode } from "@/types";
|
|
|
|
interface ComposerProcessBodyProps {
|
|
node: WorkflowNode | null | undefined;
|
|
nodes: WorkflowNode[];
|
|
edges: WorkflowEdge[];
|
|
videoStitchData: VideoStitchNodeData | null;
|
|
easeCurveData: EaseCurveNodeData | null;
|
|
}
|
|
|
|
function getNodeDisplayTitle(node: WorkflowNode | undefined): string {
|
|
if (!node) return "Unknown";
|
|
const data = node.data as { customTitle?: unknown; inputPrompt?: unknown };
|
|
if (typeof data.customTitle === "string" && data.customTitle.trim()) return data.customTitle.trim();
|
|
if (typeof data.inputPrompt === "string" && data.inputPrompt.trim()) return data.inputPrompt.trim().slice(0, 32);
|
|
return node.type;
|
|
}
|
|
|
|
function nodeHasVideoOutput(node: WorkflowNode | undefined): boolean {
|
|
if (!node) return false;
|
|
const data = node.data as { outputVideo?: unknown; video?: unknown };
|
|
return Boolean(data.outputVideo || data.video);
|
|
}
|
|
|
|
function buildBezierPreviewPath(handles: [number, number, number, number]): string {
|
|
const [x1, y1, x2, y2] = handles;
|
|
return `M 8 92 C ${8 + x1 * 84} ${92 - y1 * 84}, ${8 + x2 * 84} ${92 - y2 * 84}, 92 8`;
|
|
}
|
|
|
|
function formatBezierHandles(handles: [number, number, number, number]): string {
|
|
return handles.map((value) => value.toFixed(2)).join(", ");
|
|
}
|
|
|
|
export function ComposerProcessBody({
|
|
node,
|
|
nodes,
|
|
edges,
|
|
videoStitchData,
|
|
easeCurveData,
|
|
}: ComposerProcessBodyProps) {
|
|
const { t } = useI18n();
|
|
|
|
if (node?.type === "videoStitch") {
|
|
const stitchEdges = edges.filter((edge) => {
|
|
if (edge.target !== node.id || edge.targetHandle !== SINGLE_INPUT_HANDLE_ID) return false;
|
|
const sourceNode = nodes.find((candidate) => candidate.id === edge.source);
|
|
return sourceNode ? getNodeOutputMediaType(sourceNode) === "video" : false;
|
|
});
|
|
|
|
return (
|
|
<div className="grid gap-3 md:grid-cols-[1fr_180px]">
|
|
<div className="min-w-0 rounded-lg border border-[var(--border-subtle)] bg-[var(--surface-1)] p-3">
|
|
<div className="text-xs font-medium text-[var(--text-secondary)]">{t("composer.videoStitchInputs")}</div>
|
|
<div className="nowheel mt-3 grid max-h-28 grid-cols-2 gap-2 overflow-y-auto pr-1">
|
|
{stitchEdges.map((edge, index) => {
|
|
const sourceNode = nodes.find((candidate) => candidate.id === edge.source);
|
|
return (
|
|
<div key={edge.id} className="flex min-w-0 items-center gap-2 rounded-md border border-[var(--border-subtle)] bg-[var(--surface-2)] px-2 py-2">
|
|
<span className={`h-2 w-2 shrink-0 rounded-full ${nodeHasVideoOutput(sourceNode) ? "bg-[var(--handle-color)]" : "bg-[var(--text-disabled)]"}`} />
|
|
<div className="min-w-0">
|
|
<div className="truncate text-xs text-[var(--text-secondary)]">{t("composer.videoStitchClip", { index: index + 1 })}</div>
|
|
<div className="truncate text-[11px] text-[var(--text-muted)]">{getNodeDisplayTitle(sourceNode)}</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
<div className="rounded-lg border border-[var(--border-subtle)] bg-[var(--surface-1)] p-3 text-xs text-[var(--text-muted)]">
|
|
{videoStitchData?.outputVideo ? t("composer.videoStitchOutputReady") : t("composer.videoStitchNoOutput")}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (node?.type === "easeCurve") {
|
|
const handles = easeCurveData?.bezierHandles ?? [0.42, 0, 0.58, 1];
|
|
|
|
return (
|
|
<div className="grid gap-3 md:grid-cols-[1fr_180px]">
|
|
<div className="min-w-0 rounded-lg border border-[var(--border-subtle)] bg-[var(--surface-1)] p-3">
|
|
<div className="text-xs font-medium text-[var(--text-secondary)]">{t("composer.easeCurveSettings")}</div>
|
|
<div className="mt-3 grid gap-3 md:grid-cols-[120px_1fr]">
|
|
<div className="rounded-md border border-[var(--border-subtle)] bg-[var(--surface-1)] p-2">
|
|
<svg viewBox="0 0 100 100" className="h-24 w-full">
|
|
<path d="M 8 92 L 92 8" stroke="rgb(64 64 64)" strokeWidth="2" strokeDasharray="5 5" fill="none" />
|
|
<path d={buildBezierPreviewPath(handles)} stroke="rgb(190 242 100)" strokeWidth="4" fill="none" strokeLinecap="round" />
|
|
</svg>
|
|
</div>
|
|
<div className="min-w-0 space-y-2 text-xs text-[var(--text-secondary)]">
|
|
<div className="rounded-md border border-[var(--border-subtle)] bg-[var(--surface-2)] px-2 py-2">{easeCurveData?.easingPreset ?? t("composer.easeCurveCustom")}</div>
|
|
<div className="rounded-md border border-[var(--border-subtle)] bg-[var(--surface-2)] px-2 py-2 font-mono text-[11px]">{formatBezierHandles(handles)}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="rounded-lg border border-[var(--border-subtle)] bg-[var(--surface-1)] p-3 text-xs text-[var(--text-muted)]">
|
|
{easeCurveData?.outputVideo ? t("composer.easeCurveOutputReady") : t("composer.easeCurveNoOutput")}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|