mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-20 14:12:59 +03:00
feat: complete i18n migration — 21 pages/components translated + PT-BR README section
- Full en.json and pt-BR.json with 26 namespaces (~460 strings) - Migrated: HomePageClient, Analytics, Translator (4 components), Usage (5 components), ProviderLimits, Shared (UsageStats, TokenHealthBadge, SystemMonitor, ConsoleLogViewer, Footer), Login, Callback, ForgotPassword, Forbidden - README: added PT-BR section with quick start, features, i18n docs - README: added language switcher badges at top
This commit is contained in:
81
README.md
81
README.md
@@ -3,6 +3,8 @@
|
||||
|
||||
# 🚀 OmniRoute — The Free AI Gateway
|
||||
|
||||
🌐 **[English](#-omniroute--the-free-ai-gateway)** | **[Português (BR)](#-omniroute--gateway-de-ia-gratuito)**
|
||||
|
||||
### Never stop coding. Smart routing to **FREE & low-cost AI models** with automatic fallback.
|
||||
|
||||
_Your universal API proxy — one endpoint, 36+ providers, zero downtime._
|
||||
@@ -1145,6 +1147,85 @@ MIT License - see [LICENSE](LICENSE) for details.
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 🇧🇷 OmniRoute — Gateway de IA Gratuito
|
||||
|
||||
<a name="-omniroute--gateway-de-ia-gratuito"></a>
|
||||
|
||||
### Nunca pare de codar. Roteamento inteligente para **modelos de IA GRATUITOS e de baixo custo** com fallback automático.
|
||||
|
||||
_Seu proxy universal de API — um endpoint, 36+ provedores, zero downtime._
|
||||
|
||||
### 🌐 Internacionalização (i18n)
|
||||
|
||||
O dashboard do OmniRoute suporta **múltiplos idiomas**. Atualmente disponível em:
|
||||
|
||||
| Idioma | Código | Status |
|
||||
| --------------------- | ------- | ----------- |
|
||||
| 🇺🇸 English | `en` | ✅ Completo |
|
||||
| 🇧🇷 Português (Brasil) | `pt-BR` | ✅ Completo |
|
||||
|
||||
**Para trocar o idioma:** Clique no seletor de idioma (🇺🇸 EN) no header do dashboard → selecione o idioma desejado.
|
||||
|
||||
**Para adicionar um novo idioma:**
|
||||
|
||||
1. Crie `src/i18n/messages/{codigo}.json` baseado em `en.json`
|
||||
2. Adicione o código em `src/i18n/config.ts` → `LOCALES` e `LANGUAGES`
|
||||
3. Reinicie o servidor
|
||||
|
||||
### ⚡ Início Rápido
|
||||
|
||||
```bash
|
||||
# Instalar via npm
|
||||
npx omniroute@latest
|
||||
|
||||
# Ou rodar do código-fonte
|
||||
cp .env.example .env
|
||||
npm install
|
||||
PORT=20128 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run dev
|
||||
```
|
||||
|
||||
### 🐳 Docker
|
||||
|
||||
```bash
|
||||
docker run -d --name omniroute -p 20128:20128 diegosouzapw/omniroute:latest
|
||||
```
|
||||
|
||||
### 🔑 Funcionalidades Principais
|
||||
|
||||
- **36+ provedores de IA** — Claude, GPT, Gemini, Llama, Qwen, DeepSeek, e mais
|
||||
- **Roteamento inteligente** — Fallback automático entre provedores
|
||||
- **Tradução de formato** — OpenAI ↔ Claude ↔ Gemini automaticamente
|
||||
- **Multi-conta** — Múltiplas contas por provedor com seleção inteligente
|
||||
- **Cache semântico** — Reduz custos e latência
|
||||
- **OAuth automático** — Tokens renovam automaticamente
|
||||
- **Combos personalizados** — 6 estratégias de roteamento
|
||||
- **Dashboard completo** — Monitoramento, logs, análises, configurações
|
||||
- **CLI Tools** — Configure Claude Code, Codex, Cursor, Cline com um clique
|
||||
- **100% TypeScript** — Código limpo e tipado
|
||||
|
||||
### 📖 Documentação
|
||||
|
||||
| Documento | Descrição |
|
||||
| ----------------------------------------------- | -------------------------------------- |
|
||||
| [Guia do Usuário](docs/USER_GUIDE.md) | Provedores, combos, CLI, deploy |
|
||||
| [Referência da API](docs/API_REFERENCE.md) | Todos os endpoints com exemplos |
|
||||
| [Solução de Problemas](docs/TROUBLESHOOTING.md) | Problemas comuns e soluções |
|
||||
| [Arquitetura](docs/ARCHITECTURE.md) | Arquitetura e internos do sistema |
|
||||
| [Contribuição](CONTRIBUTING.md) | Setup de desenvolvimento e guidelines |
|
||||
| [Deploy em VM](docs/VM_DEPLOYMENT_GUIDE.md) | Guia completo: VM + nginx + Cloudflare |
|
||||
|
||||
### 📧 Suporte
|
||||
|
||||
> 💬 **Entre para a comunidade!** [Grupo WhatsApp](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) — Tire dúvidas, compartilhe dicas e fique atualizado.
|
||||
|
||||
- **Website**: [omniroute.online](https://omniroute.online)
|
||||
- **GitHub**: [github.com/diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
|
||||
- **Issues**: [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<sub>Built with ❤️ for developers who code 24/7</sub>
|
||||
<br/>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { useState, useEffect, useMemo, useCallback } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Image from "next/image";
|
||||
@@ -10,6 +12,8 @@ import { AI_PROVIDERS, FREE_PROVIDERS, OAUTH_PROVIDERS } from "@/shared/constant
|
||||
import { useNotificationStore } from "@/store/notificationStore";
|
||||
|
||||
export default function HomePageClient({ machineId }) {
|
||||
const t = useTranslations("home");
|
||||
const tc = useTranslations("common");
|
||||
const [providerConnections, setProviderConnections] = useState([]);
|
||||
const [models, setModels] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -103,14 +107,14 @@ export default function HomePageClient({ machineId }) {
|
||||
}, [selectedProvider, models]);
|
||||
|
||||
const quickStartLinks = [
|
||||
{ label: "Documentation", href: "/docs", icon: "menu_book" },
|
||||
{ label: "Providers", href: "/dashboard/providers", icon: "dns" },
|
||||
{ label: t("documentation"), href: "/docs", icon: "menu_book" },
|
||||
{ label: tc("provider") + "s", href: "/dashboard/providers", icon: "dns" },
|
||||
{ label: "Combos", href: "/dashboard/combos", icon: "layers" },
|
||||
{ label: "Analytics", href: "/dashboard/analytics", icon: "analytics" },
|
||||
{ label: "Health Monitor", href: "/dashboard/health", icon: "health_and_safety" },
|
||||
{ label: t("healthMonitor"), href: "/dashboard/health", icon: "health_and_safety" },
|
||||
{ label: "CLI Tools", href: "/dashboard/cli-tools", icon: "terminal" },
|
||||
{
|
||||
label: "Report issue",
|
||||
label: t("reportIssue"),
|
||||
href: "https://github.com/diegosouzapw/OmniRoute/issues",
|
||||
external: true,
|
||||
icon: "bug_report",
|
||||
@@ -135,17 +139,15 @@ export default function HomePageClient({ machineId }) {
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">Quick Start</h2>
|
||||
<p className="text-sm text-text-muted">
|
||||
Get up and running in 4 steps. Connect providers, route models, monitor everything.
|
||||
</p>
|
||||
<h2 className="text-lg font-semibold">{t("quickStart")}</h2>
|
||||
<p className="text-sm text-text-muted">{t("quickStartDesc")}</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/docs"
|
||||
className="hidden sm:inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border border-border text-text-muted hover:text-text-main hover:bg-bg-subtle transition-colors"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">menu_book</span>
|
||||
Full Docs
|
||||
{t("fullDocs")}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -155,7 +157,7 @@ export default function HomePageClient({ machineId }) {
|
||||
<span className="material-symbols-outlined text-[18px]">key</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">1. Create API key</span>
|
||||
<span className="font-semibold">{t("step1Title")}</span>
|
||||
<p className="text-text-muted mt-0.5">
|
||||
Go to{" "}
|
||||
<Link href="/dashboard/endpoint" className="text-primary hover:underline">
|
||||
@@ -170,7 +172,7 @@ export default function HomePageClient({ machineId }) {
|
||||
<span className="material-symbols-outlined text-[18px]">dns</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">2. Connect providers</span>
|
||||
<span className="font-semibold">{t("step2Title")}</span>
|
||||
<p className="text-text-muted mt-0.5">
|
||||
Add accounts in{" "}
|
||||
<Link href="/dashboard/providers" className="text-primary hover:underline">
|
||||
@@ -185,7 +187,7 @@ export default function HomePageClient({ machineId }) {
|
||||
<span className="material-symbols-outlined text-[18px]">link</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">3. Point your client</span>
|
||||
<span className="font-semibold">{t("step3Title")}</span>
|
||||
<p className="text-text-muted mt-0.5">
|
||||
Set base URL to{" "}
|
||||
<code className="px-1.5 py-0.5 rounded bg-surface text-xs font-mono">
|
||||
@@ -200,7 +202,7 @@ export default function HomePageClient({ machineId }) {
|
||||
<span className="material-symbols-outlined text-[18px]">analytics</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">4. Monitor & optimize</span>
|
||||
<span className="font-semibold">{t("step4Title")}</span>
|
||||
<p className="text-text-muted mt-0.5">
|
||||
Track tokens, cost and errors in{" "}
|
||||
<Link href="/dashboard/usage" className="text-primary hover:underline">
|
||||
@@ -239,7 +241,7 @@ export default function HomePageClient({ machineId }) {
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">Providers Overview</h2>
|
||||
<h2 className="text-lg font-semibold">{t("providersOverview")}</h2>
|
||||
<p className="text-sm text-text-muted">
|
||||
{providerStats.filter((item) => item.total > 0).length} configured of{" "}
|
||||
{providerStats.length} available providers
|
||||
@@ -348,7 +350,7 @@ function ProviderOverviewCard({ item, metrics, onClick }) {
|
||||
</div>
|
||||
<p className={`text-xs ${statusVariant}`}>
|
||||
{item.total === 0
|
||||
? "Not configured"
|
||||
? tc("notConfigured")
|
||||
: `${item.connected} active · ${item.errors} error`}
|
||||
</p>
|
||||
{metrics && metrics.totalRequests > 0 && (
|
||||
@@ -433,7 +435,7 @@ function ProviderModelsModal({ provider, models, onClose }) {
|
||||
<span className="material-symbols-outlined text-[32px] text-text-muted mb-2">
|
||||
search_off
|
||||
</span>
|
||||
<p className="text-sm text-text-muted">No models available for this provider.</p>
|
||||
<p className="text-sm text-text-muted">{t("noModelsAvailable")}</p>
|
||||
<p className="text-xs text-text-muted mt-1">
|
||||
Configure a connection first in{" "}
|
||||
<button
|
||||
@@ -460,7 +462,7 @@ function ProviderModelsModal({ provider, models, onClose }) {
|
||||
<button
|
||||
onClick={() => handleCopy(m.fullModel)}
|
||||
className="shrink-0 ml-2 p-1.5 rounded-lg text-text-muted hover:text-text-main hover:bg-bg-subtle transition-colors opacity-0 group-hover:opacity-100"
|
||||
title="Copy model name"
|
||||
title={t("copyModelName")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">
|
||||
{copiedModel === m.fullModel ? "check" : "content_copy"}
|
||||
@@ -481,7 +483,7 @@ function ProviderModelsModal({ provider, models, onClose }) {
|
||||
className="flex-1"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">settings</span>
|
||||
Configure Provider
|
||||
{t("configureProvider")}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={onClose}>
|
||||
Close
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
import { useState, Suspense } from "react";
|
||||
import { UsageAnalytics, CardSkeleton, SegmentedControl } from "@/shared/components";
|
||||
import EvalsTab from "../usage/components/EvalsTab";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function AnalyticsPage() {
|
||||
const [activeTab, setActiveTab] = useState("overview");
|
||||
const t = useTranslations("analytics");
|
||||
|
||||
const tabDescriptions = {
|
||||
overview:
|
||||
"Monitor your API usage patterns, token consumption, costs, and activity trends across all providers and models.",
|
||||
evals:
|
||||
"Run evaluation suites to test and validate your LLM endpoints. Compare model quality, detect regressions, and benchmark latency.",
|
||||
overview: t("overviewDescription"),
|
||||
evals: t("evalsDescription"),
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -20,15 +20,15 @@ export default function AnalyticsPage() {
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-primary text-[28px]">analytics</span>
|
||||
Analytics
|
||||
{t("title")}
|
||||
</h1>
|
||||
<p className="text-sm text-text-muted mt-1">{tabDescriptions[activeTab]}</p>
|
||||
</div>
|
||||
|
||||
<SegmentedControl
|
||||
options={[
|
||||
{ value: "overview", label: "Overview" },
|
||||
{ value: "evals", label: "Evals" },
|
||||
{ value: "overview", label: t("overview") },
|
||||
{ value: "evals", label: t("evals") },
|
||||
]}
|
||||
value={activeTab}
|
||||
onChange={setActiveTab}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Card, Button, Select, Badge } from "@/shared/components";
|
||||
import { FORMAT_META, FORMAT_OPTIONS } from "../exampleTemplates";
|
||||
@@ -12,7 +14,7 @@ const Editor = dynamic(() => import("@monaco-editor/react"), { ssr: false });
|
||||
/**
|
||||
* Chat Tester Mode:
|
||||
* - Left: Chat interface (send messages as a specific client format)
|
||||
* - Right: Pipeline visualization showing each translation step
|
||||
* - Right: {t("pipelineVisualization")} showing each translation step
|
||||
*
|
||||
* How it works:
|
||||
* 1. You type a message and select a "Client Format" (how the request is structured)
|
||||
@@ -22,6 +24,7 @@ const Editor = dynamic(() => import("@monaco-editor/react"), { ssr: false });
|
||||
*/
|
||||
|
||||
export default function ChatTesterMode() {
|
||||
const t = useTranslations("translator");
|
||||
const { provider, setProvider, providerOptions } = useProviderOptions("openai");
|
||||
const { model, setModel, availableModels, pickModelForFormat } = useAvailableModels();
|
||||
const [clientFormat, setClientFormat] = useState("openai");
|
||||
@@ -246,7 +249,7 @@ export default function ChatTesterMode() {
|
||||
info
|
||||
</span>
|
||||
<div>
|
||||
<p className="font-medium text-text-main mb-0.5">Pipeline Debugger</p>
|
||||
<p className="font-medium text-text-main mb-0.5">{t("pipelineDebugger")}</p>
|
||||
<p>
|
||||
Send messages as a specific client format and see how each step of the translation
|
||||
pipeline works. The right panel shows the full flow:{" "}
|
||||
@@ -386,7 +389,7 @@ export default function ChatTesterMode() {
|
||||
<span className="material-symbols-outlined text-[18px] text-primary">
|
||||
account_tree
|
||||
</span>
|
||||
<h3 className="text-sm font-semibold text-text-main">Translation Pipeline</h3>
|
||||
<h3 className="text-sm font-semibold text-text-main">{t("translationPipeline")}</h3>
|
||||
</div>
|
||||
<p className="text-xs text-text-muted">
|
||||
Click on any step to inspect the data at that stage
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Card, Badge } from "@/shared/components";
|
||||
import { FORMAT_META } from "../exampleTemplates";
|
||||
@@ -10,6 +12,7 @@ import { FORMAT_META } from "../exampleTemplates";
|
||||
* Polls /api/translator/history for translation events.
|
||||
*/
|
||||
export default function LiveMonitorMode() {
|
||||
const t = useTranslations("translator");
|
||||
const [events, setEvents] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [autoRefresh, setAutoRefresh] = useState(true);
|
||||
@@ -55,12 +58,12 @@ export default function LiveMonitorMode() {
|
||||
info
|
||||
</span>
|
||||
<div>
|
||||
<p className="font-medium text-text-main mb-0.5">Real-Time Translation Activity</p>
|
||||
<p className="font-medium text-text-main mb-0.5">{t("realtime")}</p>
|
||||
<p>
|
||||
Shows translation events as API calls flow through OmniRoute. Events come from the
|
||||
in-memory buffer (resets on restart). Use{" "}
|
||||
<strong className="text-text-main">Chat Tester</strong>,{" "}
|
||||
<strong className="text-text-main">Test Bench</strong>, or external API calls to
|
||||
<strong className="text-text-main">{t("chatTester")}</strong>,{" "}
|
||||
<strong className="text-text-main">{t("testBench")}</strong>, or external API calls to
|
||||
generate events.
|
||||
</p>
|
||||
</div>
|
||||
@@ -103,7 +106,7 @@ export default function LiveMonitorMode() {
|
||||
{/* Events Table */}
|
||||
<Card>
|
||||
<div className="p-4">
|
||||
<h3 className="text-sm font-semibold text-text-main mb-3">Recent Translations</h3>
|
||||
<h3 className="text-sm font-semibold text-text-main mb-3">{t("recentTranslations")}</h3>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12 text-text-muted">
|
||||
@@ -115,7 +118,7 @@ export default function LiveMonitorMode() {
|
||||
<span className="material-symbols-outlined text-[48px] mb-3 opacity-30">
|
||||
monitoring
|
||||
</span>
|
||||
<p className="text-sm font-medium mb-1">No translations yet</p>
|
||||
<p className="text-sm font-medium mb-1">{t("noTranslations")}</p>
|
||||
<p className="text-xs text-center max-w-sm">
|
||||
Translation events appear here as requests flow through OmniRoute. Use any of these
|
||||
methods to generate events:
|
||||
@@ -143,13 +146,13 @@ export default function LiveMonitorMode() {
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="text-left text-xs text-text-muted border-b border-border">
|
||||
<th className="pb-2 pr-4">Time</th>
|
||||
<th className="pb-2 pr-4">Source</th>
|
||||
<th className="pb-2 pr-4">{t(">time</")}</th>
|
||||
<th className="pb-2 pr-4">{t(">source</")}</th>
|
||||
<th className="pb-2 pr-4"></th>
|
||||
<th className="pb-2 pr-4">Target</th>
|
||||
<th className="pb-2 pr-4">Model</th>
|
||||
<th className="pb-2 pr-4">Status</th>
|
||||
<th className="pb-2 text-right">Latency</th>
|
||||
<th className="pb-2 pr-4">{t(">target</")}</th>
|
||||
<th className="pb-2 pr-4">{t(">model</")}</th>
|
||||
<th className="pb-2 pr-4">{t(">status</")}</th>
|
||||
<th className="pb-2 text-right">{t(">latency</")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { Card, Button, Select, Badge } from "@/shared/components";
|
||||
import { EXAMPLE_TEMPLATES, FORMAT_META, FORMAT_OPTIONS } from "../exampleTemplates";
|
||||
@@ -8,6 +10,7 @@ import dynamic from "next/dynamic";
|
||||
const Editor = dynamic(() => import("@monaco-editor/react"), { ssr: false });
|
||||
|
||||
export default function PlaygroundMode() {
|
||||
const t = useTranslations("translator");
|
||||
const [sourceFormat, setSourceFormat] = useState("claude");
|
||||
const [targetFormat, setTargetFormat] = useState("openai");
|
||||
const [inputContent, setInputContent] = useState("");
|
||||
@@ -114,7 +117,7 @@ export default function PlaygroundMode() {
|
||||
info
|
||||
</span>
|
||||
<div>
|
||||
<p className="font-medium text-text-main mb-0.5">Format Converter</p>
|
||||
<p className="font-medium text-text-main mb-0.5">{t("formatConverter")}</p>
|
||||
<p>
|
||||
Paste or type a JSON request body. The translator will auto-detect the source format and
|
||||
convert it to the target format. Use this to debug how OmniRoute translates requests
|
||||
@@ -201,7 +204,7 @@ export default function PlaygroundMode() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-[18px] text-text-muted">input</span>
|
||||
<h3 className="text-sm font-semibold text-text-main">Input</h3>
|
||||
<h3 className="text-sm font-semibold text-text-main">{t(">input</")}</h3>
|
||||
{detectedFormat && (
|
||||
<Badge variant="info" size="sm" dot>
|
||||
{FORMAT_META[detectedFormat]?.label || detectedFormat}
|
||||
@@ -265,7 +268,7 @@ export default function PlaygroundMode() {
|
||||
<span className="material-symbols-outlined text-[18px] text-text-muted">
|
||||
output
|
||||
</span>
|
||||
<h3 className="text-sm font-semibold text-text-main">Output</h3>
|
||||
<h3 className="text-sm font-semibold text-text-main">{t(">output</")}</h3>
|
||||
{outputContent && (
|
||||
<Badge variant="success" size="sm" dot>
|
||||
{FORMAT_META[targetFormat]?.label || targetFormat}
|
||||
@@ -303,7 +306,7 @@ export default function PlaygroundMode() {
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Example Templates */}
|
||||
{/* {t("exampleTemplates")} */}
|
||||
<Card>
|
||||
<div className="p-4 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card, Button, Select, Badge } from "@/shared/components";
|
||||
import { EXAMPLE_TEMPLATES, FORMAT_META, FORMAT_OPTIONS } from "../exampleTemplates";
|
||||
@@ -26,6 +28,7 @@ const SCENARIOS = [
|
||||
];
|
||||
|
||||
export default function TestBenchMode() {
|
||||
const t = useTranslations("translator");
|
||||
const [sourceFormat, setSourceFormat] = useState("claude");
|
||||
const { provider, setProvider, providerOptions } = useProviderOptions("openai");
|
||||
const { model, setModel, availableModels, pickModelForFormat } = useAvailableModels();
|
||||
@@ -147,7 +150,7 @@ export default function TestBenchMode() {
|
||||
info
|
||||
</span>
|
||||
<div>
|
||||
<p className="font-medium text-text-main mb-0.5">Compatibility Tester</p>
|
||||
<p className="font-medium text-text-main mb-0.5">{t("compatibilityTester")}</p>
|
||||
<p>
|
||||
Run predefined scenarios (Simple Chat, Tool Calling, etc.) to verify translation and
|
||||
provider compatibility. Select a source format and target provider, then run all tests
|
||||
@@ -232,7 +235,7 @@ export default function TestBenchMode() {
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<h3 className="text-sm font-semibold text-text-main">Compatibility Report</h3>
|
||||
<h3 className="text-sm font-semibold text-text-main">{t("compatibilityReport")}</h3>
|
||||
<Badge
|
||||
variant={
|
||||
compatibility >= 80 ? "success" : compatibility >= 50 ? "warning" : "error"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
/**
|
||||
* BudgetTab — Batch C
|
||||
*
|
||||
@@ -34,6 +36,7 @@ function ProgressBar({ value, max, warningAt = 0.8 }) {
|
||||
}
|
||||
|
||||
export default function BudgetTab() {
|
||||
const t = useTranslations("usage");
|
||||
const [keys, setKeys] = useState([]);
|
||||
const [selectedKey, setSelectedKey] = useState(null);
|
||||
const [budget, setBudget] = useState(null);
|
||||
@@ -142,11 +145,11 @@ export default function BudgetTab() {
|
||||
<div className="p-2 rounded-lg bg-emerald-500/10 text-emerald-500">
|
||||
<span className="material-symbols-outlined text-[20px]">account_balance_wallet</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold">Budget Management</h3>
|
||||
<h3 className="text-lg font-semibold">{t("budgetManagement")}</h3>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="text-sm text-text-muted mb-1 block">API Key</label>
|
||||
<label className="text-sm text-text-muted mb-1 block">{t(">apiKey</")}</label>
|
||||
<select
|
||||
value={selectedKey || ""}
|
||||
onChange={(e) => setSelectedKey(e.target.value)}
|
||||
@@ -170,7 +173,7 @@ export default function BudgetTab() {
|
||||
)}
|
||||
</div>
|
||||
<div className="p-4 rounded-lg border border-border/30 bg-surface/20">
|
||||
<p className="text-sm text-text-muted mb-2">This Month</p>
|
||||
<p className="text-sm text-text-muted mb-2">{t("thisMonth")}</p>
|
||||
<p className="text-2xl font-bold text-text-main">${monthlyCost.toFixed(2)}</p>
|
||||
{monthlyLimit > 0 && (
|
||||
<ProgressBar value={monthlyCost} max={monthlyLimit} warningAt={warnPct} />
|
||||
@@ -180,7 +183,7 @@ export default function BudgetTab() {
|
||||
|
||||
{/* Budget Form */}
|
||||
<div className="border-t border-border/30 pt-4">
|
||||
<p className="text-sm font-medium mb-3">Set Limits</p>
|
||||
<p className="text-sm font-medium mb-3">{t("setLimits")}</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||
<Input
|
||||
label="Daily Limit (USD)"
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card } from "@/shared/components";
|
||||
|
||||
export default function BudgetTelemetryCards() {
|
||||
const t = useTranslations("usage");
|
||||
const [telemetry, setTelemetry] = useState(null);
|
||||
const [cache, setCache] = useState(null);
|
||||
const [policies, setPolicies] = useState(null);
|
||||
@@ -45,12 +48,12 @@ export default function BudgetTelemetryCards() {
|
||||
<span className="font-mono">{fmt(telemetry.p99)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-t border-border pt-2 mt-2">
|
||||
<span className="text-text-muted">Total requests</span>
|
||||
<span className="text-text-muted">{t("totalRequests")}</span>
|
||||
<span className="font-mono">{telemetry.totalRequests ?? 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-text-muted">No data yet</p>
|
||||
<p className="text-sm text-text-muted">{t("noDataYet")}</p>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
@@ -63,17 +66,17 @@ export default function BudgetTelemetryCards() {
|
||||
{cache ? (
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-muted">Entries</span>
|
||||
<span className="text-text-muted">{t(">entries</")}</span>
|
||||
<span className="font-mono">
|
||||
{cache.size}/{cache.maxSize}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-muted">Hit Rate</span>
|
||||
<span className="text-text-muted">{t("hitRate")}</span>
|
||||
<span className="font-mono">{cache.hitRate?.toFixed(1) ?? 0}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-muted">Hits / Misses</span>
|
||||
<span className="text-text-muted">{t("hitsMisses")}</span>
|
||||
<span className="font-mono">
|
||||
{cache.hits ?? 0} / {cache.misses ?? 0}
|
||||
</span>
|
||||
@@ -93,11 +96,11 @@ export default function BudgetTelemetryCards() {
|
||||
{policies ? (
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-muted">Circuit Breakers</span>
|
||||
<span className="text-text-muted">{t("circuitBreakers")}</span>
|
||||
<span className="font-mono">{policies.circuitBreakers?.length ?? 0} active</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-muted">Locked IPs</span>
|
||||
<span className="text-text-muted">{t("lockedIPs")}</span>
|
||||
<span className="font-mono">{policies.lockedIdentifiers?.length ?? 0}</span>
|
||||
</div>
|
||||
{policies.circuitBreakers?.some((cb) => cb.state === "OPEN") && (
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
/**
|
||||
* EvalsTab — Batch F
|
||||
*
|
||||
@@ -49,6 +51,7 @@ const STRATEGIES = [
|
||||
];
|
||||
|
||||
export default function EvalsTab() {
|
||||
const t = useTranslations("usage");
|
||||
const [suites, setSuites] = useState([]);
|
||||
const [apiKey, setApiKey] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -255,7 +258,7 @@ export default function EvalsTab() {
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* How It Works — Collapsible */}
|
||||
{/* {t("howItWorks")} — Collapsible */}
|
||||
<Card className="p-0 overflow-hidden">
|
||||
<button
|
||||
onClick={() => setShowHowItWorks(!showHowItWorks)}
|
||||
@@ -289,7 +292,7 @@ export default function EvalsTab() {
|
||||
<div className="w-10 h-10 rounded-full bg-violet-500/20 flex items-center justify-center mb-3">
|
||||
<span className="text-lg font-bold text-violet-400">1</span>
|
||||
</div>
|
||||
<h4 className="text-sm font-semibold text-text-main mb-1">Define</h4>
|
||||
<h4 className="text-sm font-semibold text-text-main mb-1">{t(">define</")}</h4>
|
||||
<p className="text-xs text-text-muted">
|
||||
Create test cases with input prompts and expected output criteria using strategies
|
||||
like contains, regex, or exact match.
|
||||
@@ -299,7 +302,7 @@ export default function EvalsTab() {
|
||||
<div className="w-10 h-10 rounded-full bg-sky-500/20 flex items-center justify-center mb-3">
|
||||
<span className="text-lg font-bold text-sky-400">2</span>
|
||||
</div>
|
||||
<h4 className="text-sm font-semibold text-text-main mb-1">Run</h4>
|
||||
<h4 className="text-sm font-semibold text-text-main mb-1">{t(">run</")}</h4>
|
||||
<p className="text-xs text-text-muted">
|
||||
Execute test cases against your LLM endpoints through OmniRoute. Each case is sent
|
||||
as a real API request.
|
||||
@@ -309,7 +312,7 @@ export default function EvalsTab() {
|
||||
<div className="w-10 h-10 rounded-full bg-emerald-500/20 flex items-center justify-center mb-3">
|
||||
<span className="text-lg font-bold text-emerald-400">3</span>
|
||||
</div>
|
||||
<h4 className="text-sm font-semibold text-text-main mb-1">Evaluate</h4>
|
||||
<h4 className="text-sm font-semibold text-text-main mb-1">{t(">evaluate</")}</h4>
|
||||
<p className="text-xs text-text-muted">
|
||||
Responses are compared against expected criteria. See pass/fail for each case with
|
||||
latency metrics and detailed feedback.
|
||||
@@ -369,7 +372,7 @@ export default function EvalsTab() {
|
||||
<span className="material-symbols-outlined text-[20px]">science</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">Evaluation Suites</h3>
|
||||
<h3 className="text-lg font-semibold">{t("evalSuites")}</h3>
|
||||
<p className="text-xs text-text-muted">
|
||||
Click a suite to view test cases, then run to evaluate your LLM endpoints
|
||||
</p>
|
||||
@@ -663,7 +666,7 @@ function HeroSection() {
|
||||
<span className="material-symbols-outlined text-[28px]">science</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-xl font-bold text-text-main mb-1">Model Evaluations</h2>
|
||||
<h2 className="text-xl font-bold text-text-main mb-1">{t("modelEvals")}</h2>
|
||||
<p className="text-sm text-text-muted leading-relaxed max-w-2xl">
|
||||
Test and validate your LLM endpoints by running predefined evaluation suites. Each
|
||||
suite contains test cases that send real prompts through OmniRoute and compare
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
||||
import Image from "next/image";
|
||||
import { parseQuotaData, calculatePercentage, normalizePlanTier } from "./utils";
|
||||
@@ -81,6 +83,7 @@ function formatCountdown(resetAt) {
|
||||
}
|
||||
|
||||
export default function ProviderLimits() {
|
||||
const t = useTranslations("usage");
|
||||
const [connections, setConnections] = useState([]);
|
||||
const [quotaData, setQuotaData] = useState({});
|
||||
const [loading, setLoading] = useState({});
|
||||
@@ -286,7 +289,7 @@ export default function ProviderLimits() {
|
||||
<Card padding="lg">
|
||||
<div className="text-center py-12">
|
||||
<span className="material-symbols-outlined text-[64px] opacity-15">cloud_off</span>
|
||||
<h3 className="mt-4 text-lg font-semibold text-text-main">No Providers Connected</h3>
|
||||
<h3 className="mt-4 text-lg font-semibold text-text-main">{t("noProviders")}</h3>
|
||||
<p className="mt-2 text-sm text-text-muted max-w-[400px] mx-auto">
|
||||
Connect to providers with OAuth to track your API quota limits and usage.
|
||||
</p>
|
||||
@@ -300,7 +303,7 @@ export default function ProviderLimits() {
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between flex-wrap gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<h2 className="text-lg font-semibold text-text-main m-0">Provider Limits</h2>
|
||||
<h2 className="text-lg font-semibold text-text-main m-0">{t("providerLimits")}</h2>
|
||||
<span className="text-[13px] text-text-muted">
|
||||
{visibleConnections.length} account{visibleConnections.length !== 1 ? "s" : ""}
|
||||
{visibleConnections.length !== sortedConnections.length
|
||||
@@ -373,10 +376,10 @@ export default function ProviderLimits() {
|
||||
className="items-center px-4 py-2.5 border-b border-white/[0.06] text-[11px] font-semibold uppercase tracking-wider text-text-muted"
|
||||
style={{ display: "grid", gridTemplateColumns: "280px 1fr 100px 48px" }}
|
||||
>
|
||||
<div>Account</div>
|
||||
<div>Model Quotas</div>
|
||||
<div className="text-center">Last Used</div>
|
||||
<div className="text-center">Actions</div>
|
||||
<div>{t(">account</")}</div>
|
||||
<div>{t("modelQuotas")}</div>
|
||||
<div className="text-center">{t(">lastUsed</")}</div>
|
||||
<div className="text-center">{t(">actions</")}</div>
|
||||
</div>
|
||||
|
||||
{visibleConnections.map((conn, idx) => {
|
||||
@@ -491,7 +494,7 @@ export default function ProviderLimits() {
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="text-xs text-text-muted italic">No quota data</div>
|
||||
<div className="text-xs text-text-muted italic">{t("noQuotaData")}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { Card } from "@/shared/components";
|
||||
|
||||
export default function RateLimitStatus() {
|
||||
const t = useTranslations("usage");
|
||||
const [data, setData] = useState({ lockouts: [], cacheStats: null });
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@@ -31,7 +34,7 @@ export default function RateLimitStatus() {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Model Lockouts */}
|
||||
{/* {t("modelLockouts")} */}
|
||||
<Card>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 rounded-lg bg-orange-500/10 text-orange-500">
|
||||
@@ -55,7 +58,7 @@ export default function RateLimitStatus() {
|
||||
<span className="material-symbols-outlined text-[32px] mb-2 block opacity-40">
|
||||
lock_open
|
||||
</span>
|
||||
<p className="text-sm">No models currently locked</p>
|
||||
<p className="text-sm">{t("noLockouts")}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { Card } from "@/shared/components";
|
||||
|
||||
export default function SessionsTab() {
|
||||
const t = useTranslations("usage");
|
||||
const [data, setData] = useState({ count: 0, sessions: [] });
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@@ -11,7 +14,8 @@ export default function SessionsTab() {
|
||||
try {
|
||||
const res = await fetch("/api/sessions");
|
||||
if (res.ok) setData(await res.json());
|
||||
} catch {} finally {
|
||||
} catch {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
@@ -37,15 +41,15 @@ export default function SessionsTab() {
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold">Active Sessions</h3>
|
||||
<p className="text-sm text-text-muted">Tracked via request fingerprinting • Auto-refresh 5s</p>
|
||||
<h3 className="text-lg font-semibold">{t("activeSessions")}</h3>
|
||||
<p className="text-sm text-text-muted">
|
||||
Tracked via request fingerprinting • Auto-refresh 5s
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-cyan-500/10 border border-cyan-500/20">
|
||||
<span className="w-2 h-2 rounded-full bg-cyan-500 animate-pulse" />
|
||||
<span className="text-sm font-semibold tabular-nums text-cyan-400">
|
||||
{data.count}
|
||||
</span>
|
||||
<span className="text-sm font-semibold tabular-nums text-cyan-400">{data.count}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -55,23 +59,34 @@ export default function SessionsTab() {
|
||||
<span className="material-symbols-outlined text-[40px] mb-2 block opacity-40">
|
||||
fingerprint
|
||||
</span>
|
||||
<p className="text-sm">No active sessions</p>
|
||||
<p className="text-xs mt-1">Sessions appear as requests flow through the proxy</p>
|
||||
<p className="text-sm">{t("noSessions")}</p>
|
||||
<p className="text-xs mt-1">{t("sessionsHint")}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-border/30">
|
||||
<th className="text-left py-2 px-3 text-xs font-semibold text-text-muted uppercase tracking-wider">Session</th>
|
||||
<th className="text-left py-2 px-3 text-xs font-semibold text-text-muted uppercase tracking-wider">Age</th>
|
||||
<th className="text-right py-2 px-3 text-xs font-semibold text-text-muted uppercase tracking-wider">Requests</th>
|
||||
<th className="text-left py-2 px-3 text-xs font-semibold text-text-muted uppercase tracking-wider">Connection</th>
|
||||
<th className="text-left py-2 px-3 text-xs font-semibold text-text-muted uppercase tracking-wider">
|
||||
{t(">session</")}
|
||||
</th>
|
||||
<th className="text-left py-2 px-3 text-xs font-semibold text-text-muted uppercase tracking-wider">
|
||||
{t(">age</")}
|
||||
</th>
|
||||
<th className="text-right py-2 px-3 text-xs font-semibold text-text-muted uppercase tracking-wider">
|
||||
{t(">requests</")}
|
||||
</th>
|
||||
<th className="text-left py-2 px-3 text-xs font-semibold text-text-muted uppercase tracking-wider">
|
||||
{t(">connection</")}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.sessions.map((s) => (
|
||||
<tr key={s.sessionId} className="border-b border-border/10 hover:bg-surface/20 transition-colors">
|
||||
<tr
|
||||
key={s.sessionId}
|
||||
className="border-b border-border/10 hover:bg-surface/20 transition-colors"
|
||||
>
|
||||
<td className="py-2.5 px-3">
|
||||
<span className="font-mono text-xs px-2 py-1 rounded bg-surface/40 text-text-muted">
|
||||
{s.sessionId.slice(0, 12)}…
|
||||
@@ -83,7 +98,9 @@ export default function SessionsTab() {
|
||||
</td>
|
||||
<td className="py-2.5 px-3">
|
||||
{s.connectionId ? (
|
||||
<span className="text-xs font-mono text-cyan-400">{s.connectionId.slice(0, 10)}</span>
|
||||
<span className="text-xs font-mono text-cyan-400">
|
||||
{s.connectionId.slice(0, 10)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-text-muted/40">—</span>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
|
||||
@@ -94,8 +96,8 @@ function CallbackContent() {
|
||||
progress_activity
|
||||
</span>
|
||||
</div>
|
||||
<h1 className="text-xl font-semibold mb-2">Processing...</h1>
|
||||
<p className="text-text-muted">Please wait while we complete the authorization.</p>
|
||||
<h1 className="text-xl font-semibold mb-2">{t("processing")}</h1>
|
||||
<p className="text-text-muted">{t("pleaseWait")}</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -106,7 +108,7 @@ function CallbackContent() {
|
||||
check_circle
|
||||
</span>
|
||||
</div>
|
||||
<h1 className="text-xl font-semibold mb-2">Authorization Successful!</h1>
|
||||
<h1 className="text-xl font-semibold mb-2">{t("authSuccess")}</h1>
|
||||
<p className="text-text-muted">
|
||||
{status === "success"
|
||||
? "This window will close automatically..."
|
||||
@@ -120,7 +122,7 @@ function CallbackContent() {
|
||||
<div className="size-16 mx-auto mb-4 rounded-full bg-yellow-100 dark:bg-yellow-900/30 flex items-center justify-center">
|
||||
<span className="material-symbols-outlined text-3xl text-yellow-600">info</span>
|
||||
</div>
|
||||
<h1 className="text-xl font-semibold mb-2">Copy This URL</h1>
|
||||
<h1 className="text-xl font-semibold mb-2">{t("copyUrl")}</h1>
|
||||
<p className="text-text-muted mb-4">
|
||||
Please copy the URL from the address bar and paste it in the application.
|
||||
</p>
|
||||
@@ -141,6 +143,7 @@ function CallbackContent() {
|
||||
* Receives callback from OAuth providers and sends data back via multiple methods
|
||||
*/
|
||||
export default function CallbackPage() {
|
||||
const t = useTranslations("auth");
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
/**
|
||||
* 403 Forbidden Page — Phase 8.1
|
||||
*
|
||||
@@ -12,6 +14,7 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export default function ForbiddenPage() {
|
||||
const t = useTranslations("auth");
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen p-6 bg-[var(--bg-primary,#0a0a0f)] text-[var(--text-primary,#e0e0e0)] text-center">
|
||||
<div
|
||||
@@ -24,7 +27,7 @@ export default function ForbiddenPage() {
|
||||
>
|
||||
403
|
||||
</div>
|
||||
<h1 className="text-2xl font-semibold mb-2">Access Denied</h1>
|
||||
<h1 className="text-2xl font-semibold mb-2">{t("accessDenied")}</h1>
|
||||
<p className="text-[15px] text-[var(--text-secondary,#888)] max-w-[400px] leading-relaxed mb-8">
|
||||
You don't have permission to access this resource. Check your API key or contact the
|
||||
administrator.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
/**
|
||||
* Forgot Password Page — Phase 8.2
|
||||
*
|
||||
@@ -12,12 +14,13 @@ import Link from "next/link";
|
||||
import { Card } from "@/shared/components";
|
||||
|
||||
export default function ForgotPasswordPage() {
|
||||
const t = useTranslations("auth");
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-bg p-4">
|
||||
<div className="w-full max-w-lg">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-3xl font-bold text-primary mb-2">Reset Password</h1>
|
||||
<p className="text-text-muted">Choose a method to recover access to your dashboard</p>
|
||||
<h1 className="text-3xl font-bold text-primary mb-2">{t(">resetPassword</")}</h1>
|
||||
<p className="text-text-muted">{t("resetDescription")}</p>
|
||||
</div>
|
||||
|
||||
{/* Method 1: CLI Reset */}
|
||||
@@ -53,7 +56,7 @@ export default function ForgotPasswordPage() {
|
||||
Delete the password from the database and set a new one on startup:
|
||||
</p>
|
||||
<ol className="text-sm text-text-muted space-y-2 list-decimal list-inside mb-3">
|
||||
<li>Stop the OmniRoute server</li>
|
||||
<li>{t("stopServer")}</li>
|
||||
<li>
|
||||
Set a new password in your{" "}
|
||||
<code className="bg-black/30 px-1 rounded text-text-main">.env</code> file:
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button, Input } from "@/shared/components";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function LoginPage() {
|
||||
const t = useTranslations("auth");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -101,7 +104,7 @@ export default function LoginPage() {
|
||||
rocket_launch
|
||||
</span>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-text-main tracking-tight">Welcome</h1>
|
||||
<h1 className="text-3xl font-bold text-text-main tracking-tight">{t(">welcome</")}</h1>
|
||||
<p className="text-text-muted mt-2">
|
||||
Let's get your OmniRoute instance configured
|
||||
</p>
|
||||
@@ -124,7 +127,7 @@ export default function LoginPage() {
|
||||
</div>
|
||||
|
||||
<p className="text-center text-xs text-text-muted/60 mt-8">
|
||||
OmniRoute — Unified AI API Proxy
|
||||
OmniRoute — {t("unifiedProxy")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -146,7 +149,7 @@ export default function LoginPage() {
|
||||
<h1 className="text-3xl font-bold text-text-main tracking-tight">
|
||||
Secure Your Instance
|
||||
</h1>
|
||||
<p className="text-text-muted mt-2">Password protection is not enabled</p>
|
||||
<p className="text-text-muted mt-2">{t("passwordNotEnabled")}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-surface border border-border rounded-2xl p-8 shadow-soft">
|
||||
@@ -186,13 +189,13 @@ export default function LoginPage() {
|
||||
</div>
|
||||
<span className="text-xl font-semibold text-text-main tracking-tight">OmniRoute</span>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-text-main tracking-tight">Sign in</h1>
|
||||
<p className="text-text-muted mt-1.5">Enter your password to continue</p>
|
||||
<h1 className="text-2xl font-bold text-text-main tracking-tight">{t(">signIn</")}</h1>
|
||||
<p className="text-text-muted mt-1.5">{t("enterPassword")}</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleLogin} className="space-y-5">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-text-main">Password</label>
|
||||
<label className="text-sm font-medium text-text-main">{t(">password</")}</label>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Enter your password"
|
||||
|
||||
@@ -22,7 +22,41 @@
|
||||
"disabled": "Disabled",
|
||||
"active": "Active",
|
||||
"inactive": "Inactive",
|
||||
"noData": "No data available"
|
||||
"noData": "No data available",
|
||||
"configure": "Configure",
|
||||
"manage": "Manage",
|
||||
"name": "Name",
|
||||
"actions": "Actions",
|
||||
"status": "Status",
|
||||
"type": "Type",
|
||||
"model": "Model",
|
||||
"models": "models",
|
||||
"provider": "Provider",
|
||||
"account": "Account",
|
||||
"time": "Time",
|
||||
"details": "Details",
|
||||
"created": "Created",
|
||||
"lastUsed": "Last Used",
|
||||
"loadMore": "Load More",
|
||||
"noResults": "No results found",
|
||||
"reloadPage": "Reload Page",
|
||||
"connected": "Connected",
|
||||
"disconnected": "Disconnected",
|
||||
"notConfigured": "Not configured",
|
||||
"testConnection": "Test Connection",
|
||||
"enable": "Enable",
|
||||
"disable": "Disable",
|
||||
"columns": "Columns",
|
||||
"newest": "Newest",
|
||||
"oldest": "Oldest",
|
||||
"all": "All",
|
||||
"none": "None",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"warning": "Warning",
|
||||
"note": "Note",
|
||||
"free": "Free",
|
||||
"skipToContent": "Skip to content"
|
||||
},
|
||||
"sidebar": {
|
||||
"home": "Home",
|
||||
@@ -47,7 +81,14 @@
|
||||
"restart": "Restart",
|
||||
"shutdownConfirm": "Shut down OmniRoute?",
|
||||
"restartConfirm": "Restart OmniRoute?",
|
||||
"version": "v{version}"
|
||||
"version": "v{version}",
|
||||
"debug": "Debug",
|
||||
"system": "System",
|
||||
"help": "Help",
|
||||
"serverDisconnected": "Server Disconnected",
|
||||
"serverDisconnectedMsg": "The proxy server has been stopped or is restarting.",
|
||||
"expandSidebar": "Expand sidebar",
|
||||
"collapseSidebar": "Collapse sidebar"
|
||||
},
|
||||
"header": {
|
||||
"logout": "Logout",
|
||||
@@ -70,5 +111,428 @@
|
||||
"settingsDescription": "Manage your preferences",
|
||||
"openaiCompatible": "OpenAI Compatible",
|
||||
"anthropicCompatible": "Anthropic Compatible"
|
||||
},
|
||||
"home": {
|
||||
"quickStart": "Quick Start",
|
||||
"quickStartDesc": "Get up and running in 4 steps. Connect providers, route models, monitor everything.",
|
||||
"fullDocs": "Full Docs",
|
||||
"step1Title": "1. Create API key",
|
||||
"step1Desc": "Go to {endpoint} → Registered Keys. Generate one key per environment.",
|
||||
"step2Title": "2. Connect providers",
|
||||
"step2Desc": "Add accounts in {providers}. Supports OAuth, API Key, and free tiers.",
|
||||
"step3Title": "3. Point your client",
|
||||
"step3Desc": "Set base URL to {url} in your IDE or API client.",
|
||||
"step4Title": "4. Monitor & optimize",
|
||||
"step4Desc": "Track tokens, cost and errors in {logs} and {analytics}.",
|
||||
"providersOverview": "Providers Overview",
|
||||
"configuredOf": "{configured} configured of {total} available providers",
|
||||
"noModelsAvailable": "No models available for this provider.",
|
||||
"configureFirst": "Configure a connection first in {providers}",
|
||||
"configureProvider": "Configure Provider",
|
||||
"modelAvailable": "{count} model available",
|
||||
"modelsAvailable": "{count} models available",
|
||||
"connectionsActive": "{count} connection active",
|
||||
"connectionsActivePlural": "{count} connections active",
|
||||
"copyModelName": "Copy model name",
|
||||
"documentation": "Documentation",
|
||||
"healthMonitor": "Health Monitor",
|
||||
"reportIssue": "Report issue",
|
||||
"activeError": "{active} active · {errors} error"
|
||||
},
|
||||
"analytics": {
|
||||
"title": "Analytics",
|
||||
"overviewDescription": "Monitor your API usage patterns, token consumption, costs, and activity trends across all providers and models.",
|
||||
"evalsDescription": "Run evaluation suites to test and validate your LLM endpoints. Compare model quality, detect regressions, and benchmark latency.",
|
||||
"overview": "Overview",
|
||||
"evals": "Evals"
|
||||
},
|
||||
"apiManager": {
|
||||
"title": "API Keys",
|
||||
"createKey": "Create API Key",
|
||||
"key": "Key",
|
||||
"revokeKey": "Revoke Key",
|
||||
"revokeConfirm": "Are you sure you want to revoke this API key?",
|
||||
"noKeys": "No API keys created yet",
|
||||
"noKeysDesc": "Create an API key to authenticate with OmniRoute",
|
||||
"keyLabel": "Key Label",
|
||||
"permissions": "Permissions",
|
||||
"expiresAt": "Expires",
|
||||
"never": "Never",
|
||||
"revoke": "Revoke",
|
||||
"showKey": "Show Key",
|
||||
"hideKey": "Hide Key",
|
||||
"copyKey": "Copy API Key",
|
||||
"allModels": "All Models",
|
||||
"selectedModels": "Selected Models",
|
||||
"readOnly": "Read Only",
|
||||
"fullAccess": "Full Access"
|
||||
},
|
||||
"auditLog": {
|
||||
"title": "Audit Log",
|
||||
"searchPlaceholder": "Search actions...",
|
||||
"action": "Action",
|
||||
"actor": "Actor",
|
||||
"target": "Target",
|
||||
"ipAddress": "IP Address",
|
||||
"timestamp": "Timestamp",
|
||||
"noEntries": "No audit entries found",
|
||||
"filterByAction": "Filter by action"
|
||||
},
|
||||
"cliTools": {
|
||||
"title": "CLI Tools",
|
||||
"mapModels": "Map Models",
|
||||
"testConnection": "Test Connection",
|
||||
"connectionStatus": "Connection Status",
|
||||
"configureEndpoint": "Configure Endpoint",
|
||||
"instructions": "Instructions",
|
||||
"modelMapping": "Model Mapping",
|
||||
"baseUrl": "Base URL",
|
||||
"apiKey": "API Key"
|
||||
},
|
||||
"combos": {
|
||||
"title": "Combos",
|
||||
"createCombo": "Create Combo",
|
||||
"editCombo": "Edit Combo",
|
||||
"deleteCombo": "Delete Combo",
|
||||
"noModels": "No models",
|
||||
"noModelsYet": "No models added yet",
|
||||
"addModel": "Add Model",
|
||||
"routingStrategy": "Routing Strategy",
|
||||
"maxRetries": "Max Retries",
|
||||
"timeout": "Timeout (ms)",
|
||||
"healthcheck": "Healthcheck",
|
||||
"priority": "Priority",
|
||||
"fallback": "Fallback",
|
||||
"roundRobin": "Round Robin",
|
||||
"random": "Random",
|
||||
"leastLatency": "Least Latency",
|
||||
"comboName": "Combo Name",
|
||||
"comboNamePlaceholder": "e.g. my-smart-combo",
|
||||
"deleteConfirm": "Are you sure you want to delete this combo?"
|
||||
},
|
||||
"costs": {
|
||||
"title": "Costs",
|
||||
"totalCost": "Total Cost",
|
||||
"breakdown": "Cost Breakdown",
|
||||
"noData": "No cost data",
|
||||
"byModel": "By Model",
|
||||
"byProvider": "By Provider"
|
||||
},
|
||||
"endpoint": {
|
||||
"title": "API Endpoint",
|
||||
"available": "Available Endpoints",
|
||||
"cloudProxy": "Cloud Proxy",
|
||||
"disableConfirm": "Are you sure you want to disable cloud proxy?",
|
||||
"baseUrl": "Base URL",
|
||||
"apiKeyLabel": "API Key",
|
||||
"registeredKeys": "Registered Keys",
|
||||
"chatCompletions": "Chat Completions",
|
||||
"responses": "Responses",
|
||||
"listModels": "List Models"
|
||||
},
|
||||
"health": {
|
||||
"title": "System Health",
|
||||
"healthy": "Healthy",
|
||||
"degraded": "Degraded",
|
||||
"down": "Down",
|
||||
"uptime": "Uptime",
|
||||
"memory": "Memory",
|
||||
"cpu": "CPU",
|
||||
"database": "Database",
|
||||
"lastCheck": "Last Check",
|
||||
"providerHealth": "Provider Health",
|
||||
"systemMetrics": "System Metrics",
|
||||
"tokenHealth": "Token Health",
|
||||
"refreshAll": "Refresh All",
|
||||
"checkNow": "Check Now"
|
||||
},
|
||||
"limits": {
|
||||
"title": "Limits & Quotas",
|
||||
"rateLimit": "Rate Limit",
|
||||
"remaining": "Remaining",
|
||||
"requestsPerMinute": "Requests/min",
|
||||
"tokensPerMinute": "Tokens/min",
|
||||
"dailyLimit": "Daily Limit"
|
||||
},
|
||||
"logs": {
|
||||
"title": "Logs",
|
||||
"requestLogs": "Request Logs",
|
||||
"proxyLogs": "Proxy Logs",
|
||||
"auditLog": "Audit Log",
|
||||
"console": "Console"
|
||||
},
|
||||
"onboarding": {
|
||||
"welcome": "Welcome to OmniRoute",
|
||||
"setPassword": "Set Password",
|
||||
"addProvider": "Add your first provider",
|
||||
"getStarted": "Get Started",
|
||||
"skip": "Skip",
|
||||
"passwordLabel": "Password",
|
||||
"confirmPassword": "Confirm Password",
|
||||
"setupComplete": "Setup Complete!",
|
||||
"goToDashboard": "Go to Dashboard"
|
||||
},
|
||||
"providers": {
|
||||
"title": "Providers",
|
||||
"addProvider": "Add Provider",
|
||||
"editProvider": "Edit Provider",
|
||||
"deleteProvider": "Delete Provider",
|
||||
"noProviders": "No providers configured",
|
||||
"modelAvailability": "Model Availability",
|
||||
"accounts": "Accounts",
|
||||
"newAccount": "New Account",
|
||||
"deleteConfirm": "Are you sure you want to delete this provider?",
|
||||
"testing": "Testing...",
|
||||
"testSuccess": "Connection successful",
|
||||
"testFailed": "Connection failed",
|
||||
"available": "Available",
|
||||
"unavailable": "Unavailable",
|
||||
"unknown": "Unknown"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"general": "General",
|
||||
"security": "Security",
|
||||
"appearance": "Appearance",
|
||||
"routing": "Routing",
|
||||
"cache": "Cache",
|
||||
"resilience": "Resilience",
|
||||
"systemPrompt": "System Prompt",
|
||||
"thinkingBudget": "Thinking Budget",
|
||||
"proxy": "Proxy",
|
||||
"pricing": "Pricing",
|
||||
"storage": "Storage",
|
||||
"policies": "Policies",
|
||||
"ipFilter": "IP Filter",
|
||||
"comboDefaults": "Combo Defaults",
|
||||
"fallbackChains": "Fallback Chains",
|
||||
"changePassword": "Change Password",
|
||||
"enablePassword": "Enable Password",
|
||||
"darkMode": "Dark Mode",
|
||||
"lightMode": "Light Mode",
|
||||
"systemTheme": "System Theme",
|
||||
"enableCache": "Enable Cache",
|
||||
"cacheTTL": "Cache TTL",
|
||||
"maxCacheSize": "Max Cache Size",
|
||||
"clearCache": "Clear Cache",
|
||||
"cacheHits": "Cache Hits",
|
||||
"cacheMisses": "Cache Misses",
|
||||
"hitRate": "Hit Rate",
|
||||
"cacheEntries": "Cache Entries",
|
||||
"circuitBreaker": "Circuit Breaker",
|
||||
"retryPolicy": "Retry Policy",
|
||||
"maxRetries": "Max Retries",
|
||||
"retryDelay": "Retry Delay",
|
||||
"timeoutMs": "Timeout (ms)",
|
||||
"enableSystemPrompt": "Enable System Prompt",
|
||||
"systemPromptText": "System Prompt Text",
|
||||
"enableThinking": "Enable Thinking",
|
||||
"maxThinkingTokens": "Max Thinking Tokens",
|
||||
"enableProxy": "Enable Proxy",
|
||||
"proxyUrl": "Proxy URL",
|
||||
"pricingRates": "Pricing Rates Format",
|
||||
"currentPricing": "Current Pricing Overview",
|
||||
"loadingPricing": "Loading pricing data...",
|
||||
"noPricing": "No pricing data available",
|
||||
"input": "Input",
|
||||
"output": "Output",
|
||||
"cached": "Cached",
|
||||
"reasoning": "Reasoning",
|
||||
"cacheCreation": "Cache Creation",
|
||||
"customPricing": "Custom Pricing",
|
||||
"databaseSize": "Database Size",
|
||||
"backupDb": "Backup Database",
|
||||
"restoreDb": "Restore Database",
|
||||
"exportData": "Export Data",
|
||||
"importData": "Import Data",
|
||||
"clearData": "Clear All Data",
|
||||
"clearDataConfirm": "This will permanently delete all data. Are you sure?",
|
||||
"enableRequestLogs": "Enable Request Logs",
|
||||
"logRetention": "Log Retention",
|
||||
"ipWhitelist": "IP Whitelist",
|
||||
"ipBlacklist": "IP Blacklist",
|
||||
"addIP": "Add IP",
|
||||
"savedSuccessfully": "Settings saved successfully"
|
||||
},
|
||||
"translator": {
|
||||
"title": "Translator",
|
||||
"realtime": "Real-Time Translation Activity",
|
||||
"chatTester": "Chat Tester",
|
||||
"testBench": "Test Bench",
|
||||
"recentTranslations": "Recent Translations",
|
||||
"noTranslations": "No translations yet",
|
||||
"source": "Source",
|
||||
"target": "Target",
|
||||
"latency": "Latency",
|
||||
"formatConverter": "Format Converter",
|
||||
"input": "Input",
|
||||
"output": "Output",
|
||||
"exampleTemplates": "Example Templates",
|
||||
"compatibilityTester": "Compatibility Tester",
|
||||
"compatibilityReport": "Compatibility Report",
|
||||
"pipelineDebugger": "Pipeline Debugger",
|
||||
"translationPipeline": "Translation Pipeline",
|
||||
"pipelineVisualization": "Pipeline visualization"
|
||||
},
|
||||
"usage": {
|
||||
"title": "Usage",
|
||||
"budgetManagement": "Budget Management",
|
||||
"apiKey": "API Key",
|
||||
"thisMonth": "This Month",
|
||||
"setLimits": "Set Limits",
|
||||
"totalRequests": "Total requests",
|
||||
"noDataYet": "No data yet",
|
||||
"entries": "Entries",
|
||||
"hitRate": "Hit Rate",
|
||||
"hitsMisses": "Hits / Misses",
|
||||
"circuitBreakers": "Circuit Breakers",
|
||||
"lockedIPs": "Locked IPs",
|
||||
"howItWorks": "How It Works",
|
||||
"define": "Define",
|
||||
"run": "Run",
|
||||
"evaluate": "Evaluate",
|
||||
"evalSuites": "Evaluation Suites",
|
||||
"modelEvals": "Model Evaluations",
|
||||
"modelLockouts": "Model Lockouts",
|
||||
"noLockouts": "No models currently locked",
|
||||
"activeSessions": "Active Sessions",
|
||||
"noSessions": "No active sessions",
|
||||
"sessionsHint": "Sessions appear as requests flow through the proxy",
|
||||
"session": "Session",
|
||||
"age": "Age",
|
||||
"requests": "Requests",
|
||||
"connection": "Connection",
|
||||
"providerLimits": "Provider Limits",
|
||||
"noProviders": "No Providers Connected",
|
||||
"modelQuotas": "Model Quotas",
|
||||
"noQuotaData": "No quota data",
|
||||
"noQuotaDataAvailable": "No quota data available"
|
||||
},
|
||||
"modals": {
|
||||
"waitingAuth": "Waiting for Authorization",
|
||||
"verificationUrl": "Verification URL",
|
||||
"yourCode": "Your Code",
|
||||
"remoteAccess": "Remote access:",
|
||||
"connectedSuccess": "Connected Successfully!",
|
||||
"connectionFailed": "Connection Failed",
|
||||
"chooseAuthMethod": "Choose your authentication method:",
|
||||
"awsBuilderId": "AWS Builder ID",
|
||||
"awsIamIdentity": "AWS IAM Identity Center",
|
||||
"googleAccount": "Google Account",
|
||||
"githubAccount": "GitHub Account",
|
||||
"importToken": "Import Token",
|
||||
"pasteToken": "Paste refresh token from Kiro IDE.",
|
||||
"awsRegion": "AWS Region",
|
||||
"autoDetecting": "Auto-detecting tokens...",
|
||||
"readingFromCache": "Reading from AWS SSO cache",
|
||||
"readingFromCursor": "Reading from Cursor IDE database",
|
||||
"initializing": "Initializing...",
|
||||
"pricingConfig": "Pricing Configuration",
|
||||
"loadingPricing": "Loading pricing data...",
|
||||
"pricingRatesFormat": "Pricing Rates Format",
|
||||
"noPricingData": "No pricing data available",
|
||||
"noModelsFound": "No models found"
|
||||
},
|
||||
"loggers": {
|
||||
"allProviders": "All Providers",
|
||||
"allModels": "All Models",
|
||||
"allAccounts": "All Accounts",
|
||||
"allApiKeys": "All API Keys",
|
||||
"allTypes": "All Types",
|
||||
"allLevels": "All Levels",
|
||||
"modelAZ": "Model A-Z",
|
||||
"modelZA": "Model Z-A",
|
||||
"loadingLogs": "Loading logs...",
|
||||
"loadingProxyLogs": "Loading proxy logs...",
|
||||
"noLogEntries": "No log entries found",
|
||||
"noPayloadData": "No payload data available for this log entry.",
|
||||
"proxyEvent": "Proxy Event",
|
||||
"proxy": "Proxy",
|
||||
"level": "Level",
|
||||
"directNative": "Direct (native)",
|
||||
"combo": "Combo",
|
||||
"inputTokens": "I:",
|
||||
"outputTokens": "O:"
|
||||
},
|
||||
"stats": {
|
||||
"usageOverview": "Usage Overview",
|
||||
"outputTokens": "Output Tokens",
|
||||
"totalCost": "Total Cost",
|
||||
"usageByModel": "Usage by Model",
|
||||
"usageByAccount": "Usage by Account",
|
||||
"failedToLoad": "Failed to load usage statistics.",
|
||||
"tokenHealth": "Token Health",
|
||||
"totalOAuth": "Total OAuth",
|
||||
"healthy": "Healthy",
|
||||
"errored": "Errored",
|
||||
"lastCheck": "Last check",
|
||||
"noData": "No data",
|
||||
"share": "Share",
|
||||
"unableToLoad": "Unable to load system metrics",
|
||||
"product": "Product",
|
||||
"resources": "Resources",
|
||||
"company": "Company"
|
||||
},
|
||||
"auth": {
|
||||
"welcome": "Welcome",
|
||||
"signIn": "Sign in",
|
||||
"enterPassword": "Enter your password to continue",
|
||||
"password": "Password",
|
||||
"unifiedProxy": "Unified AI API Proxy",
|
||||
"passwordNotEnabled": "Password protection is not enabled",
|
||||
"resetPassword": "Reset Password",
|
||||
"resetDescription": "Choose a method to recover access to your dashboard",
|
||||
"stopServer": "Stop the OmniRoute server",
|
||||
"processing": "Processing...",
|
||||
"pleaseWait": "Please wait while we complete the authorization.",
|
||||
"authSuccess": "Authorization Successful!",
|
||||
"copyUrl": "Copy This URL",
|
||||
"accessDenied": "Access Denied",
|
||||
"forgotPassword": "Forgot password?"
|
||||
},
|
||||
"landing": {
|
||||
"allProviders": "All AI Providers",
|
||||
"oneEndpoint": "One Endpoint",
|
||||
"powerfulFeatures": "Powerful Features",
|
||||
"howItWorks": "How OmniRoute Works",
|
||||
"installOmniRoute": "Install OmniRoute",
|
||||
"openDashboard": "Open Dashboard",
|
||||
"routeRequests": "Route Requests",
|
||||
"dataLocation": "Data Location:",
|
||||
"getStarted": "Get Started",
|
||||
"product": "Product",
|
||||
"resources": "Resources",
|
||||
"legal": "Legal",
|
||||
"interactiveDiagram": "Interactive diagram visible on desktop"
|
||||
},
|
||||
"docs": {
|
||||
"title": "Documentation",
|
||||
"quickStart": "Quick Start",
|
||||
"features": "Features",
|
||||
"supportedProviders": "Supported Providers",
|
||||
"commonUseCases": "Common Use Cases",
|
||||
"clientCompatibility": "Client Compatibility",
|
||||
"apiReference": "API Reference",
|
||||
"method": "Method",
|
||||
"path": "Path",
|
||||
"notes": "Notes",
|
||||
"modelPrefixes": "Model Prefixes",
|
||||
"prefix": "Prefix",
|
||||
"troubleshooting": "Troubleshooting",
|
||||
"supportsChat": "Supports both chat and responses endpoints.",
|
||||
"oauthAutoRefresh": "OAuth connection with automatic token refresh.",
|
||||
"fullStreaming": "Full streaming support for all models."
|
||||
},
|
||||
"legal": {
|
||||
"privacyPolicy": "Privacy Policy",
|
||||
"termsOfService": "Terms of Service",
|
||||
"providerConfigurations": "Provider configurations",
|
||||
"apiKeys": "API keys",
|
||||
"usageLogs": "Usage logs",
|
||||
"applicationSettings": "Application settings",
|
||||
"viewExportAnalytics": "View and export usage analytics",
|
||||
"clearHistory": "Clear usage history at any time",
|
||||
"configureRetention": "Configure log retention policies",
|
||||
"backupRestore": "Back up and restore your database"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,41 @@
|
||||
"disabled": "Desativado",
|
||||
"active": "Ativo",
|
||||
"inactive": "Inativo",
|
||||
"noData": "Nenhum dado disponível"
|
||||
"noData": "Nenhum dado disponível",
|
||||
"configure": "Configurar",
|
||||
"manage": "Gerenciar",
|
||||
"name": "Nome",
|
||||
"actions": "Ações",
|
||||
"status": "Status",
|
||||
"type": "Tipo",
|
||||
"model": "Modelo",
|
||||
"models": "modelos",
|
||||
"provider": "Provedor",
|
||||
"account": "Conta",
|
||||
"time": "Tempo",
|
||||
"details": "Detalhes",
|
||||
"created": "Criado",
|
||||
"lastUsed": "Último Uso",
|
||||
"loadMore": "Carregar Mais",
|
||||
"noResults": "Nenhum resultado encontrado",
|
||||
"reloadPage": "Recarregar Página",
|
||||
"connected": "Conectado",
|
||||
"disconnected": "Desconectado",
|
||||
"notConfigured": "Não configurado",
|
||||
"testConnection": "Testar Conexão",
|
||||
"enable": "Ativar",
|
||||
"disable": "Desativar",
|
||||
"columns": "Colunas",
|
||||
"newest": "Mais Recente",
|
||||
"oldest": "Mais Antigo",
|
||||
"all": "Todos",
|
||||
"none": "Nenhum",
|
||||
"yes": "Sim",
|
||||
"no": "Não",
|
||||
"warning": "Aviso",
|
||||
"note": "Nota",
|
||||
"free": "Gratuito",
|
||||
"skipToContent": "Pular para conteúdo"
|
||||
},
|
||||
"sidebar": {
|
||||
"home": "Início",
|
||||
@@ -47,7 +81,14 @@
|
||||
"restart": "Reiniciar",
|
||||
"shutdownConfirm": "Desligar o OmniRoute?",
|
||||
"restartConfirm": "Reiniciar o OmniRoute?",
|
||||
"version": "v{version}"
|
||||
"version": "v{version}",
|
||||
"debug": "Depuração",
|
||||
"system": "Sistema",
|
||||
"help": "Ajuda",
|
||||
"serverDisconnected": "Servidor Desconectado",
|
||||
"serverDisconnectedMsg": "O servidor proxy foi parado ou está reiniciando.",
|
||||
"expandSidebar": "Expandir barra lateral",
|
||||
"collapseSidebar": "Recolher barra lateral"
|
||||
},
|
||||
"header": {
|
||||
"logout": "Sair",
|
||||
@@ -70,5 +111,428 @@
|
||||
"settingsDescription": "Gerencie suas preferências",
|
||||
"openaiCompatible": "Compatível com OpenAI",
|
||||
"anthropicCompatible": "Compatível com Anthropic"
|
||||
},
|
||||
"home": {
|
||||
"quickStart": "Início Rápido",
|
||||
"quickStartDesc": "Comece em 4 passos. Conecte provedores, roteie modelos, monitore tudo.",
|
||||
"fullDocs": "Docs Completa",
|
||||
"step1Title": "1. Criar chave de API",
|
||||
"step1Desc": "Vá em {endpoint} → Chaves Registradas. Gere uma chave por ambiente.",
|
||||
"step2Title": "2. Conectar provedores",
|
||||
"step2Desc": "Adicione contas em {providers}. Suporta OAuth, API Key e planos gratuitos.",
|
||||
"step3Title": "3. Apontar seu cliente",
|
||||
"step3Desc": "Defina a URL base como {url} no seu IDE ou cliente de API.",
|
||||
"step4Title": "4. Monitorar e otimizar",
|
||||
"step4Desc": "Acompanhe tokens, custos e erros em {logs} e {analytics}.",
|
||||
"providersOverview": "Visão Geral dos Provedores",
|
||||
"configuredOf": "{configured} configurados de {total} provedores disponíveis",
|
||||
"noModelsAvailable": "Nenhum modelo disponível para este provedor.",
|
||||
"configureFirst": "Configure uma conexão primeiro em {providers}",
|
||||
"configureProvider": "Configurar Provedor",
|
||||
"modelAvailable": "{count} modelo disponível",
|
||||
"modelsAvailable": "{count} modelos disponíveis",
|
||||
"connectionsActive": "{count} conexão ativa",
|
||||
"connectionsActivePlural": "{count} conexões ativas",
|
||||
"copyModelName": "Copiar nome do modelo",
|
||||
"documentation": "Documentação",
|
||||
"healthMonitor": "Monitor de Saúde",
|
||||
"reportIssue": "Reportar problema",
|
||||
"activeError": "{active} ativo · {errors} erro"
|
||||
},
|
||||
"analytics": {
|
||||
"title": "Análises",
|
||||
"overviewDescription": "Monitore padrões de uso da API, consumo de tokens, custos e tendências de atividade em todos os provedores e modelos.",
|
||||
"evalsDescription": "Execute suítes de avaliação para testar e validar seus endpoints LLM. Compare qualidade de modelos, detecte regressões e faça benchmarks de latência.",
|
||||
"overview": "Visão Geral",
|
||||
"evals": "Avaliações"
|
||||
},
|
||||
"apiManager": {
|
||||
"title": "Chaves de API",
|
||||
"createKey": "Criar Chave de API",
|
||||
"key": "Chave",
|
||||
"revokeKey": "Revogar Chave",
|
||||
"revokeConfirm": "Tem certeza que deseja revogar esta chave de API?",
|
||||
"noKeys": "Nenhuma chave de API criada",
|
||||
"noKeysDesc": "Crie uma chave de API para autenticar com o OmniRoute",
|
||||
"keyLabel": "Rótulo da Chave",
|
||||
"permissions": "Permissões",
|
||||
"expiresAt": "Expira",
|
||||
"never": "Nunca",
|
||||
"revoke": "Revogar",
|
||||
"showKey": "Mostrar Chave",
|
||||
"hideKey": "Ocultar Chave",
|
||||
"copyKey": "Copiar Chave de API",
|
||||
"allModels": "Todos os Modelos",
|
||||
"selectedModels": "Modelos Selecionados",
|
||||
"readOnly": "Somente Leitura",
|
||||
"fullAccess": "Acesso Total"
|
||||
},
|
||||
"auditLog": {
|
||||
"title": "Log de Auditoria",
|
||||
"searchPlaceholder": "Buscar ações...",
|
||||
"action": "Ação",
|
||||
"actor": "Autor",
|
||||
"target": "Alvo",
|
||||
"ipAddress": "Endereço IP",
|
||||
"timestamp": "Data/Hora",
|
||||
"noEntries": "Nenhum registro de auditoria",
|
||||
"filterByAction": "Filtrar por ação"
|
||||
},
|
||||
"cliTools": {
|
||||
"title": "Ferramentas CLI",
|
||||
"mapModels": "Mapear Modelos",
|
||||
"testConnection": "Testar Conexão",
|
||||
"connectionStatus": "Status da Conexão",
|
||||
"configureEndpoint": "Configurar Endpoint",
|
||||
"instructions": "Instruções",
|
||||
"modelMapping": "Mapeamento de Modelos",
|
||||
"baseUrl": "URL Base",
|
||||
"apiKey": "Chave de API"
|
||||
},
|
||||
"combos": {
|
||||
"title": "Combos",
|
||||
"createCombo": "Criar Combo",
|
||||
"editCombo": "Editar Combo",
|
||||
"deleteCombo": "Excluir Combo",
|
||||
"noModels": "Sem modelos",
|
||||
"noModelsYet": "Nenhum modelo adicionado",
|
||||
"addModel": "Adicionar Modelo",
|
||||
"routingStrategy": "Estratégia de Roteamento",
|
||||
"maxRetries": "Máximo de Tentativas",
|
||||
"timeout": "Timeout (ms)",
|
||||
"healthcheck": "Verificação de Saúde",
|
||||
"priority": "Prioridade",
|
||||
"fallback": "Fallback",
|
||||
"roundRobin": "Round Robin",
|
||||
"random": "Aleatório",
|
||||
"leastLatency": "Menor Latência",
|
||||
"comboName": "Nome do Combo",
|
||||
"comboNamePlaceholder": "ex: meu-combo-inteligente",
|
||||
"deleteConfirm": "Tem certeza que deseja excluir este combo?"
|
||||
},
|
||||
"costs": {
|
||||
"title": "Custos",
|
||||
"totalCost": "Custo Total",
|
||||
"breakdown": "Detalhamento de Custos",
|
||||
"noData": "Sem dados de custo",
|
||||
"byModel": "Por Modelo",
|
||||
"byProvider": "Por Provedor"
|
||||
},
|
||||
"endpoint": {
|
||||
"title": "Endpoint da API",
|
||||
"available": "Endpoints Disponíveis",
|
||||
"cloudProxy": "Proxy na Nuvem",
|
||||
"disableConfirm": "Tem certeza que deseja desativar o proxy na nuvem?",
|
||||
"baseUrl": "URL Base",
|
||||
"apiKeyLabel": "Chave de API",
|
||||
"registeredKeys": "Chaves Registradas",
|
||||
"chatCompletions": "Chat Completions",
|
||||
"responses": "Respostas",
|
||||
"listModels": "Listar Modelos"
|
||||
},
|
||||
"health": {
|
||||
"title": "Saúde do Sistema",
|
||||
"healthy": "Saudável",
|
||||
"degraded": "Degradado",
|
||||
"down": "Offline",
|
||||
"uptime": "Tempo Ativo",
|
||||
"memory": "Memória",
|
||||
"cpu": "CPU",
|
||||
"database": "Banco de Dados",
|
||||
"lastCheck": "Última Verificação",
|
||||
"providerHealth": "Saúde dos Provedores",
|
||||
"systemMetrics": "Métricas do Sistema",
|
||||
"tokenHealth": "Saúde dos Tokens",
|
||||
"refreshAll": "Atualizar Tudo",
|
||||
"checkNow": "Verificar Agora"
|
||||
},
|
||||
"limits": {
|
||||
"title": "Limites e Cotas",
|
||||
"rateLimit": "Limite de Taxa",
|
||||
"remaining": "Restante",
|
||||
"requestsPerMinute": "Requisições/min",
|
||||
"tokensPerMinute": "Tokens/min",
|
||||
"dailyLimit": "Limite Diário"
|
||||
},
|
||||
"logs": {
|
||||
"title": "Logs",
|
||||
"requestLogs": "Logs de Requisições",
|
||||
"proxyLogs": "Logs do Proxy",
|
||||
"auditLog": "Log de Auditoria",
|
||||
"console": "Console"
|
||||
},
|
||||
"onboarding": {
|
||||
"welcome": "Bem-vindo ao OmniRoute",
|
||||
"setPassword": "Definir Senha",
|
||||
"addProvider": "Adicione seu primeiro provedor",
|
||||
"getStarted": "Começar",
|
||||
"skip": "Pular",
|
||||
"passwordLabel": "Senha",
|
||||
"confirmPassword": "Confirmar Senha",
|
||||
"setupComplete": "Configuração Concluída!",
|
||||
"goToDashboard": "Ir para o Painel"
|
||||
},
|
||||
"providers": {
|
||||
"title": "Provedores",
|
||||
"addProvider": "Adicionar Provedor",
|
||||
"editProvider": "Editar Provedor",
|
||||
"deleteProvider": "Excluir Provedor",
|
||||
"noProviders": "Nenhum provedor configurado",
|
||||
"modelAvailability": "Disponibilidade de Modelos",
|
||||
"accounts": "Contas",
|
||||
"newAccount": "Nova Conta",
|
||||
"deleteConfirm": "Tem certeza que deseja excluir este provedor?",
|
||||
"testing": "Testando...",
|
||||
"testSuccess": "Conexão bem-sucedida",
|
||||
"testFailed": "Falha na conexão",
|
||||
"available": "Disponível",
|
||||
"unavailable": "Indisponível",
|
||||
"unknown": "Desconhecido"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Configurações",
|
||||
"general": "Geral",
|
||||
"security": "Segurança",
|
||||
"appearance": "Aparência",
|
||||
"routing": "Roteamento",
|
||||
"cache": "Cache",
|
||||
"resilience": "Resiliência",
|
||||
"systemPrompt": "Prompt do Sistema",
|
||||
"thinkingBudget": "Orçamento de Raciocínio",
|
||||
"proxy": "Proxy",
|
||||
"pricing": "Preços",
|
||||
"storage": "Armazenamento",
|
||||
"policies": "Políticas",
|
||||
"ipFilter": "Filtro de IP",
|
||||
"comboDefaults": "Padrões de Combo",
|
||||
"fallbackChains": "Cadeias de Fallback",
|
||||
"changePassword": "Alterar Senha",
|
||||
"enablePassword": "Ativar Senha",
|
||||
"darkMode": "Modo Escuro",
|
||||
"lightMode": "Modo Claro",
|
||||
"systemTheme": "Tema do Sistema",
|
||||
"enableCache": "Ativar Cache",
|
||||
"cacheTTL": "TTL do Cache",
|
||||
"maxCacheSize": "Tamanho Máximo do Cache",
|
||||
"clearCache": "Limpar Cache",
|
||||
"cacheHits": "Acertos de Cache",
|
||||
"cacheMisses": "Erros de Cache",
|
||||
"hitRate": "Taxa de Acerto",
|
||||
"cacheEntries": "Entradas no Cache",
|
||||
"circuitBreaker": "Disjuntor",
|
||||
"retryPolicy": "Política de Retentativa",
|
||||
"maxRetries": "Máximo de Tentativas",
|
||||
"retryDelay": "Intervalo de Retentativa",
|
||||
"timeoutMs": "Timeout (ms)",
|
||||
"enableSystemPrompt": "Ativar Prompt do Sistema",
|
||||
"systemPromptText": "Texto do Prompt do Sistema",
|
||||
"enableThinking": "Ativar Raciocínio",
|
||||
"maxThinkingTokens": "Máximo de Tokens de Raciocínio",
|
||||
"enableProxy": "Ativar Proxy",
|
||||
"proxyUrl": "URL do Proxy",
|
||||
"pricingRates": "Formato de Taxas de Preço",
|
||||
"currentPricing": "Visão Geral de Preços Atual",
|
||||
"loadingPricing": "Carregando dados de preços...",
|
||||
"noPricing": "Nenhum dado de preço disponível",
|
||||
"input": "Entrada",
|
||||
"output": "Saída",
|
||||
"cached": "Em Cache",
|
||||
"reasoning": "Raciocínio",
|
||||
"cacheCreation": "Criação de Cache",
|
||||
"customPricing": "Preços Personalizados",
|
||||
"databaseSize": "Tamanho do Banco de Dados",
|
||||
"backupDb": "Backup do Banco de Dados",
|
||||
"restoreDb": "Restaurar Banco de Dados",
|
||||
"exportData": "Exportar Dados",
|
||||
"importData": "Importar Dados",
|
||||
"clearData": "Limpar Todos os Dados",
|
||||
"clearDataConfirm": "Isso excluirá permanentemente todos os dados. Tem certeza?",
|
||||
"enableRequestLogs": "Ativar Logs de Requisição",
|
||||
"logRetention": "Retenção de Logs",
|
||||
"ipWhitelist": "Lista de IPs Permitidos",
|
||||
"ipBlacklist": "Lista de IPs Bloqueados",
|
||||
"addIP": "Adicionar IP",
|
||||
"savedSuccessfully": "Configurações salvas com sucesso"
|
||||
},
|
||||
"translator": {
|
||||
"title": "Tradutor",
|
||||
"realtime": "Atividade de Tradução em Tempo Real",
|
||||
"chatTester": "Testador de Chat",
|
||||
"testBench": "Bancada de Testes",
|
||||
"recentTranslations": "Traduções Recentes",
|
||||
"noTranslations": "Nenhuma tradução ainda",
|
||||
"source": "Origem",
|
||||
"target": "Destino",
|
||||
"latency": "Latência",
|
||||
"formatConverter": "Conversor de Formato",
|
||||
"input": "Entrada",
|
||||
"output": "Saída",
|
||||
"exampleTemplates": "Modelos de Exemplo",
|
||||
"compatibilityTester": "Testador de Compatibilidade",
|
||||
"compatibilityReport": "Relatório de Compatibilidade",
|
||||
"pipelineDebugger": "Depurador de Pipeline",
|
||||
"translationPipeline": "Pipeline de Tradução",
|
||||
"pipelineVisualization": "Visualização do pipeline"
|
||||
},
|
||||
"usage": {
|
||||
"title": "Uso",
|
||||
"budgetManagement": "Gerenciamento de Orçamento",
|
||||
"apiKey": "Chave de API",
|
||||
"thisMonth": "Este Mês",
|
||||
"setLimits": "Definir Limites",
|
||||
"totalRequests": "Total de requisições",
|
||||
"noDataYet": "Sem dados ainda",
|
||||
"entries": "Entradas",
|
||||
"hitRate": "Taxa de Acerto",
|
||||
"hitsMisses": "Acertos / Erros",
|
||||
"circuitBreakers": "Disjuntores",
|
||||
"lockedIPs": "IPs Bloqueados",
|
||||
"howItWorks": "Como Funciona",
|
||||
"define": "Definir",
|
||||
"run": "Executar",
|
||||
"evaluate": "Avaliar",
|
||||
"evalSuites": "Suítes de Avaliação",
|
||||
"modelEvals": "Avaliações de Modelos",
|
||||
"modelLockouts": "Bloqueios de Modelo",
|
||||
"noLockouts": "Nenhum modelo bloqueado",
|
||||
"activeSessions": "Sessões Ativas",
|
||||
"noSessions": "Sem sessões ativas",
|
||||
"sessionsHint": "Sessões aparecem conforme requisições passam pelo proxy",
|
||||
"session": "Sessão",
|
||||
"age": "Idade",
|
||||
"requests": "Requisições",
|
||||
"connection": "Conexão",
|
||||
"providerLimits": "Limites do Provedor",
|
||||
"noProviders": "Nenhum Provedor Conectado",
|
||||
"modelQuotas": "Cotas de Modelo",
|
||||
"noQuotaData": "Sem dados de cota",
|
||||
"noQuotaDataAvailable": "Nenhum dado de cota disponível"
|
||||
},
|
||||
"modals": {
|
||||
"waitingAuth": "Aguardando Autorização",
|
||||
"verificationUrl": "URL de Verificação",
|
||||
"yourCode": "Seu Código",
|
||||
"remoteAccess": "Acesso remoto:",
|
||||
"connectedSuccess": "Conectado com Sucesso!",
|
||||
"connectionFailed": "Falha na Conexão",
|
||||
"chooseAuthMethod": "Escolha seu método de autenticação:",
|
||||
"awsBuilderId": "AWS Builder ID",
|
||||
"awsIamIdentity": "AWS IAM Identity Center",
|
||||
"googleAccount": "Conta Google",
|
||||
"githubAccount": "Conta GitHub",
|
||||
"importToken": "Importar Token",
|
||||
"pasteToken": "Cole o refresh token do Kiro IDE.",
|
||||
"awsRegion": "Região AWS",
|
||||
"autoDetecting": "Detectando tokens automaticamente...",
|
||||
"readingFromCache": "Lendo do cache AWS SSO",
|
||||
"readingFromCursor": "Lendo do banco de dados do Cursor IDE",
|
||||
"initializing": "Inicializando...",
|
||||
"pricingConfig": "Configuração de Preços",
|
||||
"loadingPricing": "Carregando dados de preços...",
|
||||
"pricingRatesFormat": "Formato de Taxas de Preço",
|
||||
"noPricingData": "Nenhum dado de preço disponível",
|
||||
"noModelsFound": "Nenhum modelo encontrado"
|
||||
},
|
||||
"loggers": {
|
||||
"allProviders": "Todos os Provedores",
|
||||
"allModels": "Todos os Modelos",
|
||||
"allAccounts": "Todas as Contas",
|
||||
"allApiKeys": "Todas as Chaves de API",
|
||||
"allTypes": "Todos os Tipos",
|
||||
"allLevels": "Todos os Níveis",
|
||||
"modelAZ": "Modelo A-Z",
|
||||
"modelZA": "Modelo Z-A",
|
||||
"loadingLogs": "Carregando logs...",
|
||||
"loadingProxyLogs": "Carregando logs do proxy...",
|
||||
"noLogEntries": "Nenhuma entrada de log encontrada",
|
||||
"noPayloadData": "Nenhum dado de payload disponível para esta entrada.",
|
||||
"proxyEvent": "Evento do Proxy",
|
||||
"proxy": "Proxy",
|
||||
"level": "Nível",
|
||||
"directNative": "Direto (nativo)",
|
||||
"combo": "Combo",
|
||||
"inputTokens": "E:",
|
||||
"outputTokens": "S:"
|
||||
},
|
||||
"stats": {
|
||||
"usageOverview": "Visão Geral de Uso",
|
||||
"outputTokens": "Tokens de Saída",
|
||||
"totalCost": "Custo Total",
|
||||
"usageByModel": "Uso por Modelo",
|
||||
"usageByAccount": "Uso por Conta",
|
||||
"failedToLoad": "Falha ao carregar estatísticas de uso.",
|
||||
"tokenHealth": "Saúde dos Tokens",
|
||||
"totalOAuth": "Total OAuth",
|
||||
"healthy": "Saudável",
|
||||
"errored": "Com Erro",
|
||||
"lastCheck": "Última verificação",
|
||||
"noData": "Sem dados",
|
||||
"share": "Compartilhar",
|
||||
"unableToLoad": "Não foi possível carregar métricas do sistema",
|
||||
"product": "Produto",
|
||||
"resources": "Recursos",
|
||||
"company": "Empresa"
|
||||
},
|
||||
"auth": {
|
||||
"welcome": "Bem-vindo",
|
||||
"signIn": "Entrar",
|
||||
"enterPassword": "Digite sua senha para continuar",
|
||||
"password": "Senha",
|
||||
"unifiedProxy": "Proxy Unificado de API de IA",
|
||||
"passwordNotEnabled": "Proteção por senha não está ativada",
|
||||
"resetPassword": "Redefinir Senha",
|
||||
"resetDescription": "Escolha um método para recuperar acesso ao painel",
|
||||
"stopServer": "Pare o servidor OmniRoute",
|
||||
"processing": "Processando...",
|
||||
"pleaseWait": "Aguarde enquanto completamos a autorização.",
|
||||
"authSuccess": "Autorização bem-sucedida!",
|
||||
"copyUrl": "Copiar esta URL",
|
||||
"accessDenied": "Acesso Negado",
|
||||
"forgotPassword": "Esqueceu a senha?"
|
||||
},
|
||||
"landing": {
|
||||
"allProviders": "Todos os Provedores de IA",
|
||||
"oneEndpoint": "Um Endpoint",
|
||||
"powerfulFeatures": "Recursos Poderosos",
|
||||
"howItWorks": "Como o OmniRoute Funciona",
|
||||
"installOmniRoute": "Instalar o OmniRoute",
|
||||
"openDashboard": "Abrir Painel",
|
||||
"routeRequests": "Rotear Requisições",
|
||||
"dataLocation": "Local dos Dados:",
|
||||
"getStarted": "Começar",
|
||||
"product": "Produto",
|
||||
"resources": "Recursos",
|
||||
"legal": "Legal",
|
||||
"interactiveDiagram": "Diagrama interativo visível no desktop"
|
||||
},
|
||||
"docs": {
|
||||
"title": "Documentação",
|
||||
"quickStart": "Início Rápido",
|
||||
"features": "Recursos",
|
||||
"supportedProviders": "Provedores Suportados",
|
||||
"commonUseCases": "Casos de Uso Comuns",
|
||||
"clientCompatibility": "Compatibilidade de Clientes",
|
||||
"apiReference": "Referência da API",
|
||||
"method": "Método",
|
||||
"path": "Caminho",
|
||||
"notes": "Notas",
|
||||
"modelPrefixes": "Prefixos de Modelo",
|
||||
"prefix": "Prefixo",
|
||||
"troubleshooting": "Solução de Problemas",
|
||||
"supportsChat": "Suporta endpoints de chat e responses.",
|
||||
"oauthAutoRefresh": "Conexão OAuth com atualização automática de token.",
|
||||
"fullStreaming": "Suporte completo a streaming para todos os modelos."
|
||||
},
|
||||
"legal": {
|
||||
"privacyPolicy": "Política de Privacidade",
|
||||
"termsOfService": "Termos de Serviço",
|
||||
"providerConfigurations": "Configurações de provedores",
|
||||
"apiKeys": "Chaves de API",
|
||||
"usageLogs": "Logs de uso",
|
||||
"applicationSettings": "Configurações do aplicativo",
|
||||
"viewExportAnalytics": "Visualizar e exportar análises de uso",
|
||||
"clearHistory": "Limpar histórico de uso a qualquer momento",
|
||||
"configureRetention": "Configurar políticas de retenção de logs",
|
||||
"backupRestore": "Fazer backup e restaurar seu banco de dados"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
/**
|
||||
* Console Log Viewer — Real-time application log viewer.
|
||||
*
|
||||
@@ -42,6 +44,7 @@ const LEVEL_BG: Record<string, string> = {
|
||||
const POLL_INTERVAL = 5000; // 5 seconds
|
||||
|
||||
export default function ConsoleLogViewer() {
|
||||
const t = useTranslations("loggers");
|
||||
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -131,7 +134,7 @@ export default function ConsoleLogViewer() {
|
||||
aria-label="Filter by log level"
|
||||
className="px-3 py-2 rounded-lg text-sm bg-[var(--color-bg)] border border-[var(--color-border)] text-[var(--color-text-main)] focus:outline-2 focus:outline-[var(--color-accent)]"
|
||||
>
|
||||
<option value="all">All Levels</option>
|
||||
<option value="all">{t("allLevels")}</option>
|
||||
<option value="debug">Debug+</option>
|
||||
<option value="info">Info+</option>
|
||||
<option value="warn">Warn+</option>
|
||||
@@ -226,7 +229,7 @@ export default function ConsoleLogViewer() {
|
||||
<span className="material-symbols-outlined text-[40px] block mb-2 opacity-30">
|
||||
terminal
|
||||
</span>
|
||||
<p>No log entries found</p>
|
||||
<p>{t("noLogEntries")}</p>
|
||||
<p className="text-[10px] mt-1 opacity-60">
|
||||
Ensure LOG_TO_FILE=true is set in your .env file
|
||||
</p>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import Link from "next/link";
|
||||
import { APP_CONFIG } from "@/shared/constants/config";
|
||||
|
||||
@@ -36,6 +38,7 @@ const footerLinks = {
|
||||
};
|
||||
|
||||
export default function Footer() {
|
||||
const t = useTranslations("stats");
|
||||
const renderFooterLink = (link) => {
|
||||
if (link.external) {
|
||||
return (
|
||||
@@ -106,7 +109,7 @@ export default function Footer() {
|
||||
|
||||
{/* Product */}
|
||||
<div>
|
||||
<h4 className="font-semibold text-text-main mb-4">Product</h4>
|
||||
<h4 className="font-semibold text-text-main mb-4">{t(">product</")}</h4>
|
||||
<ul className="flex flex-col gap-3 text-sm text-text-muted font-light">
|
||||
{footerLinks.product.map((link) => (
|
||||
<li key={link.label}>{renderFooterLink(link)}</li>
|
||||
@@ -116,7 +119,7 @@ export default function Footer() {
|
||||
|
||||
{/* Resources */}
|
||||
<div>
|
||||
<h4 className="font-semibold text-text-main mb-4">Resources</h4>
|
||||
<h4 className="font-semibold text-text-main mb-4">{t(">resources</")}</h4>
|
||||
<ul className="flex flex-col gap-3 text-sm text-text-muted font-light">
|
||||
{footerLinks.resources.map((link) => (
|
||||
<li key={link.label}>{renderFooterLink(link)}</li>
|
||||
@@ -126,7 +129,7 @@ export default function Footer() {
|
||||
|
||||
{/* Company */}
|
||||
<div>
|
||||
<h4 className="font-semibold text-text-main mb-4">Company</h4>
|
||||
<h4 className="font-semibold text-text-main mb-4">{t(">company</")}</h4>
|
||||
<ul className="flex flex-col gap-3 text-sm text-text-muted font-light">
|
||||
{footerLinks.company.map((link) => (
|
||||
<li key={link.label}>{renderFooterLink(link)}</li>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
/**
|
||||
* SystemMonitor — Real-time system metrics widget
|
||||
*
|
||||
@@ -51,6 +53,7 @@ function MetricRow({ icon, label, value, color = "text-text-main" }) {
|
||||
}
|
||||
|
||||
export default function SystemMonitor({ compact = false }) {
|
||||
const t = useTranslations("stats");
|
||||
const [metrics, setMetrics] = useState(null);
|
||||
const [error, setError] = useState(false);
|
||||
const mountedRef = useRef(true);
|
||||
@@ -85,7 +88,7 @@ export default function SystemMonitor({ compact = false }) {
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-2 text-text-muted text-sm">
|
||||
<span className="material-symbols-outlined text-[18px] text-red-400">error</span>
|
||||
<span>Unable to load system metrics</span>
|
||||
<span>{t("unableToLoad")}</span>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
/**
|
||||
* TokenHealthBadge — Batch G
|
||||
*
|
||||
@@ -17,6 +19,7 @@ const STATUS_MAP = {
|
||||
};
|
||||
|
||||
export default function TokenHealthBadge() {
|
||||
const t = useTranslations("stats");
|
||||
const [health, setHealth] = useState(null);
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
|
||||
@@ -71,31 +74,31 @@ export default function TokenHealthBadge() {
|
||||
backdropFilter: "blur(12px)",
|
||||
}}
|
||||
>
|
||||
<p className="text-xs font-medium text-text-main mb-2">Token Health</p>
|
||||
<p className="text-xs font-medium text-text-main mb-2">{t("tokenHealth")}</p>
|
||||
<div className="flex flex-col gap-1 text-xs">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-muted">Total OAuth</span>
|
||||
<span className="text-text-muted">{t("totalOAuth")}</span>
|
||||
<span className="text-text-main">{health.total}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-emerald-400">Healthy</span>
|
||||
<span className="text-emerald-400">{t(">healthy</")}</span>
|
||||
<span className="text-text-main">{health.healthy}</span>
|
||||
</div>
|
||||
{health.errored > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-red-400">Errored</span>
|
||||
<span className="text-red-400">{t(">errored</")}</span>
|
||||
<span className="text-text-main">{health.errored}</span>
|
||||
</div>
|
||||
)}
|
||||
{health.warning > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-amber-400">Warning</span>
|
||||
<span className="text-amber-400">{t(">warning</")}</span>
|
||||
<span className="text-text-main">{health.warning}</span>
|
||||
</div>
|
||||
)}
|
||||
{health.lastCheckAt && (
|
||||
<div className="flex justify-between mt-1 pt-1 border-t border-white/5">
|
||||
<span className="text-text-muted">Last check</span>
|
||||
<span className="text-text-muted">{t("lastCheck")}</span>
|
||||
<span className="text-text-muted">
|
||||
{new Date(health.lastCheckAt).toLocaleTimeString()}
|
||||
</span>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { useState, useEffect, useMemo, useCallback, useRef } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { useSearchParams, useRouter } from "next/navigation";
|
||||
@@ -8,7 +10,15 @@ import Badge from "./Badge";
|
||||
import { CardSkeleton } from "./Loading";
|
||||
import { fmtFull, fmtCost } from "@/shared/utils/formatting";
|
||||
|
||||
function SortIcon({ field, currentSort, currentOrder }: { field: string; currentSort: string; currentOrder: string }) {
|
||||
function SortIcon({
|
||||
field,
|
||||
currentSort,
|
||||
currentOrder,
|
||||
}: {
|
||||
field: string;
|
||||
currentSort: string;
|
||||
currentOrder: string;
|
||||
}) {
|
||||
if (currentSort !== field) return <span className="ml-1 opacity-20">↕</span>;
|
||||
return <span className="ml-1">{currentOrder === "asc" ? "↑" : "↓"}</span>;
|
||||
}
|
||||
@@ -19,7 +29,13 @@ SortIcon.propTypes = {
|
||||
currentOrder: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
function MiniBarGraph({ data, colorClass = "bg-primary" }: { data: number[]; colorClass?: string }) {
|
||||
function MiniBarGraph({
|
||||
data,
|
||||
colorClass = "bg-primary",
|
||||
}: {
|
||||
data: number[];
|
||||
colorClass?: string;
|
||||
}) {
|
||||
const max = Math.max(...data, 1);
|
||||
return (
|
||||
<div className="flex items-end gap-1 h-8 w-24">
|
||||
@@ -41,6 +57,7 @@ MiniBarGraph.propTypes = {
|
||||
};
|
||||
|
||||
export default function UsageStats() {
|
||||
const t = useTranslations("stats");
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
@@ -188,7 +205,7 @@ export default function UsageStats() {
|
||||
|
||||
if (loading) return <CardSkeleton />;
|
||||
|
||||
if (!stats) return <div className="text-text-muted">Failed to load usage statistics.</div>;
|
||||
if (!stats) return <div className="text-text-muted">{t("failedToLoad")}</div>;
|
||||
|
||||
// Format number with commas — delegated to shared module
|
||||
const fmt = (n: number) => fmtFull(n);
|
||||
@@ -213,7 +230,7 @@ export default function UsageStats() {
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Header with Auto Refresh Toggle and View Toggle */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold">Usage Overview</h2>
|
||||
<h2 className="text-xl font-semibold">{t("usageOverview")}</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* View Toggle */}
|
||||
<div className="flex items-center gap-1 bg-bg-subtle rounded-lg p-1 border border-border">
|
||||
@@ -331,21 +348,25 @@ export default function UsageStats() {
|
||||
<Card className="px-4 py-2 flex flex-col gap-1">
|
||||
<div className="flex justify-between items-start gap-4">
|
||||
<div className="flex flex-col gap-1 flex-1">
|
||||
<span className="text-text-muted text-sm uppercase font-semibold">Output Tokens</span>
|
||||
<span className="text-text-muted text-sm uppercase font-semibold">
|
||||
{t("outputTokens")}
|
||||
</span>
|
||||
<span className="text-2xl font-bold text-success">
|
||||
{fmt(stats.totalCompletionTokens)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-px bg-border self-stretch mx-2" />
|
||||
<div className="flex flex-col gap-1 flex-1">
|
||||
<span className="text-text-muted text-sm uppercase font-semibold">Total Cost</span>
|
||||
<span className="text-text-muted text-sm uppercase font-semibold">
|
||||
{t("totalCost")}
|
||||
</span>
|
||||
<span className="text-2xl font-bold text-warning">{fmtCost(stats.totalCost)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Usage by Model Table */}
|
||||
{/* {t("usageByModel")} Table */}
|
||||
<Card className="overflow-hidden">
|
||||
<div className="p-4 border-b border-border bg-bg-subtle/50">
|
||||
<h3 className="font-semibold">Usage by Model</h3>
|
||||
@@ -504,7 +525,7 @@ export default function UsageStats() {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Usage by Account Table */}
|
||||
{/* {t("usageByAccount")} Table */}
|
||||
<Card className="overflow-hidden">
|
||||
<div className="p-4 border-b border-border bg-bg-subtle/50">
|
||||
<h3 className="font-semibold">Usage by Account</h3>
|
||||
|
||||
Reference in New Issue
Block a user