← back to kbarnoski__melody-memo

Function bodies 245 total

All specs Real LLM only Function bodies
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>

      {/* Sec
RawAnalysisDetails 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-sm
AnalysisDisplay 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: r
handleAnalyze 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: result
ChordTimeline 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
          clas
Generated 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; nam
timeToX 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 = gl
render 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(an
formatTime 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 round
ChatPanel 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}>
      <Dia
handleCreate 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 ? findSi
KeyDistribution 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("Fai
generateSummary 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">
                    <Bad
SimilarRecordings 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">&amp;</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");
  }
‹ prevpage 2 / 5next ›