From a92bcd0caf5b28c8bcff00b3965a43fdee0c0b6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=8B=E5=90=91=E8=88=AA?= Date: Wed, 6 May 2026 10:13:41 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dev-server.codex.err.log | 17 + dev-server.codex.log | 84 +++ src/components/nodes/GenerateVideoNode.tsx | 550 ++++++++++++++++-- .../nodes/GenerationPromptInput.tsx | 294 +++++++++- src/components/nodes/LLMGenerateNode.tsx | 98 ++-- src/components/nodes/PromptNode.tsx | 18 +- src/components/nodes/TextNodePlaceholder.tsx | 69 +++ src/components/nodes/VideoInputNode.tsx | 128 ++-- src/store/utils/nodeDefaults.ts | 1 + src/types/nodes.ts | 1 + 10 files changed, 1127 insertions(+), 133 deletions(-) create mode 100644 dev-server.codex.err.log create mode 100644 dev-server.codex.log create mode 100644 src/components/nodes/TextNodePlaceholder.tsx diff --git a/dev-server.codex.err.log b/dev-server.codex.err.log new file mode 100644 index 00000000..8784b8ac --- /dev/null +++ b/dev-server.codex.err.log @@ -0,0 +1,17 @@ +[baseline-browser-mapping] The data in this module is over two months old. To ensure accurate Baseline data, please update: `npm i baseline-browser-mapping@latest -D` +[Models] fal: fal.ai API error: 401 +[Models] fal: fal.ai API error: 401 +[Models] fal: fal.ai API error: 401 +[Models] fal: fal.ai API error: 401 +[Models] fal: fal.ai API error: 401 +[Models] fal: fal.ai API error: 401 +Several Konva instances detected. It is not recommended to use multiple Konva instances in the same environment. +[Models] fal: fal.ai API error: 401 +[Models] fal: fal.ai API error: 401 +[Models] fal: fal.ai API error: 401 +Several Konva instances detected. It is not recommended to use multiple Konva instances in the same environment. +[Models] fal: fal.ai API error: 401 +[Models] fal: fal.ai API error: 401 +[Models] fal: fal.ai API error: 401 +⚠ Fast Refresh had to perform a full reload due to a runtime error. +Several Konva instances detected. It is not recommended to use multiple Konva instances in the same environment. diff --git a/dev-server.codex.log b/dev-server.codex.log new file mode 100644 index 00000000..a6d52ebb --- /dev/null +++ b/dev-server.codex.log @@ -0,0 +1,84 @@ +Attention: Next.js now collects completely anonymous telemetry regarding usage. +This information is used to shape Next.js' roadmap and prioritize features. +You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL: +https://nextjs.org/telemetry + +Creating turbopack project { dir: 'C:\\Users\\EDY\\Desktop\\node-banana-master', testMode: true } +> Ready on http://localhost:3000 +> Server timeout set to 10 minutes + GET / 200 in 1942ms (compile: 1611ms, render: 332ms) + GET / 200 in 76ms (compile: 10ms, render: 66ms) + GET /api/env-status 200 in 75ms (compile: 62ms, render: 13ms) + GET /api/env-status 200 in 6ms (compile: 2ms, render: 4ms) + GET / 200 in 72ms (compile: 5ms, render: 67ms) + GET /api/env-status 200 in 6ms (compile: 2ms, render: 4ms) + GET /api/env-status 200 in 11ms (compile: 7ms, render: 4ms) + GET /api/models?provider=fal&capabilities=text-to-video,image-to-video,audio-to-video 500 in 715ms (compile: 44ms, render: 671ms) +✓ Compiled in 195ms + GET /api/models?provider=fal&capabilities=text-to-video,image-to-video,audio-to-video 500 in 653ms (compile: 3ms, render: 650ms) +✓ Compiled in 174ms + GET /api/models?provider=fal&capabilities=text-to-video,image-to-video,audio-to-video 500 in 467ms (compile: 5ms, render: 463ms) +✓ Compiled in 226ms + GET /api/models?provider=fal&capabilities=text-to-video,image-to-video,audio-to-video 500 in 577ms (compile: 2ms, render: 575ms) +✓ Compiled in 166ms + GET /api/models?provider=fal&capabilities=text-to-video,image-to-video,audio-to-video 500 in 405ms (compile: 5ms, render: 400ms) +✓ Compiled in 187ms + GET /api/models?provider=fal&capabilities=text-to-video,image-to-video,audio-to-video 500 in 404ms (compile: 2ms, render: 401ms) +✓ Compiled in 185ms +✓ Compiled in 198ms +✓ Compiled in 208ms +✓ Compiled in 199ms +✓ Compiled in 187ms +✓ Compiled in 192ms +✓ Compiled in 180ms +✓ Compiled in 183ms +✓ Compiled in 161ms +✓ Compiled in 190ms +✓ Compiled in 171ms + GET / 200 in 402ms (compile: 112ms, render: 289ms) + GET / 200 in 57ms (compile: 6ms, render: 51ms) + GET /api/models?provider=fal&capabilities=text-to-video,image-to-video,audio-to-video 500 in 501ms (compile: 3ms, render: 498ms) +Deleted old log file: session-exec-2026-04-29T08-04-54-vpqtdk.json + POST /api/logs 200 in 53ms (compile: 31ms, render: 22ms) +Deleted old log file: session-exec-2026-04-29T08-11-48-ddisah.json + POST /api/logs 200 in 9ms (compile: 1581µs, render: 8ms) +✓ Compiled in 217ms +✓ Compiled in 182ms + GET /api/models?provider=fal&capabilities=text-to-video,image-to-video,audio-to-video 500 in 832ms (compile: 4ms, render: 829ms) +✓ Compiled in 198ms + GET /api/models?provider=fal&capabilities=text-to-video,image-to-video,audio-to-video 500 in 387ms (compile: 2ms, render: 385ms) +✓ Compiled in 239ms +✓ Compiled in 162ms +✓ Compiled in 179ms +✓ Compiled in 179ms +✓ Compiled in 194ms +✓ Compiled in 190ms + GET / 200 in 376ms (compile: 121ms, render: 255ms) + GET / 200 in 49ms (compile: 4ms, render: 45ms) + GET / 200 in 49ms (compile: 5ms, render: 44ms) + GET /api/models?provider=fal&capabilities=text-to-video,image-to-video,audio-to-video 500 in 917ms (compile: 3ms, render: 914ms) +Deleted old log file: session-exec-2026-04-29T08-11-49-4cn1pc.json + POST /api/logs 200 in 12ms (compile: 3ms, render: 9ms) +Deleted old log file: session-exec-2026-04-29T08-11-50-7zc8dw.json + POST /api/logs 200 in 10ms (compile: 1839µs, render: 8ms) +✓ Compiled in 173ms + GET /api/models?provider=fal&capabilities=text-to-video,image-to-video,audio-to-video 500 in 566ms (compile: 3ms, render: 564ms) +✓ Compiled in 160ms +✓ Compiled in 178ms + GET /api/models?provider=fal&capabilities=text-to-video,image-to-video,audio-to-video 500 in 520ms (compile: 1765µs, render: 519ms) + GET /api/models?provider=gemini&capabilities=text-to-video,image-to-video,audio-to-video 200 in 10ms (compile: 4ms, render: 6ms) +✓ Compiled in 165ms +✓ Compiled in 168ms + GET /api/models?provider=gemini&capabilities=text-to-video,image-to-video,audio-to-video 200 in 8ms (compile: 3ms, render: 5ms) +✓ Compiled in 170ms +✓ Compiled in 187ms + GET / 200 in 455ms (compile: 153ms, render: 302ms) +✓ Compiled in 172ms +✓ Compiled in 173ms +✓ Compiled in 179ms +✓ Compiled in 173ms +✓ Compiled in 170ms +✓ Compiled in 162ms +✓ Compiled in 215ms +✓ Compiled in 177ms +✓ Compiled in 162ms diff --git a/src/components/nodes/GenerateVideoNode.tsx b/src/components/nodes/GenerateVideoNode.tsx index 521f0038..9ae13225 100644 --- a/src/components/nodes/GenerateVideoNode.tsx +++ b/src/components/nodes/GenerateVideoNode.tsx @@ -16,7 +16,7 @@ import { useVideoBlobUrl } from "@/hooks/useVideoBlobUrl"; import { useVideoAutoplay } from "@/hooks/useVideoAutoplay"; import { useInlineParameters } from "@/hooks/useInlineParameters"; import { InlineParameterPanel } from "./InlineParameterPanel"; -import { GenerationPromptInput } from "./GenerationPromptInput"; +import { GenerationPromptInput, LIBTV_VIDEO_PROMPT_PLACEHOLDER } from "./GenerationPromptInput"; import { SettingsTabBar } from "./SettingsTabBar"; import { browseRegistry } from "@/utils/browseRegistry"; import { downloadMedia } from "@/utils/downloadMedia"; @@ -46,6 +46,174 @@ function buildVeoInputSchema(modelId: string): ModelInputDef[] | undefined { return inputs; } +const VIDEO_REFERENCE_ACCEPTED_FORMATS = "image/png,image/jpeg,image/webp,video/mp4,video/webm,video/quicktime"; +const MAX_REFERENCE_IMAGE_SIZE = 10 * 1024 * 1024; +const MAX_REFERENCE_VIDEO_SIZE = 200 * 1024 * 1024; + +const BUILT_IN_VIDEO_MODELS: ProviderModel[] = [ + { + id: "veo-3.1/text-to-video", + name: "Veo 3.1", + description: "Native Gemini Veo text-to-video generation.", + provider: "gemini", + capabilities: ["text-to-video"], + pricing: { type: "per-second", amount: 0.4, currency: "USD" }, + }, + { + id: "veo-3.1/image-to-video", + name: "Veo 3.1 I2V", + description: "Native Gemini Veo image-to-video generation.", + provider: "gemini", + capabilities: ["image-to-video"], + pricing: { type: "per-second", amount: 0.4, currency: "USD" }, + }, + { + id: "veo-3.1-fast/text-to-video", + name: "Veo 3.1 Fast", + description: "Fast native Gemini Veo text-to-video generation.", + provider: "gemini", + capabilities: ["text-to-video"], + pricing: { type: "per-second", amount: 0.15, currency: "USD" }, + }, + { + id: "veo-3.1-fast/image-to-video", + name: "Veo 3.1 Fast I2V", + description: "Fast native Gemini Veo image-to-video generation.", + provider: "gemini", + capabilities: ["image-to-video"], + pricing: { type: "per-second", amount: 0.15, currency: "USD" }, + }, + { + id: "bytedance/seedance-2/text-to-video", + name: "Seedance 2.0", + description: "Seedance 2.0 text-to-video generation via Kie.ai.", + provider: "kie", + capabilities: ["text-to-video"], + }, + { + id: "bytedance/seedance-2/image-to-video", + name: "Seedance 2.0 I2V", + description: "Seedance 2.0 image-to-video generation via Kie.ai.", + provider: "kie", + capabilities: ["image-to-video"], + }, + { + id: "kling-3.0/video/text-to-video", + name: "Kling 3.0", + description: "Kling 3.0 text-to-video generation via Kie.ai.", + provider: "kie", + capabilities: ["text-to-video"], + }, + { + id: "wan/2-7-text-to-video", + name: "Wan 2.7", + description: "Wan 2.7 text-to-video generation via Kie.ai.", + provider: "kie", + capabilities: ["text-to-video"], + }, +]; + +function isVideoReference(media: string | null | undefined): boolean { + if (!media) return false; + return media.startsWith("data:video/") || media.startsWith("blob:") || /\.(mp4|webm|mov)(\?|#|$)/i.test(media); +} + +function formatVideoResolution(value: string): string { + return value.replace(/p$/i, "P").toUpperCase(); +} + +function getVideoSettingsConfig(modelId: string | undefined) { + const id = (modelId || "").toLowerCase(); + + if (id.startsWith("veo-3.1")) { + return { + aspectRatios: ["16:9", "9:16"], + resolutions: ["720p", "1080p", "4k"], + durations: ["4", "6", "8"], + }; + } + + if (id.includes("seedance")) { + return { + aspectRatios: ["16:9", "9:16", "1:1", "4:3", "3:4", "21:9"], + resolutions: id.includes("fast") ? ["480p", "720p"] : ["480p", "720p", "1080p"], + durations: ["5", "10", "15"], + }; + } + + if (id.includes("kling-3.0")) { + return { + aspectRatios: ["16:9", "9:16", "1:1"], + resolutions: [], + durations: ["3", "5", "10", "15"], + }; + } + + if (id.includes("kling")) { + return { + aspectRatios: ["16:9", "9:16", "1:1"], + resolutions: [], + durations: ["5", "10"], + }; + } + + if (id.includes("wan/2-7")) { + return { + aspectRatios: id.includes("text-to-video") ? ["16:9", "9:16", "1:1", "4:3", "3:4"] : ["16:9", "9:16", "1:1"], + resolutions: ["720p", "1080p"], + durations: ["5", "10", "15"], + }; + } + + if (id.includes("wan")) { + return { + aspectRatios: ["16:9", "9:16", "1:1"], + resolutions: ["720p", "1080p"], + durations: ["5", "10", "15"], + }; + } + + if (id.includes("grok")) { + return { + aspectRatios: ["2:3", "3:2", "1:1", "16:9", "9:16"], + resolutions: [], + durations: ["6", "10"], + }; + } + + return { + aspectRatios: ["16:9", "9:16", "1:1"], + resolutions: ["720p", "1080p"], + durations: ["5", "10"], + }; +} + +function getVideoParameterKeys(modelId: string | undefined) { + const id = (modelId || "").toLowerCase(); + + if (id.startsWith("veo-3.1")) { + return { aspect: "aspectRatio", resolution: "resolution", duration: "durationSeconds" }; + } + + if (id.includes("wan/2-7/text-to-video")) { + return { aspect: "ratio", resolution: "resolution", duration: "duration" }; + } + + if (id.includes("wan/2-7/image-to-video")) { + return { aspect: undefined, resolution: "resolution", duration: "duration" }; + } + + return { aspect: "aspect_ratio", resolution: "resolution", duration: "duration" }; +} + +function coerceVideoDuration(modelId: string | undefined, key: string, value: string): string | number { + const id = (modelId || "").toLowerCase(); + if (key === "durationSeconds" || id.includes("seedance") || id.includes("wan/2-7")) { + return Number(value); + } + return value; +} + type GenerateVideoNodeType = Node; export function GenerateVideoNode({ id, data, selected }: NodeProps) { @@ -74,6 +242,7 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps(null); // Register browse callback for floating header button useEffect(() => { @@ -81,7 +250,7 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps { browseRegistry.unregister(id); }; }, [id]); - const currentProvider: ProviderType = nodeData.selectedModel?.provider || "fal"; + const currentProvider: ProviderType = nodeData.selectedModel?.provider || "gemini"; // Get enabled providers const enabledProviders = useMemo(() => { @@ -161,6 +330,46 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps) => { + const file = e.target.files?.[0]; + e.target.value = ""; + if (!file) return; + + const isImage = file.type.match(/^image\/(png|jpeg|webp)$/); + const isVideo = file.type.match(/^video\/(mp4|webm|quicktime)$/); + + if (!isImage && !isVideo) { + alert("Unsupported format. Use PNG, JPG, WebP, MP4, WebM, or QuickTime."); + return; + } + + const maxSize = isVideo ? MAX_REFERENCE_VIDEO_SIZE : MAX_REFERENCE_IMAGE_SIZE; + if (file.size > maxSize) { + alert(isVideo ? "Video too large. Maximum size is 200MB." : "Image too large. Maximum size is 10MB."); + return; + } + + const reader = new FileReader(); + reader.onload = (event) => { + const base64 = event.target?.result as string; + updateNodeData(id, { + inputImages: [base64], + inputImageRefs: undefined, + }); + }; + reader.readAsDataURL(file); + }, + [id, updateNodeData] + ); + + const handleClearInputReference = useCallback(() => { + updateNodeData(id, { + inputImages: [], + inputImageRefs: undefined, + }); + }, [id, updateNodeData]); + // Handle provider change const handleProviderChange = useCallback( (e: React.ChangeEvent) => { @@ -351,6 +560,124 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps 1; + const inputReferencePreview = (nodeData.inputImages || []).find(Boolean) || null; + const inputReferenceIsVideo = isVideoReference(inputReferencePreview); + const inputReferenceVideoUrl = useVideoBlobUrl(inputReferenceIsVideo ? inputReferencePreview : null); + const availableVideoModels = useMemo(() => { + const modelsById = new Map(); + + BUILT_IN_VIDEO_MODELS.forEach((model) => { + modelsById.set(model.id, model); + }); + + externalModels.forEach((model) => { + modelsById.set(model.id, model); + }); + + if (nodeData.selectedModel?.modelId && !modelsById.has(nodeData.selectedModel.modelId)) { + modelsById.set(nodeData.selectedModel.modelId, { + id: nodeData.selectedModel.modelId, + name: nodeData.selectedModel.displayName || nodeData.selectedModel.modelId, + description: null, + provider: nodeData.selectedModel.provider, + capabilities: (nodeData.selectedModel.capabilities as ModelCapability[] | undefined) || ["text-to-video"], + pricing: nodeData.selectedModel.pricing + ? { + type: nodeData.selectedModel.pricing.type, + amount: nodeData.selectedModel.pricing.amount, + currency: "USD", + } + : undefined, + }); + } + + return Array.from(modelsById.values()); + }, [externalModels, nodeData.selectedModel]); + const libtvVideoModelValue = nodeData.selectedModel?.modelId || ""; + const libtvVideoModelLabel = nodeData.selectedModel?.displayName || "Seedance 2.0 VIP"; + const libtvVideoModelOptions = useMemo(() => { + return availableVideoModels.map((model) => ({ + value: model.id, + label: model.name, + })); + }, [availableVideoModels]); + const videoAspectRatio = String( + nodeData.parameters?.aspectRatio ?? + nodeData.parameters?.aspect_ratio ?? + nodeData.parameters?.ratio ?? + "16:9" + ); + const rawVideoResolution = String( + nodeData.parameters?.resolution ?? + nodeData.parameters?.quality ?? + nodeData.parameters?.mode ?? + "720p" + ); + const videoResolution = formatVideoResolution(rawVideoResolution); + const rawVideoDuration = nodeData.parameters?.durationSeconds ?? nodeData.parameters?.duration ?? nodeData.parameters?.duration_seconds ?? 5; + const videoDuration = `${String(rawVideoDuration).replace(/s$/i, "")}s`; + const libtvVideoMetaValue = `${videoAspectRatio}|${rawVideoResolution.toLowerCase()}|${String(rawVideoDuration).replace(/s$/i, "")}`; + const libtvVideoMetaOptions = useMemo(() => { + const config = getVideoSettingsConfig(nodeData.selectedModel?.modelId); + const resolutions = config.resolutions.length ? config.resolutions : [""]; + + return config.aspectRatios.flatMap((aspectRatio) => + resolutions.flatMap((resolution) => + config.durations.map((duration) => ({ + value: `${aspectRatio}|${resolution}|${duration}`, + label: [ + aspectRatio, + resolution ? formatVideoResolution(resolution) : null, + `${duration}s`, + ].filter(Boolean).join(" 路 "), + })) + ) + ); + }, [nodeData.selectedModel?.modelId]); + const libtvVideoMetaLabel = `${videoAspectRatio} · ${videoResolution} · ${videoDuration}`; + + const handleLibtvVideoModelChange = useCallback((value: string) => { + const model = availableVideoModels.find((item) => item.id === value); + if (!model) return; + + updateNodeData(id, { + selectedModel: { + provider: model.provider || currentProvider, + modelId: model.id, + displayName: model.name, + capabilities: model.capabilities, + pricing: model.pricing + ? { type: model.pricing.type, amount: model.pricing.amount } + : undefined, + }, + parameters: {}, + inputSchema: buildVeoInputSchema(model.id), + }); + }, [availableVideoModels, currentProvider, id, updateNodeData]); + + const handleLibtvVideoMetaChange = useCallback((value: string) => { + const [aspectRatio, resolution, duration] = value.split("|"); + const parameterKeys = getVideoParameterKeys(nodeData.selectedModel?.modelId); + const parameters = { ...(nodeData.parameters || {}) }; + + if (parameterKeys.aspect && aspectRatio) { + parameters[parameterKeys.aspect] = aspectRatio; + } + + if (parameterKeys.resolution) { + if (resolution) { + parameters[parameterKeys.resolution] = resolution; + } else { + delete parameters[parameterKeys.resolution]; + } + } + + if (parameterKeys.duration && duration) { + parameters[parameterKeys.duration] = coerceVideoDuration(nodeData.selectedModel?.modelId, parameterKeys.duration, duration); + } + + updateNodeData(id, { parameters }); + }, [id, nodeData.parameters, nodeData.selectedModel?.modelId, updateNodeData]); // Track previous status to detect error transitions const prevStatusRef = useRef(nodeData.status); @@ -420,43 +747,76 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps - {/* Tab bar for primary/fallback settings */} - {nodeData.fallbackModel && ( - - )} - - {/* Primary tab: external provider parameters */} - {settingsTab === "primary" && nodeData.selectedModel?.modelId && ( - - )} - - {/* Fallback tab: fallback model parameters */} - {settingsTab === "fallback" && nodeData.fallbackModel && ( - updateNodeData(id, { fallbackParameters: p })} - /> - )} + ) : undefined} > + + + {selected && ( + + )} + {/* Dynamic input handles based on model schema */} {nodeData.inputSchema && nodeData.inputSchema.length > 0 ? ( // Render handles from schema, sorted by type (images first, text second) @@ -800,8 +1160,76 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps ) : ( -
- {nodeData.status === "loading" ? ( +
+ {inputReferencePreview ? ( + <> + {inputReferenceIsVideo ? ( +
)} diff --git a/src/components/nodes/GenerationPromptInput.tsx b/src/components/nodes/GenerationPromptInput.tsx index 2a8ea452..66f2b8c6 100644 --- a/src/components/nodes/GenerationPromptInput.tsx +++ b/src/components/nodes/GenerationPromptInput.tsx @@ -12,7 +12,7 @@ interface GenerationPromptInputProps { modelLabel?: string; providerLabel?: string; placeholder?: string; - variant?: "default" | "libtvImage"; + variant?: "default" | "libtvImage" | "libtvVideo" | "libtvText"; imageMetaLabel?: string; imageCountLabel?: string; costLabel?: string; @@ -36,6 +36,8 @@ const LABELS = { marker: "\u6807\u8bb0", focus: "\u805a\u7126", camera: "\u6444\u50cf\u673a", + motion: "\u8fd0\u955c", + roleLibrary: "\u89d2\u8272\u5e93", imageCount: "1\u5f20", run: "\u53d1\u9001", expand: "\u653e\u5927", @@ -47,6 +49,12 @@ const LABELS = { export const LIBTV_IMAGE_PROMPT_PLACEHOLDER = "\u63cf\u8ff0\u4f60\u60f3\u8981\u751f\u6210\u7684\u753b\u9762\u5185\u5bb9\uff0c\u6309/\u547c\u51fa\u6307\u4ee4\uff0c@\u5f15\u7528\u7d20\u6750"; +export const LIBTV_VIDEO_PROMPT_PLACEHOLDER = + "\u63cf\u8ff0\u4f60\u60f3\u8981\u751f\u6210\u7684\u89c6\u9891\u5185\u5bb9\uff0c@\u5f15\u7528\u7d20\u6750"; + +export const LIBTV_TEXT_PROMPT_PLACEHOLDER = + "\u5199\u4e0b\u4f60\u60f3\u8bb2\u7684\u6545\u4e8b\u3001\u573a\u666f\u6216\u89d2\u8272\u8bbe\u5b9a\u3002\u4f8b\u5982\uff1a\u4e00\u4e2a\u6765\u81ea\u672a\u6765\u7684\u673a\u5668\u4eba\uff0c\u5728\u57ce\u5e02\u5c4b\u9876\u770b\u661f\u661f\u3002"; + export interface GenerationPromptOption { value: string; label: string; @@ -78,7 +86,7 @@ function RunIcon({ isRunning }: { isRunning: boolean }) { ); } -function FeatureIcon({ icon }: { icon: "cube" | "target" | "focus" }) { +function FeatureIcon({ icon }: { icon: "cube" | "target" | "focus" | "camera" | "shield" }) { if (icon === "cube") { return (
{open && hasOptions && (
@@ -303,6 +330,72 @@ export function GenerationPromptInput({
); + const renderVideoTabs = () => ( +
+ {[ + { label: "\u6587\u751f\u89c6\u9891", active: true }, + { label: "\u5168\u80fd\u53c2\u8003" }, + { label: "\u56fe\u751f\u89c6\u9891" }, + { label: "\u9996\u5c3e\u5e27" }, + { label: "\u56fe\u7247\u53c2\u8003" }, + ].map((tab) => ( + + ))} +
+ ); + + const renderVideoActions = (inDialog = false) => ( +
+
+ {[ + { label: LABELS.marker, icon: "target" as const }, + { label: LABELS.motion, icon: "camera" as const }, + { label: LABELS.roleLibrary, icon: "shield" as const }, + ].map((item) => ( + + ))} +
+ + +
+ ); + const renderToolbar = () => (
@@ -378,6 +471,126 @@ export function GenerationPromptInput({
); + const renderVideoToolbar = () => ( +
+
+ notifyUnavailable(modelLabel)} + > + + {modelLabel} + + notifyUnavailable(imageMetaLabel)} + > + + {imageMetaLabel} + + +
+
+ + notifyUnavailable(LABELS.settings)} + showChevron={false} + > + + + + {imageCountLabel} + + + + + {costLabel} + + +
+
+ ); + + const renderTextToolbar = () => ( +
+ notifyUnavailable(modelLabel)} + > + + {modelLabel} + + +
+ + + + {costLabel} + + +
+
+ ); + const renderLibtvTextarea = (expandedMode = false) => (