- Multi-provider AI analysis (Anthropic, OpenAI, Ollama, Algorithmic) - server-only guards on all provider files; client bundle fix - /settings page with provider status, Ollama model picker, preferences - Song search box on /analyze replacing raw MBID input (debounced, keyboard nav) - Auto-register song via MusicBrainz on POST /api/tracks (no more 404) - Fix WAV duration bug: last section songEnd was double-counting elapsed time - Registry sync comment updated for self-hosted HTTPS git servers Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
115 lines
3.8 KiB
TypeScript
115 lines
3.8 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import type { ProviderInfo } from "@/lib/analysis/providers";
|
|
|
|
interface ProviderStatusProps {
|
|
provider: ProviderInfo;
|
|
selectedOllamaModel?: string;
|
|
isDefault: boolean;
|
|
onSetDefault: (id: string) => void;
|
|
}
|
|
|
|
const TYPE_LABELS: Record<ProviderInfo["type"], string> = {
|
|
"cloud-ai": "Cloud AI",
|
|
"local-ai": "Local AI",
|
|
"algorithmic": "No AI",
|
|
};
|
|
|
|
export default function ProviderStatus({
|
|
provider,
|
|
selectedOllamaModel,
|
|
isDefault,
|
|
onSetDefault,
|
|
}: ProviderStatusProps) {
|
|
const [testStatus, setTestStatus] = useState<"idle" | "testing" | "ok" | "error">("idle");
|
|
const [testError, setTestError] = useState("");
|
|
|
|
async function handleTest() {
|
|
setTestStatus("testing");
|
|
setTestError("");
|
|
try {
|
|
const res = await fetch("/api/analyze/test", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
provider: provider.id,
|
|
ollamaModel: provider.id === "ollama" ? selectedOllamaModel : undefined,
|
|
}),
|
|
});
|
|
const data = await res.json() as { ok: boolean; error?: string };
|
|
if (data.ok) {
|
|
setTestStatus("ok");
|
|
} else {
|
|
setTestStatus("error");
|
|
setTestError(data.error ?? "Unknown error");
|
|
}
|
|
} catch (err) {
|
|
setTestStatus("error");
|
|
setTestError(err instanceof Error ? err.message : String(err));
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="rounded-lg border border-zinc-800 bg-zinc-900/60 p-4">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<span className="font-medium text-zinc-200">{provider.label}</span>
|
|
<span className="rounded px-1.5 py-0.5 text-xs bg-zinc-800 text-zinc-400">
|
|
{TYPE_LABELS[provider.type]}
|
|
</span>
|
|
{provider.available ? (
|
|
<span className="rounded px-1.5 py-0.5 text-xs bg-green-900/50 text-green-400">
|
|
Available
|
|
</span>
|
|
) : (
|
|
<span className="rounded px-1.5 py-0.5 text-xs bg-zinc-800 text-zinc-500">
|
|
Unavailable
|
|
</span>
|
|
)}
|
|
{isDefault && (
|
|
<span className="rounded px-1.5 py-0.5 text-xs bg-blue-900/50 text-blue-400">
|
|
Default
|
|
</span>
|
|
)}
|
|
</div>
|
|
{!provider.available && provider.unavailableReason && (
|
|
<p className="mt-1 text-xs text-zinc-600">{provider.unavailableReason}</p>
|
|
)}
|
|
{provider.id === "ollama" && provider.available && provider.ollamaBaseUrl && (
|
|
<p className="mt-1 text-xs text-zinc-600 font-mono">{provider.ollamaBaseUrl}</p>
|
|
)}
|
|
</div>
|
|
|
|
{provider.available && (
|
|
<div className="flex items-center gap-2 shrink-0">
|
|
{!isDefault && (
|
|
<button
|
|
onClick={() => onSetDefault(provider.id)}
|
|
className="text-xs text-zinc-500 hover:text-zinc-300 underline"
|
|
>
|
|
Set as default
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={handleTest}
|
|
disabled={testStatus === "testing"}
|
|
className="rounded px-3 py-1 text-xs font-medium border border-zinc-700 text-zinc-300 hover:border-zinc-500 hover:text-zinc-100 transition-colors disabled:opacity-50"
|
|
>
|
|
{testStatus === "testing" ? "Testing…" : "Test"}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{testStatus === "ok" && (
|
|
<p className="mt-2 text-xs text-green-400">✓ Working</p>
|
|
)}
|
|
{testStatus === "error" && (
|
|
<p className="mt-2 text-xs text-red-400">✗ {testError}</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|