File size: 4,753 Bytes
e6a1f55 | 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 | import { useEffect, useState } from "react";
import { listSamples, sampleDownloadUrl } from "@/lib/api";
import type { SampleAudio } from "@/types/inference";
import { useInferenceStore } from "@/store/inferenceStore";
import { cn } from "@/lib/utils";
import { CheckCircle2, Music2 } from "lucide-react";
export default function SampleLibrary() {
const [samples, setSamples] = useState<SampleAudio[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const audio = useInferenceStore((s) => s.audio);
const setAudio = useInferenceStore((s) => s.setAudio);
useEffect(() => {
let alive = true;
listSamples()
.then((r) => {
if (alive) setSamples(r.samples);
})
.catch((e) => {
if (alive)
setError(
e instanceof Error ? e.message : "Could not load sample list."
);
})
.finally(() => alive && setLoading(false));
return () => {
alive = false;
};
}, []);
if (loading) {
return (
<div className="font-mono text-xs text-ink-dim">loading samples…</div>
);
}
if (error) {
return <div className="font-mono text-xs text-danger">{error}</div>;
}
const reals = samples.filter((s) => s.label === "real");
const fakes = samples.filter((s) => s.label === "fake");
return (
<div>
<div className="mb-3 flex items-center justify-between">
<div className="label">sample library</div>
<span className="font-mono text-[10px] text-ink-mute">
{samples.length} clips
</span>
</div>
<SampleGroup title="real" tint="cyber" items={reals} audio={audio} setAudio={setAudio} />
<div className="my-3 border-t border-line/60" />
<SampleGroup title="fake" tint="danger" items={fakes} audio={audio} setAudio={setAudio} />
<div className="mt-3 font-mono text-[10px] text-ink-mute">
Tip: drop your own real-world WAVs into{" "}
<code className="font-mono">data/sample_audios/</code> to add them here.
</div>
</div>
);
}
function SampleGroup({
title,
tint,
items,
audio,
setAudio,
}: {
title: string;
tint: "cyber" | "danger";
items: SampleAudio[];
audio: ReturnType<typeof useInferenceStore.getState>["audio"];
setAudio: ReturnType<typeof useInferenceStore.getState>["setAudio"];
}) {
return (
<div>
<div className="mb-1.5 flex items-center gap-1.5">
<span
className={cn(
"h-1.5 w-1.5 rounded-full",
tint === "danger" ? "bg-danger" : "bg-cyber"
)}
/>
<span className="font-mono text-[10px] uppercase tracking-wider text-ink-dim">
{title} · {items.length}
</span>
</div>
<div className="grid gap-1.5">
{items.map((s) => {
const isSelected =
audio?.kind === "sample" && audio.sampleId === s.sample_id;
return (
<button
key={s.sample_id}
onClick={() =>
setAudio({
kind: "sample",
sampleId: s.sample_id,
url: sampleDownloadUrl(s.sample_id),
})
}
className={cn(
"group flex items-start justify-between gap-2 rounded-md border bg-surface-alt/50 px-2.5 py-2 text-left transition-all duration-150",
"hover:bg-surface-alt hover:translate-x-0.5",
isSelected
? tint === "danger"
? "border-danger/60 bg-danger/10"
: "border-cyber/60 bg-cyber/10"
: "border-line"
)}
>
<div className="flex min-w-0 items-start gap-2">
<Music2
className={cn(
"mt-0.5 h-3.5 w-3.5 shrink-0 transition-transform group-hover:scale-110",
tint === "danger" ? "text-danger" : "text-cyber"
)}
/>
<div className="min-w-0">
<div className="truncate font-mono text-[11px] text-ink">
{s.filename.replace(/\.wav$/, "")}
</div>
<div className="mt-0.5 truncate text-[10px] text-ink-dim">
{s.description}
</div>
</div>
</div>
{isSelected && (
<CheckCircle2
className={cn(
"h-3.5 w-3.5 shrink-0",
tint === "danger" ? "text-danger" : "text-cyber"
)}
/>
)}
</button>
);
})}
</div>
</div>
);
}
|