Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions frontend/src/components/DownloadDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import {
DialogTitle,
} from "./ui/dialog";
import { Progress } from "./ui/progress";
import { PIPELINES } from "../data/pipelines";
import type { PipelineId, DownloadProgress } from "../types";
import type { PipelineInfo } from "../hooks/usePipelines";

interface DownloadDialogProps {
open: boolean;
pipelines: Record<string, PipelineInfo> | null;
pipelineId: PipelineId;
onClose: () => void;
onDownload: () => void;
Expand All @@ -23,13 +24,14 @@ interface DownloadDialogProps {

export function DownloadDialog({
open,
pipelines,
pipelineId,
onClose,
onDownload,
isDownloading = false,
progress = null,
}: DownloadDialogProps) {
const pipelineInfo = PIPELINES[pipelineId];
const pipelineInfo = pipelines?.[pipelineId];
if (!pipelineInfo) return null;

return (
Expand Down
248 changes: 184 additions & 64 deletions frontend/src/components/InputAndControlsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,32 @@ import { Badge } from "./ui/badge";
import { Input } from "./ui/input";
import { Upload, ArrowUp } from "lucide-react";
import { LabelWithTooltip } from "./ui/label-with-tooltip";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "./ui/tooltip";
import type { VideoSourceMode } from "../hooks/useVideoSource";
import type { PromptItem, PromptTransition } from "../lib/api";
import type { InputMode } from "../types";
import { pipelineIsMultiMode } from "../data/pipelines";
import type { PipelineInfo } from "../hooks/usePipelines";
import {
pipelineRequiresReferenceImage,
pipelineShowsPromptInput,
pipelineCanChangeReferenceWhileStreaming,
getPipelineReferenceImageDescription,
} from "../data/pipelines";
import { PromptInput } from "./PromptInput";
import { TimelinePromptEditor } from "./TimelinePromptEditor";
import type { TimelinePrompt } from "./PromptTimeline";
import { ImageManager } from "./ImageManager";
import { Button } from "./ui/button";
import { ImageIcon } from "lucide-react";

interface InputAndControlsPanelProps {
className?: string;
pipelines: Record<string, PipelineInfo> | null;
localStream: MediaStream | null;
isInitializing: boolean;
error: string | null;
Expand Down Expand Up @@ -63,16 +77,21 @@ interface InputAndControlsPanelProps {
onInputModeChange: (mode: InputMode) => void;
// Whether Spout is available (server-side detection for native Windows, not WSL)
spoutAvailable?: boolean;
// PersonaLive reference image
referenceImageUrl?: string | null;
onReferenceImageUpload?: (file: File) => void;
isUploadingReference?: boolean;
// VACE reference images (only shown when VACE is enabled)
vaceEnabled?: boolean;
refImages?: string[];
onRefImagesChange?: (images: string[]) => void;
onSendHints?: (imagePaths: string[]) => void;
isDownloading?: boolean;
isLoading?: boolean;
}

export function InputAndControlsPanel({
className = "",
pipelines,
localStream,
isInitializing,
error,
Expand Down Expand Up @@ -109,11 +128,14 @@ export function InputAndControlsPanel({
inputMode,
onInputModeChange,
spoutAvailable = false,
referenceImageUrl = null,
onReferenceImageUpload,
isUploadingReference = false,
vaceEnabled = true,
refImages = [],
onRefImagesChange,
onSendHints,
isDownloading = false,
isLoading = false,
}: InputAndControlsPanelProps) {
// Helper function to determine if playhead is at the end of timeline
const isAtEndOfTimeline = () => {
Expand All @@ -128,7 +150,22 @@ export function InputAndControlsPanel({
const videoRef = useRef<HTMLVideoElement>(null);

// Check if this pipeline supports multiple input modes
const isMultiMode = pipelineIsMultiMode(pipelineId);
const pipeline = pipelines?.[pipelineId];
const isMultiMode = (pipeline?.supportedModes?.length ?? 0) > 1;

// Check if this pipeline requires a reference image (PersonaLive)
const needsReferenceImage = pipelineRequiresReferenceImage(pipelineId);

const handleReferenceImageUpload = (
event: React.ChangeEvent<HTMLInputElement>
) => {
const file = event.target.files?.[0];
if (file && onReferenceImageUpload) {
onReferenceImageUpload(file);
}
// Reset the input value so the same file can be selected again
event.target.value = "";
};

useEffect(() => {
if (videoRef.current && localStream) {
Expand Down Expand Up @@ -183,6 +220,80 @@ export function InputAndControlsPanel({
</div>
)}

{/* Reference Image upload - only show for pipelines that require it */}
{needsReferenceImage && (
<div>
<h3 className="text-sm font-medium mb-2">Reference Portrait</h3>
<div className="rounded-lg flex items-center justify-center bg-muted/10 overflow-hidden relative aspect-square max-h-48">
{referenceImageUrl ? (
<img
src={referenceImageUrl}
alt="Reference portrait"
className="w-full h-full object-cover"
/>
) : (
<div className="text-center text-muted-foreground text-sm p-4 flex flex-col items-center gap-2">
<ImageIcon className="h-8 w-8 opacity-50" />
<span>Upload a portrait image</span>
</div>
)}
<input
type="file"
accept="image/*"
onChange={handleReferenceImageUpload}
className="hidden"
id="reference-image-upload"
disabled={
isUploadingReference ||
((isStreaming || isConnecting) &&
!pipelineCanChangeReferenceWhileStreaming(pipelineId))
}
/>
{/* Upload button with tooltip when disabled during streaming */}
{(isStreaming || isConnecting) &&
!pipelineCanChangeReferenceWhileStreaming(pipelineId) ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="absolute bottom-2 right-2 p-2 rounded-full bg-black/50 opacity-50 cursor-not-allowed">
<Upload className="h-4 w-4 text-white" />
</div>
</TooltipTrigger>
<TooltipContent side="left" className="max-w-xs">
<p className="text-xs">
Reference image is processed when the pipeline loads.
Stop the stream to change it.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<label
htmlFor="reference-image-upload"
className={`absolute bottom-2 right-2 p-2 rounded-full bg-black/50 transition-colors ${
isUploadingReference
? "opacity-50 cursor-not-allowed"
: "hover:bg-black/70 cursor-pointer"
}`}
>
<Upload className="h-4 w-4 text-white" />
</label>
)}
</div>
{isUploadingReference && (
<p className="text-xs text-muted-foreground mt-1">
Uploading reference image...
</p>
)}
{!referenceImageUrl && (
<p className="text-xs text-muted-foreground mt-1">
{getPipelineReferenceImageDescription(pipelineId) ||
"This pipeline requires a reference image."}
</p>
)}
</div>
)}

{/* Video Source toggle - only show when in video input mode */}
{inputMode === "video" && (
<div>
Expand Down Expand Up @@ -299,7 +410,7 @@ export function InputAndControlsPanel({
<ImageManager
images={refImages}
onImagesChange={onRefImagesChange || (() => {})}
disabled={isDownloading}
disabled={isLoading}
/>
{onSendHints && refImages && refImages.length > 0 && (
<div className="flex items-center justify-end mt-2">
Expand All @@ -308,7 +419,7 @@ export function InputAndControlsPanel({
e.preventDefault();
onSendHints(refImages.filter(img => img));
}}
disabled={isDownloading || !isStreaming}
disabled={isLoading || !isStreaming}
size="sm"
className="rounded-full w-8 h-8 p-0 bg-black hover:bg-gray-800 text-white disabled:opacity-50 disabled:cursor-not-allowed"
title={
Expand All @@ -324,67 +435,76 @@ export function InputAndControlsPanel({
</div>
)}

<div>
{(() => {
// The Input can have two states: Append (default) and Edit (when a prompt is selected and the video is paused)
const isEditMode = selectedTimelinePrompt && isVideoPaused;
{/* Prompts section - only show for pipelines that support text prompts */}
{pipelineShowsPromptInput(pipelineId) && (
<div>
{(() => {
// The Input can have two states: Append (default) and Edit (when a prompt is selected and the video is paused)
const isEditMode = selectedTimelinePrompt && isVideoPaused;

return (
<div>
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-medium">Prompts</h3>
{isEditMode && (
<Badge variant="secondary" className="text-xs">
Editing
</Badge>
)}
</div>
// Hide prompts section if pipeline doesn't support prompts
if (pipeline?.supportsPrompts === false) {
return null;
}

{selectedTimelinePrompt ? (
<TimelinePromptEditor
prompt={selectedTimelinePrompt}
onPromptUpdate={onTimelinePromptUpdate}
disabled={false}
interpolationMethod={interpolationMethod}
onInterpolationMethodChange={onInterpolationMethodChange}
promptIndex={_timelinePrompts.findIndex(
p => p.id === selectedTimelinePrompt.id
return (
<div>
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-medium">Prompts</h3>
{isEditMode && (
<Badge variant="secondary" className="text-xs">
Editing
</Badge>
)}
/>
) : (
<PromptInput
prompts={prompts}
onPromptsChange={onPromptsChange}
onPromptsSubmit={onPromptsSubmit}
onTransitionSubmit={onTransitionSubmit}
disabled={
pipelineId === "passthrough" ||
(_isTimelinePlaying &&
!isVideoPaused &&
!isAtEndOfTimeline()) ||
// Disable in Append mode when paused and not at end
(!selectedTimelinePrompt &&
isVideoPaused &&
!isAtEndOfTimeline())
}
interpolationMethod={interpolationMethod}
onInterpolationMethodChange={onInterpolationMethodChange}
temporalInterpolationMethod={temporalInterpolationMethod}
onTemporalInterpolationMethodChange={
onTemporalInterpolationMethodChange
}
isLive={isLive}
onLivePromptSubmit={onLivePromptSubmit}
isStreaming={isStreaming}
transitionSteps={transitionSteps}
onTransitionStepsChange={onTransitionStepsChange}
timelinePrompts={_timelinePrompts}
/>
)}
</div>
);
})()}
</div>
</div>

{selectedTimelinePrompt ? (
<TimelinePromptEditor
prompt={selectedTimelinePrompt}
onPromptUpdate={onTimelinePromptUpdate}
disabled={false}
interpolationMethod={interpolationMethod}
onInterpolationMethodChange={onInterpolationMethodChange}
promptIndex={_timelinePrompts.findIndex(
p => p.id === selectedTimelinePrompt.id
)}
/>
) : (
<PromptInput
prompts={prompts}
onPromptsChange={onPromptsChange}
onPromptsSubmit={onPromptsSubmit}
onTransitionSubmit={onTransitionSubmit}
disabled={
pipelineId === "passthrough" ||
pipelineId === "personalive" ||
(_isTimelinePlaying &&
!isVideoPaused &&
!isAtEndOfTimeline()) ||
// Disable in Append mode when paused and not at end
(!selectedTimelinePrompt &&
isVideoPaused &&
!isAtEndOfTimeline())
}
interpolationMethod={interpolationMethod}
onInterpolationMethodChange={onInterpolationMethodChange}
temporalInterpolationMethod={temporalInterpolationMethod}
onTemporalInterpolationMethodChange={
onTemporalInterpolationMethodChange
}
isLive={isLive}
onLivePromptSubmit={onLivePromptSubmit}
isStreaming={isStreaming}
transitionSteps={transitionSteps}
onTransitionStepsChange={onTransitionStepsChange}
timelinePrompts={_timelinePrompts}
/>
)}
</div>
);
})()}
</div>
)}
</CardContent>
</Card>
);
Expand Down
Loading