import { useRef, useState, type CSSProperties } from 'react'; import { FileText, Image as ImageIcon, LoaderCircle, Music2, Paperclip, Plus, Repeat2, Video, X, } from 'lucide-react'; import { uploadFileToStorage } from '../api'; import type { PlaygroundMode } from '../types'; export type PlaygroundUploadKind = 'audio' | 'file' | 'image' | 'video'; export type PlaygroundUploadRole = 'first_frame' | 'last_frame'; export type PlaygroundVideoCreateMode = 'text_to_video' | 'first_last_frame' | 'omni_reference'; export interface PlaygroundUpload { contentType: string; id: string; kind: PlaygroundUploadKind; name: string; raw: Record; role?: PlaygroundUploadRole; size: number; url: string; } export type OpenAIChatContentPart = | { type: 'text'; text: string } | { type: 'image_url'; image_url: { url: string } } | { type: 'video_url'; video_url: { url: string } } | { type: 'audio_url'; audio_url: { url: string } } | { type: 'file_url'; file_url: { filename: string; url: string } }; export const mediaUploadAccept = 'image/*,video/*,audio/*'; export const imageOnlyUploadAccept = 'image/*'; export const chatUploadAccept = [ mediaUploadAccept, '.csv', '.doc', '.docx', '.json', '.jsonl', '.md', '.markdown', '.pdf', '.ppt', '.pptx', '.txt', '.xls', '.xlsx', '.yaml', '.yml', 'application/json', 'application/msword', 'application/pdf', 'application/vnd.ms-excel', 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'text/*', ].join(','); export function ComposerUploadButton(props: { accept: string; active?: boolean; disabled?: boolean; uploading?: boolean; onFiles?: (files: File[]) => void; }) { const inputRef = useRef(null); const disabled = props.disabled || props.uploading; return ( <> { const files = Array.from(event.currentTarget.files ?? []); event.currentTarget.value = ''; props.onFiles?.(files); }} /> ); } export function PlaygroundReferencePicker(props: { accept: string; disabled?: boolean; mode: PlaygroundMode; uploadLabel?: string; uploads: PlaygroundUpload[]; uploading?: boolean; videoMode?: PlaygroundVideoCreateMode; onFiles?: (files: File[], targetRole?: PlaygroundUploadRole) => void; onRemove?: (id: string) => void; onSwapFrames?: () => void; }) { if (props.mode === 'video' && props.videoMode === 'first_last_frame') { return ( ); } return ( ); } function StackedReferencePicker(props: { accept: string; disabled?: boolean; uploadLabel: string; uploads: PlaygroundUpload[]; uploading?: boolean; onFiles?: (files: File[]) => void; onRemove?: (id: string) => void; }) { const inputRef = useRef(null); const [hoveredId, setHoveredId] = useState(''); const [expanded, setExpanded] = useState(false); const hoveredUpload = props.uploads.find((item) => item.id === hoveredId); const disabled = props.disabled || props.uploading || !props.onFiles; const uploadCardIndex = props.uploads.length; return (
{ setExpanded(false); setHoveredId(''); }} > {hoveredUpload &&
{hoveredUpload.name}
}
{props.uploads.map((item, index) => (
{ setExpanded(true); setHoveredId(item.id); }} > {item.kind !== 'image' && {uploadKindLabel(item.kind)}} {props.onRemove && hoveredId === item.id && ( )}
))} {props.uploads.length > 0 && ( )}
{ const files = Array.from(event.currentTarget.files ?? []); event.currentTarget.value = ''; props.onFiles?.(files); }} />
); } const referenceTiltValues = [-7, 6, -3, 8, -5, 4, -8, 5]; const referenceXValues = [0, -5, 6, -3, 4, -6, 3, -4]; const referenceYValues = [0, 3, -1, 4, 1, 5, 2, -2]; function referenceCardStyle(index: number) { const valueIndex = index % referenceTiltValues.length; return { '--reference-index': index, '--reference-tilt': `${referenceTiltValues[valueIndex]}deg`, '--reference-x': `${referenceXValues[valueIndex]}px`, '--reference-y': `${referenceYValues[valueIndex]}px`, } as CSSProperties; } function FirstLastFramePicker(props: { accept: string; disabled?: boolean; uploads: PlaygroundUpload[]; uploading?: boolean; onFiles?: (files: File[], targetRole?: PlaygroundUploadRole) => void; onRemove?: (id: string) => void; onSwapFrames?: () => void; }) { const first = frameUploadByRole(props.uploads, 'first_frame'); const last = frameUploadByRole(props.uploads, 'last_frame'); const canSwap = Boolean(first && last); return (
); } function FrameSlot(props: { accept: string; disabled?: boolean; item?: PlaygroundUpload; label: string; role: PlaygroundUploadRole; uploading?: boolean; onFiles?: (files: File[], targetRole?: PlaygroundUploadRole) => void; onRemove?: (id: string) => void; }) { const inputRef = useRef(null); const disabled = props.disabled || props.uploading || !props.onFiles; return (
{props.item && props.onRemove && ( )} { const files = Array.from(event.currentTarget.files ?? []); event.currentTarget.value = ''; props.onFiles?.(files, props.role); }} />
); } function ReferencePreview(props: { item: PlaygroundUpload }) { if (props.item.kind === 'image') { return ; } if (props.item.kind === 'video') { return