Spaces:
Runtime error
Runtime error
File size: 5,611 Bytes
bf68acd 76528e0 bf68acd 76528e0 bf68acd 76528e0 bf68acd | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | "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>
);
}
|