- Restructured docker-compose for Nginx Proxy Manager (no custom nginx) - Added domain wordly.art configuration - Added Prometheus + Grafana monitoring stack with pre-configured dashboards - Added PostgreSQL backup script to NAS (daily/weekly/monthly rotation) - Added alert rules for backend, system, and Docker metrics - Updated deployment guide for NPM + IONOS DNS homelab setup - Added marketing plan document - PDF translator and watermark support - Enhanced middleware, routes, and translator modules Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
153 lines
5.7 KiB
TypeScript
153 lines
5.7 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Zap, CheckCircle2, Lock, Loader2, Globe, Brain } from 'lucide-react';
|
|
import { API_BASE } from '@/lib/config';
|
|
import { useI18n } from '@/lib/i18n';
|
|
|
|
const FALLBACK_PROVIDERS = [
|
|
{ id: "google", label: "Google Traduction", description: "Traduction rapide, 130+ langues", mode: "classic" as const },
|
|
];
|
|
|
|
interface AvailableProvider {
|
|
id: string;
|
|
label: string;
|
|
description: string;
|
|
mode: "classic" | "llm";
|
|
model?: string;
|
|
}
|
|
|
|
export default function TranslationServicesPage() {
|
|
const { t } = useI18n();
|
|
const [providers, setProviders] = useState<AvailableProvider[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
const fetchProviders = async () => {
|
|
try {
|
|
const token = localStorage.getItem("token");
|
|
const headers: Record<string, string> = {};
|
|
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
const res = await fetch(`${API_BASE}/api/v1/providers/available`, { headers });
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
const list = data.providers || [];
|
|
setProviders(list.length > 0 ? list : FALLBACK_PROVIDERS);
|
|
} else {
|
|
setProviders(FALLBACK_PROVIDERS);
|
|
}
|
|
} catch {
|
|
setProviders(FALLBACK_PROVIDERS);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
fetchProviders();
|
|
}, []);
|
|
|
|
const classicProviders = providers.filter((p) => p.mode === "classic");
|
|
const llmProviders = providers.filter((p) => p.mode === "llm");
|
|
|
|
return (
|
|
<div className="flex flex-col gap-6 p-6 lg:p-8 max-w-2xl">
|
|
<div>
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<Zap className="h-5 w-5 text-primary" />
|
|
<h1 className="text-2xl font-bold text-foreground">{t('services.title')}</h1>
|
|
</div>
|
|
<p className="text-sm text-muted-foreground">
|
|
{t('services.subtitle')}
|
|
</p>
|
|
</div>
|
|
|
|
{isLoading ? (
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
<Loader2 className="size-4 animate-spin" />
|
|
<span>{t('services.loading')}</span>
|
|
</div>
|
|
) : providers.length === 0 ? (
|
|
<Card>
|
|
<CardContent className="py-8 text-center text-sm text-muted-foreground">
|
|
{t('services.noProviders')}
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<div className="space-y-6">
|
|
{classicProviders.length > 0 && (
|
|
<div className="space-y-3">
|
|
<div className="flex items-center gap-2">
|
|
<Globe className="size-4 text-muted-foreground" />
|
|
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
|
|
{t('services.classic')}
|
|
</h2>
|
|
</div>
|
|
<div className="space-y-2">
|
|
{classicProviders.map((p) => (
|
|
<Card key={p.id}>
|
|
<CardContent className="flex items-center justify-between py-4 px-5">
|
|
<div>
|
|
<p className="font-medium text-foreground">{p.label}</p>
|
|
<p className="text-xs text-muted-foreground">{p.description}</p>
|
|
</div>
|
|
<Badge variant="outline" className="border-emerald-500/50 text-emerald-600 gap-1">
|
|
<CheckCircle2 className="size-3" />{t('services.available')}
|
|
</Badge>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{llmProviders.length > 0 && (
|
|
<div className="space-y-3">
|
|
<div className="flex items-center gap-2">
|
|
<Brain className="size-4 text-muted-foreground" />
|
|
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
|
|
{t('services.llmPro')}
|
|
</h2>
|
|
</div>
|
|
<div className="space-y-2">
|
|
{llmProviders.map((p) => (
|
|
<Card key={p.id}>
|
|
<CardContent className="flex items-center justify-between py-4 px-5">
|
|
<div>
|
|
<p className="font-medium text-foreground">{p.label}</p>
|
|
<p className="text-xs text-muted-foreground">{p.description}</p>
|
|
{p.model && (
|
|
<p className="mt-1 text-[10px] font-mono text-muted-foreground/80">
|
|
{t('services.model')}: {p.model}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<Badge variant="outline" className="border-emerald-500/50 text-emerald-600 gap-1">
|
|
<CheckCircle2 className="size-3" />{t('services.available')}
|
|
</Badge>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<Card className="border-amber-200 dark:border-amber-500/20 bg-amber-50 dark:bg-amber-500/5">
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm text-amber-800 dark:text-amber-400 flex items-center gap-2">
|
|
<Lock className="size-4" />
|
|
{t('services.adminOnly.title')}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<CardDescription className="text-amber-700 dark:text-amber-400/80 text-xs">
|
|
{t('services.adminOnly.desc')}
|
|
</CardDescription>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|