feat: analysis providers, settings UI, song search, WAV duration fix
- 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>
This commit is contained in:
123
app/(web)/settings/page.tsx
Normal file
123
app/(web)/settings/page.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user