- 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>
124 lines
3.9 KiB
TypeScript
124 lines
3.9 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState, useCallback } from "react";
|
|
import type { ProviderInfo } from "@/lib/analysis/providers";
|
|
import ProviderStatus from "@/components/settings/ProviderStatus";
|
|
import OllamaModelPicker from "@/components/settings/OllamaModelPicker";
|
|
import PreferencesPanel from "@/components/settings/PreferencesPanel";
|
|
|
|
const PROVIDER_KEY = "clicktrack_analysis_provider";
|
|
const MODEL_KEY = "clicktrack_ollama_model";
|
|
|
|
interface ProvidersResponse {
|
|
providers: ProviderInfo[];
|
|
ollamaModels: string[];
|
|
}
|
|
|
|
export default function SettingsPage() {
|
|
const [providers, setProviders] = useState<ProviderInfo[]>([]);
|
|
const [ollamaModels, setOllamaModels] = useState<string[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
const [defaultProvider, setDefaultProvider] = useState<string>("");
|
|
const [selectedOllamaModel, setSelectedOllamaModel] = useState<string>("");
|
|
|
|
const fetchProviders = useCallback(async (isRefresh = false) => {
|
|
if (isRefresh) setRefreshing(true);
|
|
try {
|
|
const res = await fetch("/api/analyze/providers");
|
|
const data = await res.json() as ProvidersResponse;
|
|
setProviders(data.providers);
|
|
setOllamaModels(data.ollamaModels);
|
|
|
|
// Initialise model selection
|
|
if (data.ollamaModels.length > 0) {
|
|
const saved = localStorage.getItem(MODEL_KEY);
|
|
if (saved && data.ollamaModels.includes(saved)) {
|
|
setSelectedOllamaModel(saved);
|
|
} else {
|
|
setSelectedOllamaModel(data.ollamaModels[0]);
|
|
}
|
|
}
|
|
} finally {
|
|
setLoading(false);
|
|
setRefreshing(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const saved = localStorage.getItem(PROVIDER_KEY);
|
|
if (saved) setDefaultProvider(saved);
|
|
fetchProviders();
|
|
}, [fetchProviders]);
|
|
|
|
function handleSetDefault(id: string) {
|
|
setDefaultProvider(id);
|
|
localStorage.setItem(PROVIDER_KEY, id);
|
|
}
|
|
|
|
function handleModelChange(model: string) {
|
|
setSelectedOllamaModel(model);
|
|
localStorage.setItem(MODEL_KEY, model);
|
|
}
|
|
|
|
const ollamaProvider = providers.find((p) => p.id === "ollama");
|
|
const ollamaAvailable = ollamaProvider?.available === true;
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="space-y-8">
|
|
<h1 className="text-2xl font-bold">Settings</h1>
|
|
<p className="text-zinc-500">Loading…</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-10 max-w-2xl">
|
|
<h1 className="text-2xl font-bold">Settings</h1>
|
|
|
|
{/* Analysis Providers */}
|
|
<section className="space-y-4">
|
|
<h2 className="text-lg font-semibold">Analysis Providers</h2>
|
|
<div className="space-y-3">
|
|
{providers.map((provider) => (
|
|
<ProviderStatus
|
|
key={provider.id}
|
|
provider={provider}
|
|
selectedOllamaModel={selectedOllamaModel}
|
|
isDefault={defaultProvider === provider.id}
|
|
onSetDefault={handleSetDefault}
|
|
/>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
{/* Ollama Models */}
|
|
{ollamaAvailable && (
|
|
<section className="space-y-4">
|
|
<h2 className="text-lg font-semibold">Ollama Models</h2>
|
|
<OllamaModelPicker
|
|
models={ollamaModels}
|
|
value={selectedOllamaModel}
|
|
onChange={handleModelChange}
|
|
onRefresh={() => fetchProviders(true)}
|
|
refreshing={refreshing}
|
|
/>
|
|
{ollamaProvider?.ollamaBaseUrl && (
|
|
<p className="text-xs text-zinc-600">
|
|
Base URL (operator-configured):{" "}
|
|
<code className="text-zinc-500">{ollamaProvider.ollamaBaseUrl}</code>
|
|
</p>
|
|
)}
|
|
</section>
|
|
)}
|
|
|
|
{/* Preferences */}
|
|
<section className="space-y-4">
|
|
<h2 className="text-lg font-semibold">Preferences</h2>
|
|
<PreferencesPanel />
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|