import { useLingui } from "@lingui/react/macro"; import { MEDIA_JOB_ACTIVE_STATUSES, MEDIA_PRESETS, type MediaJobBlock, type MediaJobStatus, type MediaOperation, type MediaOutput, type MediaPresetId, MediaPresetIdSchema, } from "@rakazo/contracts"; import { Button, buttonVariants, cn, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@rakazo/ui-web"; import { Captions, ChevronDown, Download, Film, ImageIcon, type LucideIcon, Scissors, Sparkles, } from "lucide-react"; import { useId, useState } from "react"; import type { ArtifactTarget } from "../../lib/artifact-open"; import { artifactUrl } from "../../lib/artifact-url"; import { ArtifactImage } from "../../pages/shell/message-cards"; import { ArtifactFileCard } from "../ArtifactFileCard"; import { Shimmer } from "../ai/primitives"; import { MediaPlayer } from "./MediaPlayer"; const CARD_CLASS = "w-[min(480px,96%)] rounded-[18px] border border-border bg-card/75 p-3.5 text-start shadow-xs"; export type MediaCardAction = | { kind: "export"; artifactId: string; preset: MediaPresetId } | { kind: "transcribe"; artifactId: string } | { kind: "cancel" }; /** Which follow-up jobs the space can run; actions it cannot run are not offered. */ export interface MediaCardAvailability { export: boolean; transcribe: boolean; } const OPERATION_ICONS: Record = { generate_image: Sparkles, generate_video: Sparkles, edit_image: ImageIcon, edit_video: Scissors, export_video: Film, transcribe: Captions, }; export function isActiveMediaStatus(status: MediaJobStatus): boolean { return (MEDIA_JOB_ACTIVE_STATUSES as readonly MediaJobStatus[]).includes(status); } /** Formats a video can be exported to: every preset except the shape it already has. */ export function exportPresetsFor(output: Pick) { const ratio = output.width && output.height ? output.width / output.height : null; return MediaPresetIdSchema.options.filter((id) => { if (id === output.preset) return false; const preset = MEDIA_PRESETS[id]; return ratio === null || Math.abs(preset.width / preset.height - ratio) > 0.01; }); } function StatusLabel({ status }: { status: MediaJobStatus }) { const { t } = useLingui(); if (status === "running") { return ( {t`Working…`} ); } const label = status === "queued" ? t`Queued` : status === "waiting" ? t`Waiting for GPU` : status === "done" ? t`Done` : status === "failed" ? t`Failed` : t`Cancelled`; return ( {label} ); } function OutputActions({ output, available, busy, onAction, }: { output: MediaOutput; available: MediaCardAvailability; busy: boolean; onAction?: (action: MediaCardAction) => void; }) { const { t } = useLingui(); const name = output.name; const presets = output.kind === "video" ? exportPresetsFor(output) : []; return (
{onAction && available.export && presets.length > 0 ? ( } > {t`Export`} {presets.map((preset) => ( onAction({ kind: "export", artifactId: output.artifactId, preset })} > {MEDIA_PRESETS[preset].aspect} ))} ) : null} {onAction && available.transcribe && (output.kind === "video" || output.kind === "audio") ? ( ) : null}
); } /** * The live card of a media job: what it makes, how far it is, and the results with the * follow-ups that fit them (export other formats, transcribe). Nothing else is shown. */ export function MediaCardView({ block, target, available = { export: false, transcribe: false }, onAction, }: { block: MediaJobBlock; target: ArtifactTarget; available?: MediaCardAvailability; /** Starts a follow-up job or cancels this one; rejects with a message to show. */ onAction?: (action: MediaCardAction) => Promise; }) { const { t } = useLingui(); const titleId = useId(); const [busy, setBusy] = useState(false); const [actionError, setActionError] = useState(null); const active = isActiveMediaStatus(block.status); const Icon = OPERATION_ICONS[block.operation]; const percent = Math.round((block.progress ?? 0) * 100); const images = block.outputs.filter((output) => output.kind === "image"); const others = block.outputs.filter((output) => output.kind !== "image"); const act = onAction ? (action: MediaCardAction) => { setBusy(true); setActionError(null); void onAction(action) .catch((error: unknown) => setActionError(error instanceof Error && error.message ? error.message : t`Failed`), ) .finally(() => setBusy(false)); } : undefined; return (
{active && act ? ( ) : null}
{active ? (
) : null} {block.status === "failed" && block.error ? (

{block.error}

) : null} {images.length > 0 ? (
{images.map((output) => ( ))}
) : null} {others.map((output) => (
{output.kind === "video" || output.kind === "audio" ? ( <> {active ? null : ( )} ) : ( )}
))} {actionError ? (

{actionError}

) : null}
); } /** The media card in a conversation, with its follow-up actions wired to the media job RPCs. */ export function MediaCard({ block, target }: { block: MediaJobBlock; target: ArtifactTarget }) { return ; }