"use client"; import * as React from "react"; import { CheckCircle2, CircleSlash, Loader2, XCircle } from "lucide-react"; import type { Job } from "@/lib/api"; import { api } from "@/lib/api"; import { Sheet } from "@/components/ui/sheet"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Progress } from "@/components/ui/progress"; import { useToast } from "@/components/toast"; /** * Why a request is where it is. * * Previously a failure was a red word in a list plus a three-second toast, and * the reason was raw text — "transfer Completed, Errored" — joined with a dot * after the album name in 13px dim type. Since the backend now walks a ladder * of candidates instead of giving up on the first peer, there is an actual * story to tell: who was tried, what each one said, and what is left. */ export function JobSheet({ job, onClose, onChanged, }: { job: Job | null; onClose: () => void; onChanged: () => void; }) { const toast = useToast(); const [busy, setBusy] = React.useState(false); const retry = async () => { if (!job) return; setBusy(true); try { await api.retry(job.id); toast("Requeued"); onChanged(); onClose(); } catch (e) { toast((e as Error).message, true); } setBusy(false); }; const conf = job?.confidence; return ( !v && onClose()} title={job ? job.title : ""} description={job ? [job.artist, job.album].filter(Boolean).join(" · ") : undefined} footer={ job?.state === "failed" ? ( ) : null } > {job && (
{job.attempts > 0 && ( {job.attempts} peer{job.attempts === 1 ? "" : "s"} tried {job.state !== "failed" && job.remaining > 0 ? ` · ${job.remaining} left to try` : ""} )}
{job.detail && (

{job.detail}

)} {conf != null && (

Match confidence

{Math.round(conf * 100)}%
= 0.95 ? "ok" : conf >= 0.7 ? "warn" : "err"} /> {job.match_why && (

{job.match_why}

)}
)} {job.remote_file && (

File

{job.remote_file.replace(/\\/g, "/")}

)} {job.tried.length > 0 && (

Peers tried

    {job.tried.map((t, i) => (
  1. {t.username}
    {t.reason}
  2. ))} {job.state === "downloading" && job.username && (
  3. {job.username}
    downloading now
  4. )}
)}
)}
); } function StatePill({ state }: { state: Job["state"] }) { const map = { imported: { tone: "ok", icon: CheckCircle2, label: "In your library" }, failed: { tone: "err", icon: XCircle, label: "Failed" }, review: { tone: "accent", icon: CheckCircle2, label: "Waiting on you" }, downloading: { tone: "warn", icon: Loader2, label: "Downloading" }, searching: { tone: "warn", icon: Loader2, label: "Searching Soulseek" }, queued: { tone: "neutral", icon: Loader2, label: "Queued" }, } as const; const m = map[state]; const Icon = m.icon; return ( {m.label} ); }