"use client"; import { useParams, notFound } from "next/navigation"; import Link from "next/link"; import { useState, useEffect } from "react"; import { Card, Badge } from "@/shared/components"; import ProviderIcon from "@/shared/components/ProviderIcon"; import { MEDIA_PROVIDER_KINDS, AI_PROVIDERS, getProviderAlias } from "@/shared/constants/providers"; import { getModelsByProviderId } from "@/shared/constants/models"; import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; import ConnectionsCard from "@/app/(dashboard)/dashboard/providers/components/ConnectionsCard"; import ModelsCard from "@/app/(dashboard)/dashboard/providers/components/ModelsCard"; import { TTS_PROVIDER_CONFIG } from "@/shared/constants/ttsProviders"; // Shared row layout — defined outside components to avoid re-mount on re-render function Row({ label, children }) { return (
{label}
{children}
); } const DEFAULT_RESPONSE_EXAMPLE = `{ "object": "list", "data": [{ "object": "embedding", "index": 0, "embedding": [0.002301, -0.019212, 0.004815, -0.031249, ...] }], "model": "...", "usage": { "prompt_tokens": 9, "total_tokens": 9 } }`; // EmbeddingExampleCard function EmbeddingExampleCard({ providerId }) { const providerAlias = getProviderAlias(providerId); const embeddingModels = getModelsByProviderId(providerId).filter((m) => m.type === "embedding"); const [selectedModel, setSelectedModel] = useState(embeddingModels[0]?.id ?? ""); const [input, setInput] = useState("The quick brown fox jumps over the lazy dog"); const [apiKey, setApiKey] = useState(""); const [useTunnel, setUseTunnel] = useState(false); const [localEndpoint, setLocalEndpoint] = useState(""); const [tunnelEndpoint, setTunnelEndpoint] = useState(""); const [result, setResult] = useState(null); const [running, setRunning] = useState(false); const [error, setError] = useState(""); const { copied: copiedCurl, copy: copyCurl } = useCopyToClipboard(); const { copied: copiedRes, copy: copyRes } = useCopyToClipboard(); useEffect(() => { setLocalEndpoint(window.location.origin); fetch("/api/keys") .then((r) => r.json()) .then((d) => { setApiKey((d.keys || []).find((k) => k.isActive !== false)?.key || ""); }) .catch(() => {}); fetch("/api/tunnel/status") .then((r) => r.json()) .then((d) => { if (d.publicUrl) setTunnelEndpoint(d.publicUrl); }) .catch(() => {}); }, []); const endpoint = useTunnel ? tunnelEndpoint : localEndpoint; const modelFull = selectedModel ? `${providerAlias}/${selectedModel}` : ""; const curlSnippet = `curl -X POST ${endpoint}/v1/embeddings \\ -H "Content-Type: application/json" \\ -H "Authorization: Bearer ${apiKey || "YOUR_KEY"}" \\ -d '{"model": "${modelFull}", "input": "${input}"}'`; const handleRun = async () => { if (!input.trim() || !modelFull) return; setRunning(true); setError(""); setResult(null); const start = Date.now(); try { const headers = { "Content-Type": "application/json" }; if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`; const res = await fetch("/api/v1/embeddings", { method: "POST", headers, body: JSON.stringify({ model: modelFull, input: input.trim() }), }); const latencyMs = Date.now() - start; const data = await res.json(); if (!res.ok) { setError(data?.error?.message || data?.error || `HTTP ${res.status}`); return; } setResult({ data, latencyMs }); } catch (e) { setError(e.message || "Network error"); } finally { setRunning(false); } }; // Compact embedding array: first 4 values + count const formatResultJson = (data) => { if (!data) return DEFAULT_RESPONSE_EXAMPLE; const clone = JSON.parse(JSON.stringify(data)); (clone.data || []).forEach((item) => { if (Array.isArray(item.embedding) && item.embedding.length > 4) { item.embedding = [...item.embedding.slice(0, 4).map((v) => parseFloat(v.toFixed(6))), `... (${item.embedding.length} dims)`]; } }); return JSON.stringify(clone, null, 2); }; const resultJson = result ? JSON.stringify(result.data, null, 2) : ""; return (

Example

{/* Model */} {/* Endpoint */}
useTunnel ? setTunnelEndpoint(e.target.value) : setLocalEndpoint(e.target.value)} className="flex-1 px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary font-mono" placeholder="http://localhost:3000" /> {/* Tunnel toggle — only show if tunnel URL is available */} {tunnelEndpoint && ( )}
{/* API Key */} setApiKey(e.target.value)} placeholder="sk-..." className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary font-mono" /> {/* Input */}
setInput(e.target.value)} className="w-full px-3 py-1.5 pr-7 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" /> {input && ( )}
{/* Curl + Run */}
Request
{curlSnippet}
{/* Error */} {error &&

{error}

} {/* Response — default example or real result */}
Response {result && ⚡ {result.latencyMs}ms} {result && ( )}
            {formatResultJson(result?.data)}
          
); } // ─── TTS Example Card ──────────────────────────────────────────────────────── function TtsExampleCard({ providerId }) { const providerAlias = getProviderAlias(providerId); const config = TTS_PROVIDER_CONFIG[providerId] || TTS_PROVIDER_CONFIG["edge-tts"]; // Voice state const [selectedVoice, setSelectedVoice] = useState(""); const [selectedVoiceName, setSelectedVoiceName] = useState(""); const [voiceId, setVoiceId] = useState(""); // editable voice id (elevenlabs) // Voices shown below Voice row after language selected const [countryVoices, setCountryVoices] = useState([]); const [selectedLang, setSelectedLang] = useState(""); const [selectedModel, setSelectedModel] = useState(() => { if (config.hasModelSelector && config.modelKey) { const models = getModelsByProviderId(config.modelKey); return models?.[0]?.id || ""; } return ""; }); // Form state const [input, setInput] = useState("Hello, this is a text to speech test."); const [apiKey, setApiKey] = useState(""); const [useTunnel, setUseTunnel] = useState(false); const [localEndpoint, setLocalEndpoint] = useState(""); const [tunnelEndpoint, setTunnelEndpoint] = useState(""); const [responseFormat, setResponseFormat] = useState("mp3"); // mp3 | json const [audioUrl, setAudioUrl] = useState(""); const [jsonResponse, setJsonResponse] = useState(null); // Store JSON response const [running, setRunning] = useState(false); const [error, setError] = useState(""); const [latency, setLatency] = useState(null); const { copied: copiedCurl, copy: copyCurl } = useCopyToClipboard(); // Country picker modal state const [modalOpen, setModalOpen] = useState(false); const [languages, setLanguages] = useState([]); const [modalLoading, setModalLoading] = useState(false); const [modalSearch, setModalSearch] = useState(""); const [modalError, setModalError] = useState(""); const [byLang, setByLang] = useState({}); useEffect(() => { setLocalEndpoint(window.location.origin); fetch("/api/keys") .then((r) => r.json()) .then((d) => { setApiKey((d.keys || []).find((k) => k.isActive !== false)?.key || ""); }) .catch(() => {}); fetch("/api/tunnel/status") .then((r) => r.json()) .then((d) => { if (d.publicUrl) setTunnelEndpoint(d.publicUrl); }) .catch(() => {}); // Pre-select default voice based on provider config if (config.voiceSource === "hardcoded") { const voiceKey = config.voiceKey || providerId; const voices = getModelsByProviderId(voiceKey).filter((m) => m.type === "tts"); if (voices.length) { if (config.hasLanguageDropdown) { // Google TTS: just set voice setSelectedVoice(voices[0].id); setSelectedVoiceName(voices[0].name || voices[0].id); } else { // OpenAI: set voice chips setCountryVoices(voices); setSelectedVoice(voices[0].id); setSelectedVoiceName(voices[0].name || voices[0].id); } } } // api-language (edge-tts, local-device, elevenlabs): NO default load, wait for user to pick language }, [providerId]); // Open modal — load language list const openModal = async () => { setModalOpen(true); setModalSearch(""); setModalError(""); if (languages.length) return; // already loaded setModalLoading(true); try { // Use provider-specific apiEndpoint if available, else default to edge-tts voices API const url = config.apiEndpoint ? config.apiEndpoint : `/api/media-providers/tts/voices?provider=${providerId === "local-device" ? "local-device" : "edge-tts"}`; const r = await fetch(url); const d = await r.json(); if (d.error) { setModalError(d.error); return; } setLanguages(d.languages || []); setByLang(d.byLang || {}); } catch (e) { setModalError(e.message); } finally { setModalLoading(false); } }; // Click language → close modal → show voices below const handlePickLanguage = (lang) => { setModalOpen(false); setSelectedLang(lang.code); const voices = byLang[lang.code]?.voices || []; setCountryVoices(voices); // Auto-select first voice if (voices.length) { setSelectedVoice(voices[0].id); setSelectedVoiceName(voices[0].name); if (config.hasVoiceIdInput) setVoiceId(voices[0].id); } }; const filteredLanguages = modalSearch ? languages.filter((c) => c.name.toLowerCase().includes(modalSearch.toLowerCase()) || c.code.toLowerCase().includes(modalSearch.toLowerCase()) ) : languages; const endpoint = useTunnel ? tunnelEndpoint : localEndpoint; // For ElevenLabs: use voiceId (editable) instead of selectedVoice const activeVoiceId = config.hasVoiceIdInput ? voiceId : selectedVoice; const modelFull = config.hasModelSelector && activeVoiceId && selectedModel ? `${providerAlias}/${selectedModel}/${activeVoiceId}` : activeVoiceId ? `${providerAlias}/${activeVoiceId}` : ""; const curlSnippet = `curl -X POST ${endpoint}/v1/audio/speech${responseFormat === "json" ? "?response_format=json" : ""} \\ -H "Content-Type: application/json" \\ -H "Authorization: Bearer ${apiKey || "YOUR_KEY"}" \\ -d '{"model": "${modelFull}", "input": "${input}"}' \\ ${responseFormat === "json" ? "" : "--output speech.mp3"}`; const handleRun = async () => { if (!input.trim() || !modelFull) return; setRunning(true); setError(""); setAudioUrl(""); setJsonResponse(null); const start = Date.now(); try { const headers = { "Content-Type": "application/json" }; if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`; const url = `/api/v1/audio/speech${responseFormat === "json" ? "?response_format=json" : ""}`; const res = await fetch(url, { method: "POST", headers, body: JSON.stringify({ model: modelFull, input: input.trim() }), }); setLatency(Date.now() - start); if (!res.ok) { const d = await res.json().catch(() => ({})); setError(d?.error?.message || d?.error || `HTTP ${res.status}`); return; } if (responseFormat === "json") { const data = await res.json(); setJsonResponse(data); // Store full JSON response const audioBlob = await fetch(`data:audio/mp3;base64,${data.audio}`).then(r => r.blob()); setAudioUrl(URL.createObjectURL(audioBlob)); } else { const blob = await res.blob(); setAudioUrl(URL.createObjectURL(blob)); } } catch (e) { setError(e.message || "Network error"); } finally { setRunning(false); } }; return ( <>

Example

{/* Endpoint + API Key as read-only text */}
{endpoint}/v1/audio/speech {tunnelEndpoint && ( )}
{apiKey ? `${apiKey.slice(0, 8)}${"•".repeat(Math.min(20, apiKey.length - 8))}` : No key configured} {/* Model selector (OpenAI, ElevenLabs) */} {config.hasModelSelector && config.modelKey && ( )} {/* Language row + Browse button (edge-tts, local-device, elevenlabs) */} {config.hasBrowseButton && (
)} {/* Voice chips — shown after language picked (edge-tts, local-device) or always (OpenAI/ElevenLabs) */} {countryVoices.length > 0 && (
{countryVoices.map((v) => ( ))}
)} {/* Voice ID input (ElevenLabs) — manual entry or auto-fill from chip */} {config.hasVoiceIdInput && (
{ setVoiceId(e.target.value); setSelectedVoice(e.target.value); }} placeholder="e.g. CwhRBWXzGAHq8TQ4Fs17" className="w-full px-3 py-1.5 pr-7 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary font-mono" /> {voiceId && ( )}
)} {/* Google TTS: Language dropdown */} {config.hasLanguageDropdown && ( )} {/* Input */}
setInput(e.target.value)} className="w-full px-3 py-1.5 pr-7 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" /> {input && ( )}
{/* Output Format */} {/* Curl + Run */}
Request
{curlSnippet}
{error &&

{error}

} {/* Audio player */} {audioUrl ? (
Response {latency && ⚡ {latency}ms} download Download
) : (

Audio will appear here after running.

)}
{/* Country Picker Modal */} {modalOpen && (
setModalOpen(false)} >
e.stopPropagation()} > {/* Header */}

Select Language

{/* Search */}
setModalSearch(e.target.value)} placeholder="Search language..." className="w-full px-3 py-1.5 text-sm border border-border rounded-lg bg-background focus:outline-none focus:border-primary" />
{/* Language list */}
{modalError &&

{modalError}

} {modalLoading ? (

Loading...

) : (
{filteredLanguages.map((c) => ( ))} {filteredLanguages.length === 0 && (

No languages found.

)}
)}
)} ); } // MediaProviderDetailPage export default function MediaProviderDetailPage() { const { kind, id } = useParams(); const kindConfig = MEDIA_PROVIDER_KINDS.find((k) => k.id === kind); if (!kindConfig) return notFound(); const provider = AI_PROVIDERS[id]; if (!provider) return notFound(); const kinds = provider.serviceKinds ?? ["llm"]; if (!kinds.includes(kind)) return notFound(); return (
{/* Back */}
arrow_back {kindConfig.label} {/* Header */}

{provider.name}

{kinds.map((k) => ( {k.toUpperCase()} ))}
{/* Connections */} {provider.noAuth ? (
lock_open

No authentication required

This provider is ready to use.

) : ( )} {/* Models - only for non-tts kinds */} {kind !== "tts" && } {/* Example — per kind */} {kind === "embedding" && } {kind === "tts" && }
); }