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