Files
clicktrack/components/TempoMapEditor.tsx
AJ Avezzano 7ba4381bff fix: TempoMapEditor crash on missing count_in + registry URL + Ollama schema
- Defensive default for count_in in TempoMapEditor prevents crash when AI omits field
- Fix hardcoded GitHub registry URL → git.avezzano.io/the_og/clicktrack-registry
- Add response_format json_schema to Ollama provider so count_in is always required

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 19:14:37 -04:00

135 lines
4.3 KiB
TypeScript

"use client";
/**
* TempoMapEditor
*
* Renders a read-only or editable view of a CTP document's sections.
* The full interactive editor (drag-to-resize bars, BPM knob, etc.)
* is a future milestone — this version is a structured table view
* with inline editing stubs.
*/
import type { CTPDocument, CTPSection } from "@/lib/ctp/schema";
import { isRampSection, sectionStartBpm } from "@/lib/ctp/schema";
interface TempoMapEditorProps {
ctpDoc: CTPDocument;
/** When true, all inputs are disabled. */
readOnly?: boolean;
onChange?: (doc: CTPDocument) => void;
}
function timeSigLabel(ts: CTPSection["time_signature"]): string {
return `${ts.numerator}/${ts.denominator}`;
}
function bpmLabel(section: CTPSection): string {
if (isRampSection(section)) {
return `${section.bpm_start}${section.bpm_end}`;
}
return String(section.bpm);
}
export default function TempoMapEditor({
ctpDoc,
readOnly = false,
}: TempoMapEditorProps) {
const {
metadata,
count_in = { enabled: false, bars: 2, use_first_section_tempo: true },
sections,
} = ctpDoc;
return (
<div className="space-y-5">
{/* Metadata strip */}
<div className="flex flex-wrap gap-x-6 gap-y-1 text-sm text-zinc-500">
<span>
Contributed by{" "}
<span className="text-zinc-300 font-medium">{metadata.contributed_by}</span>
</span>
{metadata.verified && (
<span className="text-green-400 font-medium"> Verified</span>
)}
<span>CTP v{ctpDoc.version}</span>
{metadata.mbid && (
<a
href={`https://musicbrainz.org/recording/${metadata.mbid}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:underline"
>
MusicBrainz
</a>
)}
</div>
{/* Count-in */}
<div className="flex items-center gap-3 text-sm">
<span className="text-zinc-500">Count-in:</span>
<span className="text-zinc-300">
{count_in.enabled ? `${count_in.bars} bar${count_in.bars !== 1 ? "s" : ""}` : "Disabled"}
</span>
</div>
{/* Sections table */}
<div className="overflow-x-auto rounded-lg border border-zinc-800">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-zinc-800 text-left text-xs uppercase tracking-widest text-zinc-600">
<th className="px-4 py-3">Label</th>
<th className="px-4 py-3">Start Bar</th>
<th className="px-4 py-3">BPM</th>
<th className="px-4 py-3">Time Sig</th>
<th className="px-4 py-3">Transition</th>
</tr>
</thead>
<tbody>
{sections.map((section, i) => (
<tr
key={i}
className="border-b border-zinc-800/60 last:border-0 hover:bg-zinc-800/30"
>
<td className="px-4 py-3 font-medium text-zinc-200">
{section.label}
</td>
<td className="px-4 py-3 text-zinc-400">{section.start_bar}</td>
<td className="px-4 py-3 font-mono text-green-400">
{bpmLabel(section)}
</td>
<td className="px-4 py-3 text-zinc-400">
{timeSigLabel(section.time_signature)}
</td>
<td className="px-4 py-3">
<span
className={`rounded-full px-2 py-0.5 text-xs font-medium ${
section.transition === "ramp"
? "bg-amber-900/40 text-amber-400"
: "bg-zinc-800 text-zinc-400"
}`}
>
{section.transition}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
{!readOnly && (
<p className="text-xs text-zinc-600">
Interactive editor coming soon contribute via the{" "}
<a
href="https://git.avezzano.io/the_og/clicktrack-registry"
className="text-blue-500 hover:underline"
>
community registry
</a>{" "}
in the meantime.
</p>
)}
</div>
);
}