diff --git a/frontend/package-lock.json b/frontend/package-lock.json index cf45e47ba..f7516238c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -22,7 +22,8 @@ "react": "^19.1.1", "react-dom": "^19.1.1", "sonner": "^2.0.7", - "tailwind-merge": "^3.3.1" + "tailwind-merge": "^3.3.1", + "zustand": "^4.5.2" }, "devDependencies": { "@eslint/js": "^9.33.0", @@ -5143,6 +5144,43 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zustand": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.2.tgz", + "integrity": "sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "1.2.0" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/zustand/node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } } } } diff --git a/frontend/package.json b/frontend/package.json index 1ff5b34cd..ddd984206 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,7 +28,8 @@ "react": "^19.1.1", "react-dom": "^19.1.1", "sonner": "^2.0.7", - "tailwind-merge": "^3.3.1" + "tailwind-merge": "^3.3.1", + "zustand": "^4.5.2" }, "devDependencies": { "@eslint/js": "^9.33.0", diff --git a/frontend/src/components/InputAndControlsPanel.tsx b/frontend/src/components/InputAndControlsPanel.tsx index ac28dc21f..710bc7144 100644 --- a/frontend/src/components/InputAndControlsPanel.tsx +++ b/frontend/src/components/InputAndControlsPanel.tsx @@ -49,6 +49,7 @@ interface InputAndControlsPanelProps { timelinePrompts?: TimelinePrompt[]; transitionSteps: number; onTransitionStepsChange: (steps: number) => void; + cloudMode?: boolean; } export function InputAndControlsPanel({ @@ -84,6 +85,7 @@ export function InputAndControlsPanel({ timelinePrompts: _timelinePrompts = [], transitionSteps, onTransitionStepsChange, + cloudMode = false, }: InputAndControlsPanelProps) { // Helper function to determine if playhead is at the end of timeline const isAtEndOfTimeline = () => { @@ -271,6 +273,7 @@ export function InputAndControlsPanel({ transitionSteps={transitionSteps} onTransitionStepsChange={onTransitionStepsChange} timelinePrompts={_timelinePrompts} + cloudMode={cloudMode} /> )} diff --git a/frontend/src/components/PromptInput.tsx b/frontend/src/components/PromptInput.tsx index 7d1ffc23b..6e59f890b 100644 --- a/frontend/src/components/PromptInput.tsx +++ b/frontend/src/components/PromptInput.tsx @@ -32,6 +32,7 @@ interface PromptInputProps { transitionSteps?: number; onTransitionStepsChange?: (steps: number) => void; timelinePrompts?: TimelinePrompt[]; + cloudMode?: boolean; } export function PromptInput({ @@ -51,6 +52,7 @@ export function PromptInput({ transitionSteps = 4, onTransitionStepsChange, timelinePrompts = [], + cloudMode = false, }: PromptInputProps) { const [isProcessing, setIsProcessing] = useState(false); const [focusedIndex, setFocusedIndex] = useState(null); @@ -160,7 +162,8 @@ export function PromptInput({ /> -
+
+ {!cloudMode && ( onTransitionStepsChange?.(steps)} @@ -171,42 +174,43 @@ export function PromptInput({ disabled={disabled || !isStreaming || timelinePrompts.length === 0} className="space-y-2" /> + )} - {/* Add/Submit buttons - Bottom row */} -
- {managedPrompts.length < 4 && ( - - )} + {/* Add/Submit buttons - Bottom row */} +
+ {managedPrompts.length < 4 && ( -
+ )} +
- ); +
+ ); } // Multiple prompts mode: show weights and controls @@ -241,43 +245,47 @@ export function PromptInput({ })}
- {/* Spatial Blend - only for multiple prompts */} - {managedPrompts.length >= 2 && ( -
- - Spatial Blend: - - + onInterpolationMethodChange?.(value as "linear" | "slerp") + } + disabled={disabled} + > + + + + + Linear + 2}> + Slerp + + + +
+ )} + + onTransitionStepsChange?.(steps)} + temporalInterpolationMethod={temporalInterpolationMethod} + onTemporalInterpolationMethodChange={method => + onTemporalInterpolationMethodChange?.(method) } - disabled={disabled} - > - - - - - Linear - 2}> - Slerp - - - -
+ disabled={disabled || !isStreaming || timelinePrompts.length === 0} + className="space-y-2" + /> + )} - onTransitionStepsChange?.(steps)} - temporalInterpolationMethod={temporalInterpolationMethod} - onTemporalInterpolationMethodChange={method => - onTemporalInterpolationMethodChange?.(method) - } - disabled={disabled || !isStreaming || timelinePrompts.length === 0} - className="space-y-2" - /> - {/* Add/Submit buttons - Bottom row */}
{managedPrompts.length < 4 && ( diff --git a/frontend/src/components/PromptInputWithTimeline.tsx b/frontend/src/components/PromptInputWithTimeline.tsx index f6d3cf7fd..d4a52991a 100644 --- a/frontend/src/components/PromptInputWithTimeline.tsx +++ b/frontend/src/components/PromptInputWithTimeline.tsx @@ -255,7 +255,7 @@ export function PromptInputWithTimeline({ } return false; } - return isStreaming; // Already streaming + return true; // Already streaming }, [isStreaming, onStartStream]); // Check if at end of timeline @@ -289,8 +289,10 @@ export function PromptInputWithTimeline({ setIsLive(true); onLiveStateChange?.(true); + console.log("[PromptInputWithTimeline] handleStartPlayback", prompts.length); // Only create a new live prompt if there are no prompts at all in the timeline if (prompts.length === 0) { + console.log("[PromptInputWithTimeline] Creating new live prompt ", prompts); const streamStartedAgain = await initializeStream(); if (streamStartedAgain) { const livePrompt = buildLivePromptFromCurrent( diff --git a/frontend/src/components/SettingsPanel.tsx b/frontend/src/components/SettingsPanel.tsx index 4d4d6b085..0df1c5eb3 100644 --- a/frontend/src/components/SettingsPanel.tsx +++ b/frontend/src/components/SettingsPanel.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useEffect, useMemo } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"; import { Select, @@ -36,6 +36,8 @@ interface SettingsPanelProps { onPipelineIdChange?: (pipelineId: PipelineId) => void; isStreaming?: boolean; isDownloading?: boolean; + cloudMode?: boolean; + onCloudModeChange?: (enabled: boolean) => void; resolution?: { height: number; width: number; @@ -68,6 +70,8 @@ export function SettingsPanel({ onPipelineIdChange, isStreaming = false, isDownloading = false, + cloudMode = false, + onCloudModeChange, resolution, onResolutionChange, seed = 42, @@ -105,6 +109,27 @@ export function SettingsPanel({ const [widthError, setWidthError] = useState(null); const [seedError, setSeedError] = useState(null); + // Get filtered pipeline IDs based on cloudMode + const filteredPipelineIds = useMemo(() => { + return Object.keys(PIPELINES).filter(id => { + const pipeline = PIPELINES[id]; + const compatibility = pipeline.pipelineCompatibility || "local"; + + if (cloudMode) { + return compatibility === "cloud" || compatibility === "both"; + } else { + return compatibility === "local" || compatibility === "both"; + } + }); + }, [cloudMode]); + + // Auto-select first available pipeline if current selection is no longer available + useEffect(() => { + if (filteredPipelineIds.length > 0 && !filteredPipelineIds.includes(pipelineId)) { + onPipelineIdChange?.(filteredPipelineIds[0] as PipelineId); + } + }, [cloudMode, filteredPipelineIds, pipelineId, onPipelineIdChange]); + const handlePipelineIdChange = (value: string) => { if (value in PIPELINES) { onPipelineIdChange?.(value as PipelineId); @@ -136,6 +161,13 @@ export function SettingsPanel({ } else { setWidthError(`Must be at most ${maxValue}`); } + } else if (cloudMode && value % 64 !== 0) { + // In cloud mode, dimension must be a multiple of 64 + if (dimension === "height") { + setHeightError(`Must be a multiple of 64 in cloud mode`); + } else { + setWidthError(`Must be a multiple of 64 in cloud mode`); + } } else { // Clear error if valid if (dimension === "height") { @@ -154,7 +186,18 @@ export function SettingsPanel({ const incrementResolution = (dimension: "height" | "width") => { const maxValue = 2048; - const newValue = Math.min(maxValue, effectiveResolution[dimension] + 1); + const currentValue = effectiveResolution[dimension]; + + let newValue: number; + if (cloudMode && currentValue % 64 !== 0) { + // Snap to next multiple of 64 + newValue = Math.ceil(currentValue / 64) * 64; + } else { + const step = cloudMode ? 64 : 1; + newValue = currentValue + step; + } + + newValue = Math.min(maxValue, newValue); handleResolutionChange(dimension, newValue); }; @@ -165,7 +208,18 @@ export function SettingsPanel({ pipelineId === "krea-realtime-video" ? MIN_DIMENSION : 1; - const newValue = Math.max(minValue, effectiveResolution[dimension] - 1); + const currentValue = effectiveResolution[dimension]; + + let newValue: number; + if (cloudMode && currentValue % 64 !== 0) { + // Snap to previous multiple of 64 + newValue = Math.floor(currentValue / 64) * 64; + } else { + const step = cloudMode ? 64 : 1; + newValue = currentValue - step; + } + + newValue = Math.max(minValue, newValue); handleResolutionChange(dimension, newValue); }; @@ -206,6 +260,25 @@ export function SettingsPanel({ Settings + {(import.meta as any).env?.VITE_DAYDREAM_API_KEY && ( +
+

Cloud mode

+
+ Use Daydream (WebRTC) + {})} + variant="outline" + size="sm" + className="h-7" + disabled={isStreaming || isDownloading} + > + {cloudMode ? "ON" : "OFF"} + +
+
+ )} +

Pipeline ID