Spaces:
Sleeping
Sleeping
nyk
feat: full i18n — 1752 keys across 10 languages, all panels translated (#326)
b180108 unverified | 'use client' | |
| import { useState, useEffect, useCallback } from 'react' | |
| import { useTranslations } from 'next-intl' | |
| import { Button } from '@/components/ui/button' | |
| type Tab = 'status' | 'health' | 'models' | 'apicall' | |
| export function DebugPanel() { | |
| const t = useTranslations('debug') | |
| const [activeTab, setActiveTab] = useState<Tab>('status') | |
| const tabLabels: Record<Tab, string> = { | |
| status: t('tabStatus'), | |
| health: t('tabHealth'), | |
| models: t('tabModels'), | |
| apicall: t('tabApiCall'), | |
| } | |
| return ( | |
| <div className="m-4"> | |
| <div className="flex gap-1 mb-4 border-b border-border pb-2"> | |
| {(['status', 'health', 'models', 'apicall'] as const).map((tab) => ( | |
| <Button | |
| key={tab} | |
| variant={activeTab === tab ? 'default' : 'ghost'} | |
| size="sm" | |
| onClick={() => setActiveTab(tab)} | |
| > | |
| {tabLabels[tab]} | |
| </Button> | |
| ))} | |
| </div> | |
| {activeTab === 'status' && <StatusTab />} | |
| {activeTab === 'health' && <HealthTab />} | |
| {activeTab === 'models' && <ModelsTab />} | |
| {activeTab === 'apicall' && <ApiCallTab />} | |
| </div> | |
| ) | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Status Tab | |
| // --------------------------------------------------------------------------- | |
| function StatusTab() { | |
| const t = useTranslations('debug') | |
| const [data, setData] = useState<any>(null) | |
| const [loading, setLoading] = useState(true) | |
| const fetchStatus = useCallback(async () => { | |
| setLoading(true) | |
| try { | |
| const res = await fetch('/api/debug?action=status') | |
| setData(await res.json()) | |
| } catch { | |
| setData({ error: 'Failed to fetch status' }) | |
| } finally { | |
| setLoading(false) | |
| } | |
| }, []) | |
| useEffect(() => { fetchStatus() }, [fetchStatus]) | |
| const reachable = data && !data.gatewayReachable === false && data.gatewayReachable !== false | |
| return ( | |
| <div> | |
| <div className="flex items-center gap-3 mb-3"> | |
| <span className="text-sm text-muted-foreground">{t('gateway')}:</span> | |
| {loading ? ( | |
| <span className="text-xs text-muted-foreground">{t('checking')}</span> | |
| ) : ( | |
| <span | |
| className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${ | |
| reachable | |
| ? 'bg-green-500/20 text-green-400 border border-green-500/30' | |
| : 'bg-red-500/20 text-red-400 border border-red-500/30' | |
| }`} | |
| > | |
| {reachable ? t('reachable') : t('unreachable')} | |
| </span> | |
| )} | |
| <Button variant="ghost" size="xs" onClick={fetchStatus} disabled={loading}> | |
| {t('refresh')} | |
| </Button> | |
| </div> | |
| <pre className="bg-secondary rounded-lg p-4 text-xs font-mono overflow-auto max-h-96 text-foreground"> | |
| {loading ? t('loading') : JSON.stringify(data, null, 2)} | |
| </pre> | |
| </div> | |
| ) | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Health Tab | |
| // --------------------------------------------------------------------------- | |
| function HealthTab() { | |
| const t = useTranslations('debug') | |
| const [data, setData] = useState<any>(null) | |
| const [loading, setLoading] = useState(true) | |
| const [heartbeat, setHeartbeat] = useState<{ ok: boolean; latencyMs: number; timestamp: number } | null>(null) | |
| const [hbLoading, setHbLoading] = useState(false) | |
| const fetchHealth = useCallback(async () => { | |
| setLoading(true) | |
| try { | |
| const res = await fetch('/api/debug?action=health') | |
| setData(await res.json()) | |
| } catch { | |
| setData({ healthy: false, error: 'Failed to fetch' }) | |
| } finally { | |
| setLoading(false) | |
| } | |
| }, []) | |
| useEffect(() => { fetchHealth() }, [fetchHealth]) | |
| const pingHeartbeat = async () => { | |
| setHbLoading(true) | |
| try { | |
| const res = await fetch('/api/debug?action=heartbeat') | |
| setHeartbeat(await res.json()) | |
| } catch { | |
| setHeartbeat({ ok: false, latencyMs: -1, timestamp: Date.now() }) | |
| } finally { | |
| setHbLoading(false) | |
| } | |
| } | |
| const healthy = data?.healthy === true || (data && !data.error && data.healthy !== false) | |
| return ( | |
| <div> | |
| <div className="flex items-center gap-3 mb-3"> | |
| <span className="text-sm text-muted-foreground">{t('health')}:</span> | |
| {loading ? ( | |
| <span className="text-xs text-muted-foreground">{t('checking')}</span> | |
| ) : ( | |
| <span | |
| className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${ | |
| healthy | |
| ? 'bg-green-500/20 text-green-400 border border-green-500/30' | |
| : 'bg-red-500/20 text-red-400 border border-red-500/30' | |
| }`} | |
| > | |
| {healthy ? t('healthy') : t('unhealthy')} | |
| </span> | |
| )} | |
| <Button variant="ghost" size="xs" onClick={fetchHealth} disabled={loading}> | |
| {t('refresh')} | |
| </Button> | |
| <Button variant="outline" size="xs" onClick={pingHeartbeat} disabled={hbLoading}> | |
| {hbLoading ? t('pinging') : t('heartbeat')} | |
| </Button> | |
| {heartbeat && ( | |
| <span className="text-xs text-muted-foreground"> | |
| {heartbeat.ok ? t('ok') : t('failed')} - {heartbeat.latencyMs}ms | |
| </span> | |
| )} | |
| </div> | |
| {data && !loading && ( | |
| <div className="bg-secondary rounded-lg p-4 text-xs overflow-auto max-h-96"> | |
| <table className="w-full text-left"> | |
| <tbody> | |
| {Object.entries(data).map(([key, value]) => ( | |
| <tr key={key} className="border-b border-border/50 last:border-0"> | |
| <td className="py-1 pr-4 font-medium text-muted-foreground whitespace-nowrap">{key}</td> | |
| <td className="py-1 font-mono text-foreground"> | |
| {typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)} | |
| </td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| )} | |
| </div> | |
| ) | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Models Tab | |
| // --------------------------------------------------------------------------- | |
| interface ModelEntry { | |
| name?: string | |
| id?: string | |
| provider?: string | |
| context_length?: number | |
| [key: string]: any | |
| } | |
| function ModelsTab() { | |
| const t = useTranslations('debug') | |
| const [data, setData] = useState<any>(null) | |
| const [loading, setLoading] = useState(true) | |
| const fetchModels = useCallback(async () => { | |
| setLoading(true) | |
| try { | |
| const res = await fetch('/api/debug?action=models') | |
| setData(await res.json()) | |
| } catch { | |
| setData({ models: [] }) | |
| } finally { | |
| setLoading(false) | |
| } | |
| }, []) | |
| useEffect(() => { fetchModels() }, [fetchModels]) | |
| const models: ModelEntry[] = Array.isArray(data?.models) ? data.models : (Array.isArray(data?.data) ? data.data : []) | |
| return ( | |
| <div> | |
| <div className="flex items-center gap-3 mb-3"> | |
| <span className="text-sm text-muted-foreground">{t('models')}</span> | |
| <Button variant="ghost" size="xs" onClick={fetchModels} disabled={loading}> | |
| {t('refresh')} | |
| </Button> | |
| </div> | |
| {loading ? ( | |
| <p className="text-xs text-muted-foreground">{t('loading')}</p> | |
| ) : models.length === 0 ? ( | |
| <p className="text-sm text-muted-foreground">{t('noModels')}</p> | |
| ) : ( | |
| <div className="bg-secondary rounded-lg overflow-auto max-h-96"> | |
| <table className="w-full text-xs text-left"> | |
| <thead> | |
| <tr className="border-b border-border"> | |
| <th className="py-2 px-3 font-medium text-muted-foreground">{t('colName')}</th> | |
| <th className="py-2 px-3 font-medium text-muted-foreground">{t('colProvider')}</th> | |
| <th className="py-2 px-3 font-medium text-muted-foreground">{t('colContextLength')}</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {models.map((m, i) => ( | |
| <tr key={m.id || m.name || i} className="border-b border-border/50 last:border-0"> | |
| <td className="py-1.5 px-3 font-mono text-foreground">{m.name || m.id || '?'}</td> | |
| <td className="py-1.5 px-3 text-muted-foreground">{m.provider || '-'}</td> | |
| <td className="py-1.5 px-3 text-muted-foreground">{m.context_length ?? '-'}</td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| )} | |
| </div> | |
| ) | |
| } | |
| // --------------------------------------------------------------------------- | |
| // API Call Tab | |
| // --------------------------------------------------------------------------- | |
| function ApiCallTab() { | |
| const t = useTranslations('debug') | |
| const [method, setMethod] = useState<'GET' | 'POST'>('GET') | |
| const [path, setPath] = useState('/api/') | |
| const [body, setBody] = useState('') | |
| const [response, setResponse] = useState<any>(null) | |
| const [loading, setLoading] = useState(false) | |
| const send = async () => { | |
| setLoading(true) | |
| setResponse(null) | |
| try { | |
| let parsedBody: any = undefined | |
| if (method === 'POST' && body.trim()) { | |
| try { | |
| parsedBody = JSON.parse(body) | |
| } catch { | |
| setResponse({ error: 'Invalid JSON in body' }) | |
| setLoading(false) | |
| return | |
| } | |
| } | |
| const res = await fetch('/api/debug?action=call', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ method, path, body: parsedBody }), | |
| }) | |
| setResponse(await res.json()) | |
| } catch { | |
| setResponse({ error: 'Request failed' }) | |
| } finally { | |
| setLoading(false) | |
| } | |
| } | |
| return ( | |
| <div> | |
| <div className="flex items-end gap-2 mb-4"> | |
| <div> | |
| <label className="block text-xs text-muted-foreground mb-1">{t('method')}</label> | |
| <select | |
| value={method} | |
| onChange={(e) => setMethod(e.target.value as 'GET' | 'POST')} | |
| className="h-8 px-2 rounded border border-border bg-secondary text-foreground text-sm" | |
| > | |
| <option value="GET">GET</option> | |
| <option value="POST">POST</option> | |
| </select> | |
| </div> | |
| <div className="flex-1"> | |
| <label className="block text-xs text-muted-foreground mb-1">{t('path')}</label> | |
| <input | |
| type="text" | |
| value={path} | |
| onChange={(e) => setPath(e.target.value)} | |
| placeholder="/api/" | |
| className="h-8 w-full px-2 rounded border border-border bg-secondary text-foreground text-sm font-mono" | |
| /> | |
| </div> | |
| <Button variant="default" size="sm" onClick={send} disabled={loading}> | |
| {loading ? t('sending') : t('send')} | |
| </Button> | |
| </div> | |
| {method === 'POST' && ( | |
| <div className="mb-4"> | |
| <label className="block text-xs text-muted-foreground mb-1">{t('bodyJson')}</label> | |
| <textarea | |
| value={body} | |
| onChange={(e) => setBody(e.target.value)} | |
| rows={5} | |
| placeholder='{"key": "value"}' | |
| className="w-full px-3 py-2 rounded border border-border bg-secondary text-foreground text-xs font-mono resize-y" | |
| /> | |
| </div> | |
| )} | |
| {response && ( | |
| <div> | |
| {response.status && ( | |
| <div className="flex items-center gap-2 mb-2"> | |
| <span className="text-xs text-muted-foreground">{t('statusLabel')}:</span> | |
| <span | |
| className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${ | |
| response.status >= 200 && response.status < 300 | |
| ? 'bg-green-500/20 text-green-400 border border-green-500/30' | |
| : response.status >= 400 | |
| ? 'bg-red-500/20 text-red-400 border border-red-500/30' | |
| : 'bg-yellow-500/20 text-yellow-400 border border-yellow-500/30' | |
| }`} | |
| > | |
| {response.status} {response.statusText} | |
| </span> | |
| {response.contentType && ( | |
| <span className="text-xs text-muted-foreground">{response.contentType}</span> | |
| )} | |
| </div> | |
| )} | |
| <pre className="bg-secondary rounded-lg p-4 text-xs font-mono overflow-auto max-h-96 text-foreground"> | |
| {typeof response.body !== 'undefined' | |
| ? (typeof response.body === 'string' ? response.body : JSON.stringify(response.body, null, 2)) | |
| : JSON.stringify(response, null, 2)} | |
| </pre> | |
| </div> | |
| )} | |
| </div> | |
| ) | |
| } | |