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>
  );
}