yue2-studio / src /components /queue.tsx
RalphThings's picture
Upload folder using huggingface_hub
76528e0 verified
Raw History Blame Contribute Delete
5.61 kB
"use client";
import { AnimatePresence, motion } from "motion/react";
import { DownloadSimple, Play, Prohibit, Warning } from "@phosphor-icons/react";
import { Panel } from "@/components/ui";
import { downloadName } from "@/lib/download";
import { explainJobError } from "@/lib/errors";
import { progressPercent } from "@/lib/progress";
import { STAGE_LABEL, type Job } from "@/lib/types";
export function Queue({
jobs,
selectedId,
onSelect,
onCancel,
}: {
jobs: Job[];
selectedId: string | null;
onSelect: (job: Job) => void;
onCancel: (job: Job) => void;
}) {
const running = jobs.filter((job) => !TERMINAL.has(job.stage)).length;
return (
<Panel
title="Takes"
hint={running > 0 ? `${running} on the desk` : undefined}
className="overflow-hidden"
>
{jobs.length === 0 ? (
<div className="rounded-console border border-dashed border-line-strong px-5 py-8 text-center">
<p className="text-[13px] text-ink">Nothing rendered yet</p>
<p className="mx-auto mt-1 max-w-[34ch] text-[12px] leading-snug text-ink-faint">
Write a style and some lyrics, then hit render. Every take stays here with its seed and
its score so you can go back to it.
</p>
</div>
) : (
<ul className="flex flex-col gap-1.5">
<AnimatePresence initial={false}>
{jobs.map((job) => (
<motion.li
key={job.id}
layout
initial={{ opacity: 0, y: -6 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.18 }}
>
<Row
job={job}
selected={job.id === selectedId}
onSelect={() => onSelect(job)}
onCancel={() => onCancel(job)}
/>
</motion.li>
))}
</AnimatePresence>
</ul>
)}
</Panel>
);
}
const TERMINAL = new Set(["done", "failed", "cancelled"]);
function Row({
job,
selected,
onSelect,
onCancel,
}: {
job: Job;
selected: boolean;
onSelect: () => void;
onCancel: () => void;
}) {
const active = !TERMINAL.has(job.stage);
const failed = job.stage === "failed";
const percent = progressPercent(job);
// Keep the raw text on the title attribute: the plain version is for acting
// on, the original is for diagnosing.
const explained = failed ? explainJobError(job.error) : null;
return (
<div
className={`group relative overflow-hidden rounded-console border transition-colors duration-150 ${
selected ? "border-citron/50 bg-citron/5" : "border-line bg-raised hover:border-line-strong"
}`}
>
<button
type="button"
onClick={onSelect}
disabled={job.stage !== "done"}
className="flex w-full items-center gap-3 px-3 py-2.5 text-left disabled:cursor-default"
>
<span
className={`flex h-7 w-7 shrink-0 items-center justify-center rounded-console ${
failed ? "bg-alarm/15 text-alarm" : selected ? "bg-citron text-void" : "bg-void text-ink-faint"
}`}
>
{failed ? <Warning size={13} weight="fill" /> : <Play size={12} weight="fill" />}
</span>
<span className="min-w-0 flex-1">
<span className="block truncate text-[13px] text-ink">{job.title}</span>
<span
className={`block truncate text-[11.5px] ${failed ? "text-alarm" : "text-ink-faint"}`}
title={failed ? (job.error ?? undefined) : undefined}
>
{failed ? (explained ?? job.error) : STAGE_LABEL[job.stage]}
{active && percent !== null ? ` · ${percent}%` : ""}
{active && percent === null && job.queuePosition
? ` · ${job.queuePosition} ahead`
: ""}
</span>
</span>
<span className="shrink-0 font-mono text-[11px] text-ink-faint">
{job.seedUsed.toString(36).slice(0, 6)}
</span>
</button>
<div className="absolute top-2 right-2 flex gap-1 opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100">
{job.stage === "done" && job.audio ? (
<a
href={job.audio.url}
download={downloadName(job)}
onClick={(event) => event.stopPropagation()}
className="flex h-6 w-6 items-center justify-center rounded-console bg-void text-ink-dim transition-colors hover:text-citron"
aria-label="Download this take"
>
<DownloadSimple size={12} weight="bold" />
</a>
) : null}
{active ? (
<button
type="button"
onClick={onCancel}
className="flex h-6 w-6 items-center justify-center rounded-console bg-void text-ink-dim transition-colors hover:text-alarm"
aria-label="Stop this take"
>
<Prohibit size={12} weight="bold" />
</button>
) : null}
</div>
{active ? (
percent === null ? (
// No step reporting, so show motion rather than a number we guessed.
<div className="h-[2px] overflow-hidden bg-line">
<div className="h-full w-1/3 animate-pulse bg-citron" />
</div>
) : (
<div
className="h-[2px] bg-citron transition-[width] duration-300 ease-out"
style={{ width: `${Math.max(3, percent)}%` }}
/>
)
) : null}
</div>
);
}