Function bodies 245 total
midiToNote function · typescript · L40-L42 (3 LOC)src/components/analysis/analysis-display.tsx
function midiToNote(midi: number): string {
return `${NOTE_NAMES[midi % 12]}${Math.floor(midi / 12) - 1}`;
}TeachingSummary function · typescript · L44-L154 (111 LOC)src/components/analysis/analysis-display.tsx
function TeachingSummary({ summary }: { summary: Summary }) {
return (
<div className="space-y-4">
{/* Overview Card */}
<Card>
<CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
<BookOpen className="h-4 w-4" />
Overview
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm leading-relaxed">{summary.overview}</p>
<p className="mt-2 text-sm">
<span className="font-medium">Key Center:</span>{" "}
<span className="text-muted-foreground">{summary.key_center}</span>
</p>
{summary.rhythm_and_feel && (
<p className="mt-1 text-sm">
<span className="font-medium">Feel:</span>{" "}
<span className="text-muted-foreground">{summary.rhythm_and_feel}</span>
</p>
)}
</CardContent>
</Card>
{/* SecRawAnalysisDetails function · typescript · L156-L293 (138 LOC)src/components/analysis/analysis-display.tsx
function RawAnalysisDetails({ analysis }: AnalysisDisplayProps) {
const uniqueChords = [...new Set(analysis.chords.map((c) => c.chord))];
const chordCounts = new Map<string, number>();
for (const c of analysis.chords) {
chordCounts.set(c.chord, (chordCounts.get(c.chord) || 0) + 1);
}
const sortedChords = [...chordCounts.entries()]
.sort((a, b) => b[1] - a[1]);
const midiValues = analysis.notes.map((n) => n.midi);
const minNote = midiValues.length > 0 ? midiToNote(Math.min(...midiValues)) : "N/A";
const maxNote = midiValues.length > 0 ? midiToNote(Math.max(...midiValues)) : "N/A";
const confidence = analysis.key_confidence ?? 0;
const confidenceLabel = confidence > 0.85 ? "high" : confidence > 0.7 ? "moderate" : "low";
return (
<div className="space-y-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
<Card>
<CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-smAnalysisDisplay function · typescript · L295-L328 (34 LOC)src/components/analysis/analysis-display.tsx
export function AnalysisDisplay({ analysis }: AnalysisDisplayProps) {
const [showRawData, setShowRawData] = useState(false);
const hasSummary = analysis.summary && typeof analysis.summary === "object";
if (!hasSummary) {
return <RawAnalysisDetails analysis={analysis} />;
}
return (
<div className="space-y-4">
<TeachingSummary summary={analysis.summary!} />
<Button
variant="ghost"
className="w-full text-muted-foreground"
onClick={() => setShowRawData(!showRawData)}
>
{showRawData ? (
<>
<ChevronUp className="mr-2 h-4 w-4" />
Hide Raw Analysis Data
</>
) : (
<>
<ChevronDown className="mr-2 h-4 w-4" />
Show Raw Analysis Data
</>
)}
</Button>
{showRawData && <RawAnalysisDetails analysis={analysis} />}
</div>
);
}AnalyzeButton function · typescript · L19-L147 (129 LOC)src/components/analysis/analyze-button.tsx
export function AnalyzeButton({ recordingId, recordingTitle, audioUrl, onComplete, hasExisting }: AnalyzeButtonProps) {
const [analyzing, setAnalyzing] = useState(false);
const [stage, setStage] = useState("");
const [progress, setProgress] = useState(0);
async function handleAnalyze() {
setAnalyzing(true);
setStage("Starting analysis...");
setProgress(0);
try {
const { transcribeAudio } = await import("@/lib/audio/transcribe");
const { analyzeNotes } = await import("@/lib/audio/analyze");
const notes = await transcribeAudio(audioUrl, (stageMsg, prog) => {
setStage(stageMsg);
setProgress(prog);
});
setStage("Analyzing music theory...");
setProgress(90);
const result = analyzeNotes(notes);
setStage("Saving results...");
setProgress(95);
const supabase = createClient();
const { data, error } = await supabase
.from("analyses")
.upsert({
recording_id: rhandleAnalyze function · typescript · L24-L96 (73 LOC)src/components/analysis/analyze-button.tsx
async function handleAnalyze() {
setAnalyzing(true);
setStage("Starting analysis...");
setProgress(0);
try {
const { transcribeAudio } = await import("@/lib/audio/transcribe");
const { analyzeNotes } = await import("@/lib/audio/analyze");
const notes = await transcribeAudio(audioUrl, (stageMsg, prog) => {
setStage(stageMsg);
setProgress(prog);
});
setStage("Analyzing music theory...");
setProgress(90);
const result = analyzeNotes(notes);
setStage("Saving results...");
setProgress(95);
const supabase = createClient();
const { data, error } = await supabase
.from("analyses")
.upsert({
recording_id: recordingId,
status: result.status,
key_signature: result.key_signature,
tempo: result.tempo,
time_signature: result.time_signature,
chords: result.chords,
notes: result.notes,
midi_data: resultChordTimeline function · typescript · L20-L74 (55 LOC)src/components/analysis/chord-timeline.tsx
export function ChordTimeline({ chords, currentTime, duration }: ChordTimelineProps) {
if (chords.length === 0 || duration === 0) return null;
// Assign colors to unique chords
const uniqueChords = [...new Set(chords.map((c) => c.chord))];
const colorMap = new Map(
uniqueChords.map((chord, i) => [chord, CHORD_COLORS[i % CHORD_COLORS.length]])
);
// Find currently active chord
const activeChord = chords.find(
(c) => currentTime >= c.time && currentTime < c.time + c.duration
);
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium">Chord Timeline</h3>
{activeChord && (
<span className="rounded-md bg-primary/10 px-2 py-1 text-sm font-bold text-primary">
{activeChord.chord}
</span>
)}
</div>
<div className="relative h-10 rounded-lg border bg-card overflow-hidden">
{/* Playhead */}
<div
clasGenerated by Repobility's multi-pass static-analysis pipeline (https://repobility.com)
ExportMidiButton function · typescript · L12-L24 (13 LOC)src/components/analysis/export-midi-button.tsx
export function ExportMidiButton({ notes, filename = "transcription.mid" }: ExportMidiButtonProps) {
async function handleExport() {
const { downloadMidi } = await import("@/lib/audio/midi-utils");
downloadMidi(notes, filename);
}
return (
<Button variant="outline" size="sm" onClick={handleExport} disabled={notes.length === 0}>
<Download className="mr-2 h-4 w-4" />
Export MIDI
</Button>
);
}handleExport function · typescript · L13-L16 (4 LOC)src/components/analysis/export-midi-button.tsx
async function handleExport() {
const { downloadMidi } = await import("@/lib/audio/midi-utils");
downloadMidi(notes, filename);
}PianoRoll function · typescript · L9-L116 (108 LOC)src/components/analysis/piano-roll.tsx
export function PianoRoll({ notes, currentTime, duration }: PianoRollProps) {
if (notes.length === 0 || duration === 0) return null;
const SVG_WIDTH = 1200;
const SVG_HEIGHT = 300;
const PADDING = { top: 10, bottom: 10, left: 40, right: 10 };
const plotWidth = SVG_WIDTH - PADDING.left - PADDING.right;
const plotHeight = SVG_HEIGHT - PADDING.top - PADDING.bottom;
// Find note range
const midiValues = notes.map((n) => n.midi);
const minMidi = Math.max(0, Math.min(...midiValues) - 2);
const maxMidi = Math.min(127, Math.max(...midiValues) + 2);
const midiRange = maxMidi - minMidi || 1;
function timeToX(t: number): number {
return PADDING.left + (t / duration) * plotWidth;
}
function midiToY(midi: number): number {
return PADDING.top + plotHeight - ((midi - minMidi) / midiRange) * plotHeight;
}
const noteHeight = Math.max(2, Math.min(8, plotHeight / midiRange));
// Generate piano key labels (every octave C)
const labels: { midi: number; namtimeToX function · typescript · L25-L27 (3 LOC)src/components/analysis/piano-roll.tsx
function timeToX(t: number): number {
return PADDING.left + (t / duration) * plotWidth;
}midiToY function · typescript · L29-L31 (3 LOC)src/components/analysis/piano-roll.tsx
function midiToY(midi: number): number {
return PADDING.top + plotHeight - ((midi - minMidi) / midiRange) * plotHeight;
}createShaderProgram function · typescript · L600-L627 (28 LOC)src/components/audio/visualizer.tsx
function createShaderProgram(gl: WebGLRenderingContext, fragSource: string): WebGLProgram | null {
const vs = gl.createShader(gl.VERTEX_SHADER)!;
gl.shaderSource(vs, VERTEX_SHADER);
gl.compileShader(vs);
if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS)) {
console.error("Vertex shader:", gl.getShaderInfoLog(vs));
return null;
}
const fs = gl.createShader(gl.FRAGMENT_SHADER)!;
gl.shaderSource(fs, fragSource);
gl.compileShader(fs);
if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS)) {
console.error("Fragment shader:", gl.getShaderInfoLog(fs));
return null;
}
const prog = gl.createProgram()!;
gl.attachShader(prog, vs);
gl.attachShader(prog, fs);
gl.linkProgram(prog);
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
console.error("Program link:", gl.getProgramInfoLog(prog));
return null;
}
return prog;
}ShaderVisualizer function · typescript · L651-L726 (76 LOC)src/components/audio/visualizer.tsx
function ShaderVisualizer({
analyser,
dataArray,
fragShader,
}: {
analyser: AnalyserNode;
dataArray: Uint8Array<ArrayBuffer>;
fragShader: string;
}) {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const gl = canvas.getContext("webgl");
if (!gl) return;
const program = createShaderProgram(gl, fragShader);
if (!program) return;
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, 1,1]), gl.STATIC_DRAW);
const posLoc = gl.getAttribLocation(program, "a_position");
gl.enableVertexAttribArray(posLoc);
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
const uTime = gl.getUniformLocation(program, "u_time");
const uRes = gl.getUniformLocation(program, "u_resolution");
const uBass = gl.getUniformLocation(program, "u_bass");
const uMid = glrender function · typescript · L690-L711 (22 LOC)src/components/audio/visualizer.tsx
function render() {
if (!canvas || !gl) return;
canvas.width = canvas.clientWidth * devicePixelRatio;
canvas.height = canvas.clientHeight * devicePixelRatio;
gl.viewport(0, 0, canvas.width, canvas.height);
const time = (performance.now() - startTime) / 1000;
// Still read audio to keep analyser flowing, but don't pass meaningful values
analyser.getByteFrequencyData(dataArray);
gl.useProgram(program);
gl.uniform1f(uTime, time);
gl.uniform2f(uRes, canvas.width, canvas.height);
gl.uniform1f(uBass, 0.0);
gl.uniform1f(uMid, 0.0);
gl.uniform1f(uTreble, 0.0);
gl.uniform1f(uAmplitude, 0.0);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
animId = requestAnimationFrame(render);
}Source: Repobility analyzer · https://repobility.com
Visualizer function · typescript · L728-L804 (77 LOC)src/components/audio/visualizer.tsx
export function Visualizer({ audioElement, onClose }: VisualizerProps) {
const [mode, setMode] = useState<VisualizerMode>("mandala");
const [analyser, setAnalyser] = useState<AnalyserNode | null>(null);
const [dataArray, setDataArray] = useState<Uint8Array<ArrayBuffer> | null>(null);
useEffect(() => {
if (!audioElement) return;
const cached = audioNodeCache.get(audioElement);
if (cached) {
if (cached.ctx.state === "suspended") cached.ctx.resume();
setAnalyser(cached.analyser);
setDataArray(new Uint8Array(cached.analyser.frequencyBinCount));
return;
}
const ctx = new AudioContext();
const source = ctx.createMediaElementSource(audioElement);
const analyserNode = ctx.createAnalyser();
analyserNode.fftSize = 256;
source.connect(analyserNode);
analyserNode.connect(ctx.destination);
audioNodeCache.set(audioElement, { ctx, analyser: analyserNode });
setAnalyser(analyserNode);
setDataArray(new Uint8Array(anformatTime function · typescript · L30-L34 (5 LOC)src/components/audio/waveform-player.tsx
function formatTime(seconds: number): string {
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${m}:${s.toString().padStart(2, "0")}`;
}isChromium function · typescript · L36-L40 (5 LOC)src/components/audio/waveform-player.tsx
function isChromium(): boolean {
if (typeof navigator === "undefined") return false;
const ua = navigator.userAgent;
return /Chrome|Chromium|Edg\//.test(ua);
}getCachedUrl function · typescript · L51-L64 (14 LOC)src/components/audio/waveform-player.tsx
function getCachedUrl(recordingId: string): string | null {
try {
const raw = sessionStorage.getItem(`audio-url-${recordingId}`);
if (!raw) return null;
const entry: CachedUrlEntry = JSON.parse(raw);
if (Date.now() - entry.timestamp > URL_CACHE_TTL_MS) {
sessionStorage.removeItem(`audio-url-${recordingId}`);
return null;
}
return entry.url;
} catch {
return null;
}
}setCachedUrl function · typescript · L66-L73 (8 LOC)src/components/audio/waveform-player.tsx
function setCachedUrl(recordingId: string, url: string): void {
try {
const entry: CachedUrlEntry = { url, timestamp: Date.now() };
sessionStorage.setItem(`audio-url-${recordingId}`, JSON.stringify(entry));
} catch {
// sessionStorage may be full or unavailable
}
}resolveAudioUrlImpl function · typescript · L77-L125 (49 LOC)src/components/audio/waveform-player.tsx
async function resolveAudioUrlImpl(
audioUrl: string,
recordingId?: string,
): Promise<string> {
// Check cache first
if (recordingId) {
const cached = getCachedUrl(recordingId);
if (cached) return cached;
}
if (!audioUrl.startsWith("/api/")) return audioUrl;
try {
const res = await fetch(audioUrl);
const data = await res.json();
if (data.url) {
if (data.hasAac || (data.codec && data.codec !== "alac")) {
if (recordingId) setCachedUrl(recordingId, data.url);
return data.url;
}
if (data.codec === "alac" && isChromium()) {
return audioUrl + "?transcode=1";
}
// Unknown codec — test playability
const testAudio = new Audio();
try {
const canPlay = await new Promise<boolean>((resolve) => {
testAudio.preload = "metadata";
testAudio.onloadedmetadata = () => resolve(true);
testAudio.onerror = () => resolve(false);
testAudio.src = data.url;
WaveformPlayer function · typescript · L128-L526 (399 LOC)src/components/audio/waveform-player.tsx
function WaveformPlayer({ audioUrl, recordingId, peaks, duration: propDuration, onTimeUpdate, markers = [], onVisualizerOpen }, ref) {
const themeColors = useThemeColors();
const hasPeaks = !!(peaks && peaks.length > 0 && propDuration);
// --- State (6 variables) ---
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(propDuration ?? 0);
const [isReady, setIsReady] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isLoadingAudio, setIsLoadingAudio] = useState(false);
// --- Refs ---
const containerRef = useRef<HTMLDivElement>(null);
const wavesurferRef = useRef<WaveSurfer | null>(null);
const audioRef = useRef<HTMLAudioElement | null>(null);
const resolvedUrlRef = useRef<string | null>(null);
const urlResolvePromiseRef = useRef<Promise<string> | null>(null);
const peaksSavedRef = useRef(false);
togglePlay function · typescript · L368-L400 (33 LOC)src/components/audio/waveform-player.tsx
async function togglePlay() {
const ws = wavesurferRef.current;
if (!ws) return;
// If URL is already resolved, just play/pause
if (resolvedUrlRef.current) {
ws.playPause();
return;
}
// URL still resolving — show spinner, await, then play
setIsLoadingAudio(true);
try {
let promise = urlResolvePromiseRef.current;
if (!promise) {
// Edge case: resolve fresh
promise = resolveAudioUrlImpl(audioUrl, recordingId);
urlResolvePromiseRef.current = promise;
}
const url = await promise;
if (cancelledRef.current) return;
resolvedUrlRef.current = url;
const audio = audioRef.current;
if (audio && !audio.src) {
audio.src = url;
}
ws.playPause();
} finally {
setIsLoadingAudio(false);
}
}All rows scored by the Repobility analyzer (https://repobility.com)
skip function · typescript · L402-L409 (8 LOC)src/components/audio/waveform-player.tsx
function skip(seconds: number) {
const ws = wavesurferRef.current;
if (!ws) return;
const d = ws.getDuration();
if (d <= 0) return;
const newTime = Math.max(0, Math.min(ws.getCurrentTime() + seconds, d));
ws.seekTo(newTime / d);
}handleMarkerClick function · typescript · L411-L415 (5 LOC)src/components/audio/waveform-player.tsx
function handleMarkerClick(time: number) {
if (wavesurferRef.current && duration > 0) {
wavesurferRef.current.seekTo(time / duration);
}
}ChatMessage function · typescript · L12-L48 (37 LOC)src/components/chat/chat-message.tsx
export function ChatMessage({ role, content }: ChatMessageProps) {
return (
<div
className={cn(
"flex gap-3",
role === "user" ? "justify-end" : "justify-start"
)}
>
{role === "assistant" && (
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-primary/10">
<Bot className="h-4 w-4 text-primary" />
</div>
)}
<div
className={cn(
"max-w-[80%] rounded-2xl px-4 py-3 text-sm",
role === "user"
? "bg-primary text-primary-foreground rounded-tr-sm"
: "bg-muted rounded-tl-sm"
)}
>
{role === "assistant" ? (
<div className="prose prose-sm dark:prose-invert max-w-none">
<ReactMarkdown>{content}</ReactMarkdown>
</div>
) : (
<p>{content}</p>
)}
</div>
{role === "user" && (
<div className="flex h-8 w-8 shrink-0 items-center justify-center roundChatPanel function · typescript · L31-L115 (85 LOC)src/components/chat/chat-panel.tsx
export function ChatPanel({ recordingId, analysis, initialMessages = [] }: ChatPanelProps) {
const scrollRef = useRef<HTMLDivElement>(null);
const { messages, input, handleInputChange, handleSubmit, isLoading, append } =
useChat({
api: "/api/chat",
body: { recordingId, analysis },
initialMessages: initialMessages.map((m) => ({
id: m.id,
role: m.role as "user" | "assistant",
content: m.content,
})),
onError: (error) => {
toast.error(error.message || "Failed to get response. Please try again.");
},
onFinish: async (message) => {
const supabase = createClient();
const { data: { user } } = await supabase.auth.getUser();
if (user) {
await supabase.from("chat_messages").insert({
recording_id: recordingId,
user_id: user.id,
role: "assistant",
content: message.content,
});
}
},
});
useEffect(() => {
handleSuggestedPrompt function · typescript · L66-L68 (3 LOC)src/components/chat/chat-panel.tsx
function handleSuggestedPrompt(prompt: string) {
append({ role: "user", content: prompt });
}SuggestedPrompts function · typescript · L18-L37 (20 LOC)src/components/chat/suggested-prompts.tsx
export function SuggestedPrompts({ onSelect }: SuggestedPromptsProps) {
return (
<div className="space-y-2">
<p className="text-sm text-muted-foreground">Suggested questions:</p>
<div className="flex flex-wrap gap-2">
{PROMPTS.map((prompt) => (
<Button
key={prompt}
variant="outline"
size="sm"
className="text-xs hover:border-primary/30"
onClick={() => onSelect(prompt)}
>
{prompt}
</Button>
))}
</div>
</div>
);
}CollectionDetail function · typescript · L99-L331 (233 LOC)src/components/collections/collection-detail.tsx
export function CollectionDetail({
collectionId,
initialName,
initialDescription,
initialRecordings,
availableRecordings: initialAvailable = [],
}: {
collectionId: string;
initialName: string;
initialDescription: string;
initialRecordings: Recording[];
availableRecordings?: AvailableRecording[];
}) {
const [recordings, setRecordings] = useState(initialRecordings);
const [available, setAvailable] = useState(initialAvailable);
const [showPicker, setShowPicker] = useState(false);
const [name, setName] = useState(initialName);
const [isEditingName, setIsEditingName] = useState(false);
const [editName, setEditName] = useState(initialName);
const nameInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (isEditingName && nameInputRef.current) {
nameInputRef.current.focus();
nameInputRef.current.select();
}
}, [isEditingName]);
async function saveName() {
const trimmed = editName.trim();
if (!trimmed || trimmed ==saveName function · typescript · L127-L147 (21 LOC)src/components/collections/collection-detail.tsx
async function saveName() {
const trimmed = editName.trim();
if (!trimmed || trimmed === name) {
setEditName(name);
setIsEditingName(false);
return;
}
const supabase = createClient();
const { error } = await supabase
.from("collections")
.update({ name: trimmed })
.eq("id", collectionId);
if (error) {
toast.error("Failed to rename collection");
setEditName(name);
} else {
setName(trimmed);
toast.success("Collection renamed");
}
setIsEditingName(false);
}Powered by Repobility — scan your code at https://repobility.com
handleDragEnd function · typescript · L156-L174 (19 LOC)src/components/collections/collection-detail.tsx
async function handleDragEnd(event: DragEndEvent) {
const { active, over } = event;
if (!over || active.id === over.id) return;
const oldIndex = recordings.findIndex((r) => r.id === active.id);
const newIndex = recordings.findIndex((r) => r.id === over.id);
const newOrder = arrayMove(recordings, oldIndex, newIndex);
setRecordings(newOrder);
const supabase = createClient();
for (let i = 0; i < newOrder.length; i++) {
await supabase
.from("collection_recordings")
.update({ position: i })
.eq("collection_id", collectionId)
.eq("recording_id", newOrder[i].id);
}
}addRecording function · typescript · L176-L197 (22 LOC)src/components/collections/collection-detail.tsx
async function addRecording(rec: AvailableRecording) {
const supabase = createClient();
const position = recordings.length;
const { error } = await supabase.from("collection_recordings").insert({
collection_id: collectionId,
recording_id: rec.id,
position,
});
if (error) {
toast.error("Failed to add recording");
return;
}
setRecordings((prev) => [
...prev,
{ ...rec, position },
]);
setAvailable((prev) => prev.filter((r) => r.id !== rec.id));
toast.success(`Added "${rec.title}"`);
}removeRecording function · typescript · L199-L222 (24 LOC)src/components/collections/collection-detail.tsx
async function removeRecording(recordingId: string) {
const supabase = createClient();
const { error } = await supabase
.from("collection_recordings")
.delete()
.eq("collection_id", collectionId)
.eq("recording_id", recordingId);
if (error) {
toast.error("Failed to remove recording");
return;
}
const removed = recordings.find((r) => r.id === recordingId);
setRecordings((prev) => prev.filter((r) => r.id !== recordingId));
if (removed) {
setAvailable((prev) => [
...prev,
{ id: removed.id, title: removed.title, duration: removed.duration, created_at: removed.created_at },
]);
}
toast.success("Recording removed from collection");
}CreateCollectionDialog function · typescript · L19-L92 (74 LOC)src/components/collections/create-collection-dialog.tsx
export function CreateCollectionDialog() {
const [open, setOpen] = useState(false);
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [loading, setLoading] = useState(false);
const router = useRouter();
async function handleCreate(e: React.FormEvent) {
e.preventDefault();
if (!name.trim()) return;
setLoading(true);
const supabase = createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) return;
const { error } = await supabase.from("collections").insert({
name: name.trim(),
description: description.trim() || null,
user_id: user.id,
});
if (error) {
toast.error("Failed to create collection");
} else {
toast.success("Collection created");
setName("");
setDescription("");
setOpen(false);
router.refresh();
}
setLoading(false);
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DiahandleCreate function · typescript · L26-L51 (26 LOC)src/components/collections/create-collection-dialog.tsx
async function handleCreate(e: React.FormEvent) {
e.preventDefault();
if (!name.trim()) return;
setLoading(true);
const supabase = createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) return;
const { error } = await supabase.from("collections").insert({
name: name.trim(),
description: description.trim() || null,
user_id: user.id,
});
if (error) {
toast.error("Failed to create collection");
} else {
toast.success("Collection created");
setName("");
setDescription("");
setOpen(false);
router.refresh();
}
setLoading(false);
}ChordFrequency function · typescript · L11-L35 (25 LOC)src/components/insights/chord-frequency.tsx
export function ChordFrequency({ data }: ChordFrequencyProps) {
const { chart1 } = useThemeColors();
return (
<Card>
<CardHeader>
<CardTitle className="text-base">Most Used Chords</CardTitle>
</CardHeader>
<CardContent>
{data.length > 0 ? (
<ResponsiveContainer width="100%" height={250}>
<BarChart data={data.slice(0, 15)} layout="vertical">
<XAxis type="number" fontSize={11} allowDecimals={false} />
<YAxis type="category" dataKey="chord" fontSize={11} width={60} />
<Tooltip />
<Bar dataKey="count" fill={chart1} radius={[0, 4, 4, 0]} />
</BarChart>
</ResponsiveContainer>
) : (
<p className="text-sm text-muted-foreground">No chord data available</p>
)}
</CardContent>
</Card>
);
}InsightsChat function · typescript · L28-L103 (76 LOC)src/components/insights/insights-chat.tsx
export function InsightsChat({ analyses }: InsightsChatProps) {
const scrollRef = useRef<HTMLDivElement>(null);
const { messages, input, handleInputChange, handleSubmit, isLoading, append } =
useChat({
api: "/api/chat/insights",
body: { analyses },
});
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [messages]);
return (
<div className="flex h-[350px] flex-col rounded-lg border sm:h-[500px]">
<ScrollArea ref={scrollRef} className="flex-1 p-4">
{messages.length === 0 ? (
<div className="flex h-full flex-col items-center justify-center gap-3">
<p className="text-sm text-muted-foreground">
Ask about patterns across your {analyses.length} recordings
</p>
<div className="flex flex-wrap justify-center gap-2">
{SUGGESTED.map((prompt) => (
<Button
key={prompt}
formatTotalTime function · typescript · L38-L43 (6 LOC)src/components/insights/insights-dashboard.tsx
function formatTotalTime(seconds: number): string {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours > 0) return `${hours}h ${minutes}m`;
return `${minutes}m`;
}Generated by Repobility's multi-pass static-analysis pipeline (https://repobility.com)
StatCard function · typescript · L45-L69 (25 LOC)src/components/insights/insights-dashboard.tsx
function StatCard({
label,
value,
icon: Icon,
}: {
label: string;
value: string;
icon: React.ComponentType<{ className?: string }>;
}) {
return (
<Card>
<CardContent className="flex items-center gap-3 p-4">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/10">
<Icon className="h-5 w-5 text-primary" />
</div>
<div>
<p className="text-2xl font-bold leading-none tracking-tight tabular-nums sm:text-3xl">{value}</p>
<p className="mt-1 text-xs uppercase tracking-widest text-muted-foreground">
{label}
</p>
</div>
</CardContent>
</Card>
);
}InsightsDashboard function · typescript · L71-L247 (177 LOC)src/components/insights/insights-dashboard.tsx
export function InsightsDashboard({ analyses, totalRecordings }: InsightsDashboardProps) {
const count = analyses.length;
const keyDist = getKeyDistribution(analyses);
const chordFreq = getChordFrequency(analyses);
const { tendencies, dominantStyle } = getHarmonicTendencies(analyses);
// Quick stats
const mostCommonKey = keyDist.length > 0 ? keyDist[0].key : "--";
const tempos = analyses.map((a) => a.tempo).filter((t): t is number => t != null);
const avgTempo = tempos.length > 0 ? Math.round(tempos.reduce((a, b) => a + b, 0) / tempos.length) : null;
const allUniqueChords = new Set<string>();
for (const a of analyses) {
for (const c of a.chords) allUniqueChords.add(c.chord);
}
const totalDuration = analyses
.map((a) => a.duration)
.filter((d): d is number => d != null)
.reduce((a, b) => a + b, 0);
// For sections that need 2+ or 3+
const progressions = count >= 2 ? findCommonProgressions(analyses) : [];
const similar = count >= 3 ? findSiKeyDistribution function · typescript · L11-L35 (25 LOC)src/components/insights/key-distribution.tsx
export function KeyDistribution({ data }: KeyDistributionProps) {
const { chart1 } = useThemeColors();
return (
<Card>
<CardHeader>
<CardTitle className="text-base">Key Distribution</CardTitle>
</CardHeader>
<CardContent>
{data.length > 0 ? (
<ResponsiveContainer width="100%" height={250}>
<BarChart data={data}>
<XAxis dataKey="key" fontSize={11} angle={-45} textAnchor="end" height={60} />
<YAxis fontSize={11} allowDecimals={false} />
<Tooltip />
<Bar dataKey="count" fill={chart1} radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
) : (
<p className="text-sm text-muted-foreground">No key data available</p>
)}
</CardContent>
</Card>
);
}LibrarySummaryPanel function · typescript · L38-L231 (194 LOC)src/components/insights/library-summary.tsx
export function LibrarySummaryPanel({ analyses }: LibrarySummaryProps) {
const [summary, setSummary] = useState<LibrarySummary | null>(null);
const [loading, setLoading] = useState(false);
const [expandedClusters, setExpandedClusters] = useState<Set<number>>(new Set());
useEffect(() => {
const cached = localStorage.getItem(CACHE_KEY);
if (cached) {
try {
const { data, timestamp, count } = JSON.parse(cached);
if (Date.now() - timestamp < CACHE_DURATION && count === analyses.length) {
setSummary(data);
return;
}
} catch {}
}
generateSummary();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
async function generateSummary() {
setLoading(true);
try {
const res = await fetch("/api/insights/summarize", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ analyses }),
});
if (!res.ok) throw new Error("FaigenerateSummary function · typescript · L58-L79 (22 LOC)src/components/insights/library-summary.tsx
async function generateSummary() {
setLoading(true);
try {
const res = await fetch("/api/insights/summarize", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ analyses }),
});
if (!res.ok) throw new Error("Failed to generate");
const { summary: data } = await res.json();
setSummary(data);
localStorage.setItem(CACHE_KEY, JSON.stringify({
data,
timestamp: Date.now(),
count: analyses.length,
}));
} catch (err) {
console.error("Failed to generate library summary:", err);
} finally {
setLoading(false);
}
}toggleCluster function · typescript · L81-L91 (11 LOC)src/components/insights/library-summary.tsx
function toggleCluster(index: number) {
setExpandedClusters((prev) => {
const next = new Set(prev);
if (next.has(index)) {
next.delete(index);
} else {
next.add(index);
}
return next;
});
}ProgressionPatterns function · typescript · L14-L61 (48 LOC)src/components/insights/progression-patterns.tsx
export function ProgressionPatterns({ progressions }: ProgressionPatternsProps) {
if (progressions.length === 0) {
return (
<Card>
<CardHeader>
<CardTitle className="text-base">Recurring Progressions</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Analyze more recordings to discover recurring chord progressions
</p>
</CardContent>
</Card>
);
}
return (
<Card>
<CardHeader>
<CardTitle className="text-base">Recurring Progressions</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{progressions.slice(0, 10).map((p, i) => (
<div key={i} className="space-y-1">
<div className="flex items-center gap-2">
<div className="flex gap-1">
{p.progression.map((chord, j) => (
<span key={j} className="flex items-center gap-1">
<BadSimilarRecordings function · typescript · L14-L58 (45 LOC)src/components/insights/similar-recordings.tsx
export function SimilarRecordings({ pairs }: SimilarRecordingsProps) {
if (pairs.length === 0) {
return (
<Card>
<CardHeader>
<CardTitle className="text-base">Similar Recordings</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Analyze more recordings to discover similarities
</p>
</CardContent>
</Card>
);
}
return (
<Card>
<CardHeader>
<CardTitle className="text-base">Similar Recordings</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{pairs.map((pair, i) => (
<div key={i} className="rounded-lg border p-3 space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-sm font-medium">
<span className="truncate">{pair.pair[0]}</span>
<span className="text-muted-foreground">&</span>
Source: Repobility analyzer · https://repobility.com
SearchFilters function · typescript · L22-L143 (122 LOC)src/components/library/search-filters.tsx
export function SearchFilters() {
const router = useRouter();
const searchParams = useSearchParams();
const [query, setQuery] = useState(searchParams.get("q") ?? "");
const [keyFilter, setKeyFilter] = useState(searchParams.get("key") ?? "");
const [tempoRange, setTempoRange] = useState<[number, number]>([
parseInt(searchParams.get("tempoMin") ?? "0"),
parseInt(searchParams.get("tempoMax") ?? "300"),
]);
const [tagFilter, setTagFilter] = useState(searchParams.get("tag") ?? "");
const [tags, setTags] = useState<{ id: string; name: string; color: string }[]>([]);
useEffect(() => {
const supabase = createClient();
supabase.from("tags").select("*").order("name").then(({ data }) => {
if (data) setTags(data);
});
}, []);
function applyFilters() {
const params = new URLSearchParams();
if (query) params.set("q", query);
if (keyFilter) params.set("key", keyFilter);
if (tagFilter) params.set("tag", tagFilter);
if (tempoRange[0]applyFilters function · typescript · L41-L50 (10 LOC)src/components/library/search-filters.tsx
function applyFilters() {
const params = new URLSearchParams();
if (query) params.set("q", query);
if (keyFilter) params.set("key", keyFilter);
if (tagFilter) params.set("tag", tagFilter);
if (tempoRange[0] > 0) params.set("tempoMin", tempoRange[0].toString());
if (tempoRange[1] < 300) params.set("tempoMax", tempoRange[1].toString());
router.push(`/library?${params.toString()}`);
}clearFilters function · typescript · L52-L58 (7 LOC)src/components/library/search-filters.tsx
function clearFilters() {
setQuery("");
setKeyFilter("");
setTagFilter("");
setTempoRange([0, 300]);
router.push("/library");
}