aimusic-attribution / client /src /pages /TrackDetail.tsx
emresar's picture
Upload folder using huggingface_hub
6678fa1 verified
import { useLocation, useParams } from "wouter";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ArrowLeft, Loader2, Music, FileAudio, TrendingUp, AlertCircle, RefreshCw } from "lucide-react";
import { trpc } from "@/lib/trpc";
import { Badge } from "@/components/ui/badge";
import { AudioPlayerWithMatches } from "@/components/AudioPlayerWithMatches";
export default function TrackDetail() {
const params = useParams<{ id: string }>();
const [, setLocation] = useLocation();
const trackId = parseInt(params.id || "0");
const utils = trpc.useUtils();
const { data, isLoading, error } = trpc.tracks.get.useQuery(
{ id: trackId },
{
enabled: !!trackId,
refetchInterval: (query) => {
// Refetch every 2 seconds if track is still processing
const trackData = query.state.data;
return trackData?.track?.status === "processing" || trackData?.track?.status === "pending" ? 2000 : false;
},
}
);
const reanalyzeMutation = trpc.tracks.reanalyze.useMutation({
onSuccess: () => {
utils.tracks.get.invalidate({ id: trackId });
},
});
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<Loader2 className="w-8 h-8 animate-spin text-primary" />
</div>
);
}
if (error || !data) {
return (
<div className="min-h-screen flex items-center justify-center">
<Card className="max-w-md">
<CardContent className="py-12 text-center">
<AlertCircle className="w-12 h-12 text-destructive mx-auto mb-4" />
<h3 className="text-lg font-semibold mb-2">Track not found</h3>
<p className="text-muted-foreground mb-6">
{error?.message || "The track you're looking for doesn't exist"}
</p>
<Button onClick={() => setLocation("/")}>
<ArrowLeft className="mr-2 w-4 h-4" />
Back to Tracks
</Button>
</CardContent>
</Card>
</div>
);
}
const { track, stems, attributions, jobs } = data;
const getStatusColor = (status: string) => {
switch (status) {
case "completed": return "bg-green-500";
case "processing": return "bg-blue-500";
case "failed": return "bg-red-500";
default: return "bg-yellow-500";
}
};
const getJobProgress = (jobType: string) => {
const job = jobs.find(j => j.jobType === jobType);
return job?.progress || 0;
};
const isProcessing = track.status === "processing" || track.status === "pending";
return (
<div className="min-h-screen bg-gradient-to-br from-purple-50 via-white to-blue-50">
<div className="container py-8">
<div className="max-w-6xl mx-auto">
<Button
variant="ghost"
onClick={() => setLocation("/")}
className="mb-6"
>
<ArrowLeft className="mr-2 w-4 h-4" />
Back to Tracks
</Button>
<div className="grid lg:grid-cols-3 gap-6">
{/* Track Info */}
<div className="lg:col-span-1">
<Card>
<CardHeader>
<div className="flex items-center justify-between mb-2">
<Music className="w-8 h-8 text-primary" />
<div className={`w-3 h-3 rounded-full ${getStatusColor(track.status)}`} />
</div>
<CardTitle className="text-2xl">{track.title}</CardTitle>
{track.artist && <CardDescription className="text-base">{track.artist}</CardDescription>}
</CardHeader>
<CardContent className="space-y-4">
<div>
<p className="text-sm text-muted-foreground mb-1">Status</p>
<div className="flex items-center gap-2">
<Badge variant={track.status === "completed" ? "default" : "secondary"}>
{track.status}
</Badge>
{(track.status === "completed" || track.status === "processing" || track.status === "failed" || track.status === "pending") && (
<Button
variant="outline"
size="sm"
onClick={() => reanalyzeMutation.mutate({ id: trackId })}
disabled={reanalyzeMutation.isPending}
>
{reanalyzeMutation.isPending ? (
<Loader2 className="w-3 h-3 animate-spin" />
) : (
<RefreshCw className="w-3 h-3" />
)}
<span className="ml-1">{track.status === "processing" ? "Restart" : "Re-analyze"}</span>
</Button>
)}
</div>
</div>
<div>
<p className="text-sm text-muted-foreground mb-1">Uploaded</p>
<p className="text-sm">{new Date(track.createdAt).toLocaleString()}</p>
</div>
{track.duration && (
<div>
<p className="text-sm text-muted-foreground mb-1">Duration</p>
<p className="text-sm">{Math.floor(track.duration / 60)}:{(track.duration % 60).toFixed(2).padStart(5, '0')}</p>
</div>
)}
</CardContent>
</Card>
{/* Processing Status */}
{isProcessing && (
<Card className="mt-6">
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
<Loader2 className="w-5 h-5 animate-spin" />
Processing
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<div className="flex justify-between text-sm mb-2">
<span>Stem Separation</span>
<span>{getJobProgress("stem_separation")}%</span>
</div>
<Progress value={getJobProgress("stem_separation")} />
</div>
<div>
<div className="flex justify-between text-sm mb-2">
<span>Fingerprinting</span>
<span>{getJobProgress("fingerprinting")}%</span>
</div>
<Progress value={getJobProgress("fingerprinting")} />
</div>
<div>
<div className="flex justify-between text-sm mb-2">
<span>Embedding</span>
<span>{getJobProgress("embedding")}%</span>
</div>
<Progress value={getJobProgress("embedding")} />
</div>
<div>
<div className="flex justify-between text-sm mb-2">
<span>Attribution</span>
<span>{getJobProgress("attribution")}%</span>
</div>
<Progress value={getJobProgress("attribution")} />
</div>
</CardContent>
</Card>
)}
</div>
{/* Main Content */}
<div className="lg:col-span-2">
<Tabs defaultValue="attribution" className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="attribution">Attribution Results</TabsTrigger>
<TabsTrigger value="stems">Stems</TabsTrigger>
</TabsList>
<TabsContent value="attribution" className="space-y-4 mt-6">
{track.status === "completed" ? (
(() => {
// All chunk-level matches (both fingerprint and style have chunk info now)
// Exact matches: chromaprint OR matchType === "exact" in metadata
const fingerprintMatches = attributions.filter(a =>
(a.metadata as any)?.method === "chromaprint" ||
(a.metadata as any)?.matchType === "exact"
);
// Style matches: method === "style" AND not exact matchType
const styleMatches = attributions.filter(a =>
a.method === "style" && (a.metadata as any)?.matchType !== "exact"
);
// Combine both for the hover player
const allChunkMatches = attributions.filter(a => (a.metadata as any)?.chunkIndex !== undefined);
return (
<div className="space-y-6">
{/* Main player with ALL chunk matches (fingerprint + style) */}
<div>
<h3 className="text-sm font-medium mb-3">Attribution Analysis</h3>
<div className="flex gap-2 mb-2">
{fingerprintMatches.length > 0 && (
<Badge variant="outline" className="text-xs">
<span className="w-2 h-2 bg-green-500 rounded-full mr-1"></span>
{fingerprintMatches.length} exact matches
</Badge>
)}
{styleMatches.length > 0 && (
<Badge variant="outline" className="text-xs">
<span className="w-2 h-2 bg-orange-500 rounded-full mr-1"></span>
{styleMatches.length} style matches
</Badge>
)}
</div>
<AudioPlayerWithMatches
src={track.fileUrl}
title={track.title}
attributions={allChunkMatches.map(a => ({
id: a.id,
score: a.score,
metadata: a.metadata as any,
}))}
/>
<p className="text-xs text-muted-foreground mt-2">
Hover over the waveform to see matching training tracks for each section
</p>
</div>
{/* Stem players if available */}
{stems.length > 0 && (
<div>
<h3 className="text-sm font-medium mb-3">Stem Analysis</h3>
<div className="space-y-3">
{stems.map((stem) => {
const stemAttrs = allChunkMatches
.filter(a => (a.metadata as any)?.stemType === stem.stemType)
.map(a => ({
id: a.id,
score: a.score,
metadata: a.metadata as any,
}));
return (
<AudioPlayerWithMatches
key={stem.id}
src={stem.fileUrl}
title={`${track.title}`}
stemType={stem.stemType}
attributions={stemAttrs}
/>
);
})}
</div>
</div>
)}
{/* Summary stats */}
{attributions.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="text-base">Attribution Summary</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div>
<p className="text-muted-foreground mb-1">Style Matches</p>
<p className="text-2xl font-bold text-orange-600">{styleMatches.length}</p>
</div>
<div>
<p className="text-muted-foreground mb-1">Exact Matches</p>
<p className="text-2xl font-bold text-green-600">{fingerprintMatches.length}</p>
</div>
<div>
<p className="text-muted-foreground mb-1">Highest Score</p>
<p className="text-2xl font-bold">
{(Math.max(...attributions.map(a => a.score)) * 100).toFixed(0)}%
</p>
</div>
<div>
<p className="text-muted-foreground mb-1">Unique Tracks</p>
<p className="text-2xl font-bold">
{new Set(attributions.map(a => (a.metadata as any)?.matchedTitle)).size}
</p>
</div>
</div>
</CardContent>
</Card>
)}
{attributions.length === 0 && (
<Card>
<CardContent className="py-12 text-center">
<TrendingUp className="w-12 h-12 text-muted-foreground mx-auto mb-4" />
<h3 className="text-lg font-semibold mb-2">No matches found</h3>
<p className="text-muted-foreground">
No training tracks matched this AI-generated music
</p>
</CardContent>
</Card>
)}
</div>
);
})()
) : (
<Card>
<CardContent className="py-12 text-center">
<Loader2 className="w-12 h-12 text-primary mx-auto mb-4 animate-spin" />
<h3 className="text-lg font-semibold mb-2">Processing track...</h3>
<p className="text-muted-foreground">
Attribution results will appear here once processing is complete
</p>
</CardContent>
</Card>
)}
</TabsContent>
<TabsContent value="stems" className="space-y-4 mt-6">
{stems.length > 0 ? (
<div className="grid sm:grid-cols-2 gap-4">
{stems.map((stem) => (
<Card key={stem.id}>
<CardHeader>
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-primary/10 rounded-lg flex items-center justify-center">
<FileAudio className="w-5 h-5 text-primary" />
</div>
<div>
<CardTitle className="text-base capitalize">{stem.stemType}</CardTitle>
<CardDescription className="text-xs">
{stem.duration ? `${Math.floor(stem.duration / 60)}:${(stem.duration % 60).toString().padStart(2, '0')}` : "N/A"}
</CardDescription>
</div>
</div>
</CardHeader>
</Card>
))}
</div>
) : (
<Card>
<CardContent className="py-12 text-center">
<FileAudio className="w-12 h-12 text-muted-foreground mx-auto mb-4" />
<h3 className="text-lg font-semibold mb-2">
{isProcessing ? "Separating stems..." : "No stems available"}
</h3>
<p className="text-muted-foreground">
{isProcessing
? "Stems will appear here once separation is complete"
: "Stem separation failed or hasn't started yet"
}
</p>
</CardContent>
</Card>
)}
</TabsContent>
</Tabs>
</div>
</div>
</div>
</div>
</div>
);
}