mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 13:52:09 +03:00
feat(i18n): replace hardcoded strings with translation keys in HomePageClient
Replace hardcoded English text in quick start links and getting started steps with proper i18n translation keys. Use `t.rich()` for step descriptions containing inline links to support rich text interpolation.
This commit is contained in:
@@ -14,6 +14,7 @@ import { useNotificationStore } from "@/store/notificationStore";
|
||||
export default function HomePageClient({ machineId }) {
|
||||
const t = useTranslations("home");
|
||||
const tc = useTranslations("common");
|
||||
const ts = useTranslations("sidebar");
|
||||
const [providerConnections, setProviderConnections] = useState([]);
|
||||
const [models, setModels] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -108,11 +109,11 @@ export default function HomePageClient({ machineId }) {
|
||||
|
||||
const quickStartLinks = [
|
||||
{ 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: ts("providers"), href: "/dashboard/providers", icon: "dns" },
|
||||
{ label: ts("combos"), href: "/dashboard/combos", icon: "layers" },
|
||||
{ label: ts("analytics"), href: "/dashboard/analytics", icon: "analytics" },
|
||||
{ label: t("healthMonitor"), href: "/dashboard/health", icon: "health_and_safety" },
|
||||
{ label: "CLI Tools", href: "/dashboard/cli-tools", icon: "terminal" },
|
||||
{ label: ts("cliTools"), href: "/dashboard/cli-tools", icon: "terminal" },
|
||||
{
|
||||
label: t("reportIssue"),
|
||||
href: "https://github.com/diegosouzapw/OmniRoute/issues",
|
||||
@@ -159,11 +160,13 @@ export default function HomePageClient({ machineId }) {
|
||||
<div>
|
||||
<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">
|
||||
Endpoint
|
||||
</Link>{" "}
|
||||
→ Registered Keys. Generate one key per environment.
|
||||
{t.rich("step1Desc", {
|
||||
endpoint: (chunks) => (
|
||||
<Link href="/dashboard/endpoint" className="text-primary hover:underline">
|
||||
{chunks}
|
||||
</Link>
|
||||
),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
@@ -174,11 +177,13 @@ export default function HomePageClient({ machineId }) {
|
||||
<div>
|
||||
<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">
|
||||
Providers
|
||||
</Link>
|
||||
. Supports OAuth, API Key, and free tiers.
|
||||
{t.rich("step2Desc", {
|
||||
providers: (chunks) => (
|
||||
<Link href="/dashboard/providers" className="text-primary hover:underline">
|
||||
{chunks}
|
||||
</Link>
|
||||
),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
@@ -188,13 +193,7 @@ export default function HomePageClient({ machineId }) {
|
||||
</div>
|
||||
<div>
|
||||
<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">
|
||||
{currentEndpoint}
|
||||
</code>{" "}
|
||||
in your IDE or API client.
|
||||
</p>
|
||||
<p className="text-text-muted mt-0.5">{t("step3Desc", { url: currentEndpoint })}</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className="rounded-lg border border-border bg-bg-subtle p-4 flex gap-3">
|
||||
@@ -204,15 +203,18 @@ export default function HomePageClient({ machineId }) {
|
||||
<div>
|
||||
<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">
|
||||
Request Logs
|
||||
</Link>{" "}
|
||||
and{" "}
|
||||
<Link href="/dashboard/analytics" className="text-primary hover:underline">
|
||||
Analytics
|
||||
</Link>
|
||||
.
|
||||
{t.rich("step4Desc", {
|
||||
logs: (chunks) => (
|
||||
<Link href="/dashboard/usage" className="text-primary hover:underline">
|
||||
{chunks}
|
||||
</Link>
|
||||
),
|
||||
analytics: (chunks) => (
|
||||
<Link href="/dashboard/analytics" className="text-primary hover:underline">
|
||||
{chunks}
|
||||
</Link>
|
||||
),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
@@ -243,20 +245,22 @@ export default function HomePageClient({ machineId }) {
|
||||
<div>
|
||||
<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
|
||||
{t("configuredOf", {
|
||||
configured: providerStats.filter((item) => item.total > 0).length,
|
||||
total: providerStats.length,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="hidden sm:flex items-center gap-3 text-[11px] text-text-muted">
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="size-2 rounded-full bg-green-500" /> Free
|
||||
<span className="size-2 rounded-full bg-green-500" /> {tc("free")}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="size-2 rounded-full bg-blue-500" /> OAuth
|
||||
<span className="size-2 rounded-full bg-blue-500" /> {t("oauthLabel")}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="size-2 rounded-full bg-amber-500" /> API Key
|
||||
<span className="size-2 rounded-full bg-amber-500" /> {t("apiKeyLabel")}
|
||||
</span>
|
||||
</div>
|
||||
<Link
|
||||
@@ -264,7 +268,7 @@ export default function HomePageClient({ machineId }) {
|
||||
className="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]">settings</span>
|
||||
Manage
|
||||
{tc("manage")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -299,15 +303,16 @@ HomePageClient.propTypes = {
|
||||
|
||||
function ProviderOverviewCard({ item, metrics, onClick }) {
|
||||
const [imgError, setImgError] = useState(false);
|
||||
const t = useTranslations("home");
|
||||
const tc = useTranslations("common");
|
||||
|
||||
const statusVariant =
|
||||
item.errors > 0 ? "text-red-500" : item.connected > 0 ? "text-green-500" : "text-text-muted";
|
||||
|
||||
const authTypeConfig = {
|
||||
free: { color: "bg-green-500", label: "Free" },
|
||||
oauth: { color: "bg-blue-500", label: "OAuth" },
|
||||
apikey: { color: "bg-amber-500", label: "API Key" },
|
||||
free: { color: "bg-green-500", label: tc("free") },
|
||||
oauth: { color: "bg-blue-500", label: t("oauthLabel") },
|
||||
apikey: { color: "bg-amber-500", label: t("apiKeyLabel") },
|
||||
};
|
||||
const authInfo = authTypeConfig[item.authType] || authTypeConfig.apikey;
|
||||
|
||||
@@ -352,13 +357,13 @@ function ProviderOverviewCard({ item, metrics, onClick }) {
|
||||
<p className={`text-xs ${statusVariant}`}>
|
||||
{item.total === 0
|
||||
? tc("notConfigured")
|
||||
: `${item.connected} active · ${item.errors} error`}
|
||||
: t("activeError", { active: item.connected, errors: item.errors })}
|
||||
</p>
|
||||
{metrics && metrics.totalRequests > 0 && (
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
<span className="text-[10px] text-text-muted">
|
||||
<span className="text-emerald-500">{metrics.totalSuccesses}</span>/
|
||||
{metrics.totalRequests} reqs
|
||||
{t("requestsShort", { count: metrics.totalRequests })}
|
||||
</span>
|
||||
<span className="text-[10px] text-text-muted">{metrics.successRate}%</span>
|
||||
<span className="text-[10px] text-text-muted">~{metrics.avgLatencyMs}ms</span>
|
||||
@@ -368,7 +373,7 @@ function ProviderOverviewCard({ item, metrics, onClick }) {
|
||||
|
||||
<div className="text-right shrink-0">
|
||||
<p className="text-xs font-medium text-text-main">{item.modelCount}</p>
|
||||
<p className="text-[10px] text-text-muted">models</p>
|
||||
<p className="text-[10px] text-text-muted">{tc("models")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
@@ -406,6 +411,7 @@ function ProviderModelsModal({ provider, models, onClose }) {
|
||||
const router = useRouter();
|
||||
const t = useTranslations("home");
|
||||
const tc = useTranslations("common");
|
||||
const ts = useTranslations("sidebar");
|
||||
|
||||
const navigateTo = (path) => {
|
||||
onClose();
|
||||
@@ -415,20 +421,29 @@ function ProviderModelsModal({ provider, models, onClose }) {
|
||||
const handleCopy = (text) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
setCopiedModel(text);
|
||||
notify.success(`Copied: ${text}`);
|
||||
notify.success(t("copiedModel", { model: text }));
|
||||
setTimeout(() => setCopiedModel(null), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={true} title={`${provider.provider.name} — Models`} onClose={onClose}>
|
||||
<Modal
|
||||
isOpen={true}
|
||||
title={t("providerModelsTitle", { provider: provider.provider.name })}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* Summary */}
|
||||
<div className="flex items-center gap-2 text-sm text-text-muted">
|
||||
<span className="material-symbols-outlined text-[16px]">token</span>
|
||||
{models.length} model{models.length !== 1 ? "s" : ""} available
|
||||
{models.length === 1
|
||||
? t("modelAvailable", { count: models.length })
|
||||
: t("modelsAvailable", { count: models.length })}
|
||||
{provider.total > 0 && (
|
||||
<span className="ml-auto text-xs text-green-500">
|
||||
● {provider.connected} connection{provider.connected !== 1 ? "s" : ""} active
|
||||
●{" "}
|
||||
{provider.connected === 1
|
||||
? t("connectionsActive", { count: provider.connected })
|
||||
: t("connectionsActivePlural", { count: provider.connected })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -440,13 +455,7 @@ function ProviderModelsModal({ provider, models, onClose }) {
|
||||
</span>
|
||||
<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
|
||||
onClick={() => navigateTo("/dashboard/providers")}
|
||||
className="text-primary hover:underline cursor-pointer"
|
||||
>
|
||||
Providers
|
||||
</button>
|
||||
{t("configureFirst", { providers: ts("providers") })}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
@@ -459,7 +468,9 @@ function ProviderModelsModal({ provider, models, onClose }) {
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="font-mono text-sm text-text-main truncate">{m.fullModel}</p>
|
||||
{m.alias !== m.model && (
|
||||
<p className="text-[10px] text-text-muted">alias: {m.alias}</p>
|
||||
<p className="text-[10px] text-text-muted">
|
||||
{t("aliasLabel")}: {m.alias}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
@@ -489,7 +500,7 @@ function ProviderModelsModal({ provider, models, onClose }) {
|
||||
{t("configureProvider")}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={onClose}>
|
||||
Close
|
||||
{tc("close")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -50,11 +50,11 @@ export default function AuditLogPage() {
|
||||
setHasMore(data.length > PAGE_SIZE);
|
||||
setEntries(data.slice(0, PAGE_SIZE));
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Failed to fetch audit log");
|
||||
setError(err.message || t("failedFetchAuditLog"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [actionFilter, actorFilter, offset]);
|
||||
}, [actionFilter, actorFilter, offset, t]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchEntries();
|
||||
@@ -96,7 +96,7 @@ export default function AuditLogPage() {
|
||||
<button
|
||||
onClick={fetchEntries}
|
||||
disabled={loading}
|
||||
aria-label="Refresh audit log"
|
||||
aria-label={t("refreshAuditLogAria")}
|
||||
className="px-4 py-2 rounded-lg text-sm font-medium bg-[var(--color-surface)] border border-[var(--color-border)] text-[var(--color-text-main)] hover:bg-[var(--color-bg-alt)] transition-colors disabled:opacity-50"
|
||||
>
|
||||
{loading ? tc("loading") : tc("refresh")}
|
||||
@@ -107,7 +107,7 @@ export default function AuditLogPage() {
|
||||
<div
|
||||
className="flex flex-wrap gap-3 p-4 rounded-xl bg-[var(--color-surface)] border border-[var(--color-border)]"
|
||||
role="search"
|
||||
aria-label="Filter audit log entries"
|
||||
aria-label={t("filterEntriesAria")}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
@@ -115,7 +115,7 @@ export default function AuditLogPage() {
|
||||
value={actionFilter}
|
||||
onChange={(e) => setActionFilter(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
|
||||
aria-label="Filter by action type"
|
||||
aria-label={t("filterByActionTypeAria")}
|
||||
className="flex-1 min-w-[180px] px-3 py-2 rounded-lg text-sm bg-[var(--color-bg)] border border-[var(--color-border)] text-[var(--color-text-main)] placeholder:text-[var(--color-text-muted)] focus:outline-2 focus:outline-[var(--color-accent)]"
|
||||
/>
|
||||
<input
|
||||
@@ -124,7 +124,7 @@ export default function AuditLogPage() {
|
||||
value={actorFilter}
|
||||
onChange={(e) => setActorFilter(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
|
||||
aria-label="Filter by actor"
|
||||
aria-label={t("filterByActorAria")}
|
||||
className="flex-1 min-w-[180px] px-3 py-2 rounded-lg text-sm bg-[var(--color-bg)] border border-[var(--color-border)] text-[var(--color-text-main)] placeholder:text-[var(--color-text-muted)] focus:outline-2 focus:outline-[var(--color-accent)]"
|
||||
/>
|
||||
<button
|
||||
@@ -147,7 +147,7 @@ export default function AuditLogPage() {
|
||||
|
||||
{/* Table */}
|
||||
<div className="overflow-x-auto rounded-xl border border-[var(--color-border)]">
|
||||
<table className="w-full text-sm" role="table" aria-label="Audit log entries">
|
||||
<table className="w-full text-sm" role="table" aria-label={t("tableAria")}>
|
||||
<thead>
|
||||
<tr className="bg-[var(--color-bg-alt)] border-b border-[var(--color-border)]">
|
||||
<th className="text-left px-4 py-3 font-medium text-[var(--color-text-muted)]">
|
||||
@@ -195,13 +195,13 @@ export default function AuditLogPage() {
|
||||
</td>
|
||||
<td className="px-4 py-3 text-[var(--color-text-main)]">{entry.actor}</td>
|
||||
<td className="px-4 py-3 text-[var(--color-text-muted)] max-w-[200px] truncate">
|
||||
{entry.target || "—"}
|
||||
{entry.target || t("notAvailable")}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-[var(--color-text-muted)] max-w-[300px] truncate font-mono text-xs">
|
||||
{entry.details ? JSON.stringify(entry.details) : "—"}
|
||||
{entry.details ? JSON.stringify(entry.details) : t("notAvailable")}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-[var(--color-text-muted)] font-mono text-xs whitespace-nowrap">
|
||||
{entry.ip_address || "—"}
|
||||
{entry.ip_address || t("notAvailable")}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card, Button, Badge, Modal, Input, ModelSelectModal } from "@/shared/components";
|
||||
import Image from "next/image";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function AntigravityToolCard({
|
||||
tool,
|
||||
@@ -14,6 +15,7 @@ export default function AntigravityToolCard({
|
||||
hasActiveProviders,
|
||||
cloudEnabled,
|
||||
}) {
|
||||
const t = useTranslations("cliTools");
|
||||
const [status, setStatus] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showPasswordModal, setShowPasswordModal] = useState(false);
|
||||
@@ -104,12 +106,12 @@ export default function AntigravityToolCard({
|
||||
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
setMessage({ type: "success", text: "MITM started" });
|
||||
setMessage({ type: "success", text: t("mitmStarted") });
|
||||
setShowPasswordModal(false);
|
||||
setSudoPassword("");
|
||||
fetchStatus();
|
||||
} else {
|
||||
setMessage({ type: "error", text: data.error || "Failed to start" });
|
||||
setMessage({ type: "error", text: data.error || t("failedStart") });
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage({ type: "error", text: error.message });
|
||||
@@ -130,12 +132,12 @@ export default function AntigravityToolCard({
|
||||
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
setMessage({ type: "success", text: "MITM stopped" });
|
||||
setMessage({ type: "success", text: t("mitmStopped") });
|
||||
setShowPasswordModal(false);
|
||||
setSudoPassword("");
|
||||
fetchStatus();
|
||||
} else {
|
||||
setMessage({ type: "error", text: data.error || "Failed to stop" });
|
||||
setMessage({ type: "error", text: data.error || t("failedStop") });
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage({ type: "error", text: error.message });
|
||||
@@ -146,7 +148,7 @@ export default function AntigravityToolCard({
|
||||
|
||||
const handleConfirmPassword = () => {
|
||||
if (!sudoPassword.trim()) {
|
||||
setMessage({ type: "error", text: "Sudo password is required" });
|
||||
setMessage({ type: "error", text: t("sudoPasswordRequiredError") });
|
||||
return;
|
||||
}
|
||||
if (status?.running) {
|
||||
@@ -190,10 +192,10 @@ export default function AntigravityToolCard({
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
throw new Error(data.error || "Failed to save mappings");
|
||||
throw new Error(data.error || t("failedSaveMappings"));
|
||||
}
|
||||
|
||||
setMessage({ type: "success", text: "Mappings saved!" });
|
||||
setMessage({ type: "success", text: t("mappingsSaved") });
|
||||
} catch (error) {
|
||||
setMessage({ type: "error", text: error.message });
|
||||
} finally {
|
||||
@@ -225,15 +227,15 @@ export default function AntigravityToolCard({
|
||||
<h3 className="font-medium text-sm">{tool.name}</h3>
|
||||
{isRunning ? (
|
||||
<Badge variant="success" size="sm">
|
||||
Active
|
||||
{t("active")}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="default" size="sm">
|
||||
Inactive
|
||||
{t("inactive")}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-text-muted truncate">{tool.description}</p>
|
||||
<p className="text-xs text-text-muted truncate">{t("toolDescriptions.antigravity")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
@@ -254,7 +256,7 @@ export default function AntigravityToolCard({
|
||||
className="px-4 py-2 rounded-lg bg-red-500/10 border border-red-500/30 text-red-500 font-medium text-sm flex items-center gap-2 hover:bg-red-500/20 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[18px]">stop_circle</span>
|
||||
Stop MITM
|
||||
{t("stopMitm")}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
@@ -263,7 +265,7 @@ export default function AntigravityToolCard({
|
||||
className="px-4 py-2 rounded-lg bg-primary/10 border border-primary/30 text-primary font-medium text-sm flex items-center gap-2 hover:bg-primary/20 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[18px]">play_circle</span>
|
||||
Start MITM
|
||||
{t("startMitm")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -280,7 +282,7 @@ export default function AntigravityToolCard({
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">
|
||||
API Key
|
||||
{t("apiKey")}
|
||||
</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">
|
||||
arrow_forward
|
||||
@@ -299,9 +301,7 @@ export default function AntigravityToolCard({
|
||||
</select>
|
||||
) : (
|
||||
<span className="flex-1 text-xs text-text-muted px-2 py-1.5">
|
||||
{cloudEnabled
|
||||
? "No API keys - Create one in Keys page"
|
||||
: "sk_omniroute (default)"}
|
||||
{cloudEnabled ? t("noApiKeysCreateOne") : t("defaultOmnirouteKey")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -318,7 +318,7 @@ export default function AntigravityToolCard({
|
||||
type="text"
|
||||
value={modelMappings[model.alias] || ""}
|
||||
onChange={(e) => handleModelMappingChange(model.alias, e.target.value)}
|
||||
placeholder="provider/model-id"
|
||||
placeholder={t("modelPlaceholder")}
|
||||
className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
/>
|
||||
<button
|
||||
@@ -326,13 +326,13 @@ export default function AntigravityToolCard({
|
||||
disabled={!hasActiveProviders}
|
||||
className={`px-2 py-1.5 rounded border text-xs transition-colors shrink-0 whitespace-nowrap ${hasActiveProviders ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}
|
||||
>
|
||||
Select
|
||||
{t("select")}
|
||||
</button>
|
||||
{modelMappings[model.alias] && (
|
||||
<button
|
||||
onClick={() => handleModelMappingChange(model.alias, "")}
|
||||
className="p-1 text-text-muted hover:text-red-500 rounded transition-colors"
|
||||
title="Clear"
|
||||
title={t("clear")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">close</span>
|
||||
</button>
|
||||
@@ -348,7 +348,7 @@ export default function AntigravityToolCard({
|
||||
disabled={loading || Object.keys(modelMappings).length === 0}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">save</span>
|
||||
Save Mappings
|
||||
{t("saveMappings")}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
@@ -358,19 +358,19 @@ export default function AntigravityToolCard({
|
||||
{!isRunning && (
|
||||
<div className="flex flex-col gap-1.5 px-1">
|
||||
<p className="text-xs text-text-muted">
|
||||
<span className="font-medium text-text-main">How it works:</span> Intercepts
|
||||
Antigravity traffic via DNS redirect, letting you reroute models through OmniRoute.
|
||||
<span className="font-medium text-text-main">{t("howItWorks")}</span>{" "}
|
||||
{t("antigravityHowWorksDesc")}
|
||||
</p>
|
||||
<div className="flex flex-col gap-0.5 text-[11px] text-text-muted">
|
||||
<span>1. Generates SSL cert & adds to system keychain</span>
|
||||
<span>{t("antigravityStep1")}</span>
|
||||
<span>
|
||||
2. Redirects{" "}
|
||||
{t("antigravityStep2Prefix")}{" "}
|
||||
<code className="text-[10px] bg-surface px-1 rounded">
|
||||
daily-cloudcode-pa.googleapis.com
|
||||
</code>{" "}
|
||||
→ localhost
|
||||
{t("antigravityStep2Suffix")}
|
||||
</span>
|
||||
<span>3. Maps Antigravity models to any provider via OmniRoute</span>
|
||||
<span>{t("antigravityStep3")}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -385,20 +385,18 @@ export default function AntigravityToolCard({
|
||||
setSudoPassword("");
|
||||
setMessage(null);
|
||||
}}
|
||||
title="Sudo Password Required"
|
||||
title={t("sudoPasswordRequiredTitle")}
|
||||
size="sm"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-start gap-3 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
|
||||
<span className="material-symbols-outlined text-yellow-500 text-[20px]">warning</span>
|
||||
<p className="text-xs text-text-muted">
|
||||
Required for SSL certificate and DNS configuration
|
||||
</p>
|
||||
<p className="text-xs text-text-muted">{t("sudoPasswordHint")}</p>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Enter sudo password"
|
||||
placeholder={t("enterSudoPassword")}
|
||||
value={sudoPassword}
|
||||
onChange={(e) => setSudoPassword(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
@@ -428,10 +426,10 @@ export default function AntigravityToolCard({
|
||||
}}
|
||||
disabled={loading}
|
||||
>
|
||||
Cancel
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" onClick={handleConfirmPassword} loading={loading}>
|
||||
Confirm
|
||||
{t("confirm")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -444,7 +442,7 @@ export default function AntigravityToolCard({
|
||||
onSelect={handleModelSelect}
|
||||
selectedModel={currentEditingAlias ? modelMappings[currentEditingAlias] : null}
|
||||
activeProviders={activeProviders}
|
||||
title={`Select model for ${currentEditingAlias}`}
|
||||
title={t("selectModelForAlias", { alias: currentEditingAlias || "" })}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState, useEffect, useRef } from "react";
|
||||
import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components";
|
||||
import Image from "next/image";
|
||||
import CliStatusBadge from "./CliStatusBadge";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL;
|
||||
|
||||
@@ -21,6 +22,7 @@ export default function ClaudeToolCard({
|
||||
batchStatus,
|
||||
lastConfiguredAt,
|
||||
}) {
|
||||
const t = useTranslations("cliTools");
|
||||
const [claudeStatus, setClaudeStatus] = useState(null);
|
||||
const [checkingClaude, setCheckingClaude] = useState(false);
|
||||
const [applying, setApplying] = useState(false);
|
||||
@@ -151,14 +153,14 @@ export default function ClaudeToolCard({
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
setMessage({ type: "success", text: "Settings applied successfully!" });
|
||||
setMessage({ type: "success", text: t("settingsApplied") });
|
||||
setClaudeStatus((prev) => ({
|
||||
...prev,
|
||||
hasBackup: true,
|
||||
settings: { ...prev?.settings, env },
|
||||
}));
|
||||
} else {
|
||||
setMessage({ type: "error", text: data.error || "Failed to apply settings" });
|
||||
setMessage({ type: "error", text: data.error || t("failedApplySettings") });
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage({ type: "error", text: error.message });
|
||||
@@ -174,13 +176,13 @@ export default function ClaudeToolCard({
|
||||
const res = await fetch("/api/cli-tools/claude-settings", { method: "DELETE" });
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
setMessage({ type: "success", text: "Settings reset successfully!" });
|
||||
setMessage({ type: "success", text: t("settingsReset") });
|
||||
tool.defaultModels.forEach((model) =>
|
||||
onModelMappingChange(model.alias, model.defaultValue || "")
|
||||
);
|
||||
setSelectedApiKey("");
|
||||
} else {
|
||||
setMessage({ type: "error", text: data.error || "Failed to reset settings" });
|
||||
setMessage({ type: "error", text: data.error || t("failedResetSettings") });
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage({ type: "error", text: error.message });
|
||||
@@ -242,11 +244,11 @@ export default function ClaudeToolCard({
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
setMessage({ type: "success", text: "Backup restored!" });
|
||||
setMessage({ type: "success", text: t("backupRestored") });
|
||||
checkClaudeStatus();
|
||||
fetchBackups();
|
||||
} else {
|
||||
setMessage({ type: "error", text: data.error || "Failed to restore" });
|
||||
setMessage({ type: "error", text: data.error || t("failedRestore") });
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage({ type: "error", text: error.message });
|
||||
@@ -281,7 +283,7 @@ export default function ClaudeToolCard({
|
||||
lastConfiguredAt={lastConfiguredAt}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-text-muted truncate">{tool.description}</p>
|
||||
<p className="text-xs text-text-muted truncate">{t("toolDescriptions.claude")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
@@ -296,7 +298,7 @@ export default function ClaudeToolCard({
|
||||
{checkingClaude && (
|
||||
<div className="flex items-center gap-2 text-text-muted">
|
||||
<span className="material-symbols-outlined animate-spin">progress_activity</span>
|
||||
<span>Checking Claude CLI...</span>
|
||||
<span>{t("checkingCli", { tool: "Claude" })}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -307,13 +309,16 @@ export default function ClaudeToolCard({
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-yellow-600 dark:text-yellow-400">
|
||||
{claudeStatus.installed
|
||||
? "Claude CLI not runnable"
|
||||
: "Claude CLI not installed"}
|
||||
? t("cliNotRunnable", { tool: "Claude" })
|
||||
: t("cliNotInstalled", { tool: "Claude" })}
|
||||
</p>
|
||||
<p className="text-sm text-text-muted">
|
||||
{claudeStatus.installed
|
||||
? `Claude CLI was found but failed runtime healthcheck${claudeStatus.reason ? ` (${claudeStatus.reason})` : ""}.`
|
||||
: "Please install Claude CLI to use this feature."}
|
||||
? t("cliFoundFailedHealthcheck", {
|
||||
tool: "Claude",
|
||||
reason: claudeStatus.reason ? ` (${claudeStatus.reason})` : "",
|
||||
})
|
||||
: t("installCliPrompt", { tool: "Claude" })}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
@@ -324,23 +329,23 @@ export default function ClaudeToolCard({
|
||||
<span className="material-symbols-outlined text-[18px] mr-1">
|
||||
{showInstallGuide ? "expand_less" : "help"}
|
||||
</span>
|
||||
{showInstallGuide ? "Hide" : "How to Install"}
|
||||
{showInstallGuide ? t("hide") : t("howToInstall")}
|
||||
</Button>
|
||||
</div>
|
||||
{showInstallGuide && (
|
||||
<div className="p-4 bg-surface border border-border rounded-lg">
|
||||
<h4 className="font-medium mb-3">Installation Guide</h4>
|
||||
<h4 className="font-medium mb-3">{t("installationGuide")}</h4>
|
||||
<div className="space-y-3 text-sm">
|
||||
<div>
|
||||
<p className="text-text-muted mb-1">macOS / Linux / Windows:</p>
|
||||
<p className="text-text-muted mb-1">{t("platforms")}</p>
|
||||
<code className="block px-3 py-2 bg-black/5 dark:bg-white/5 rounded font-mono text-xs">
|
||||
npm install -g @anthropic-ai/claude-code
|
||||
</code>
|
||||
</div>
|
||||
<p className="text-text-muted">
|
||||
After installation, run{" "}
|
||||
<code className="px-1 bg-black/5 dark:bg-white/5 rounded">claude</code> to
|
||||
verify.
|
||||
{t("afterInstallationRun")}{" "}
|
||||
<code className="px-1 bg-black/5 dark:bg-white/5 rounded">claude</code>{" "}
|
||||
{t("toVerify")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -355,7 +360,7 @@ export default function ClaudeToolCard({
|
||||
{claudeStatus?.settings?.env?.ANTHROPIC_BASE_URL && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">
|
||||
Current
|
||||
{t("current")}
|
||||
</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">
|
||||
arrow_forward
|
||||
@@ -369,7 +374,7 @@ export default function ClaudeToolCard({
|
||||
{/* Base URL */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">
|
||||
Base URL
|
||||
{t("baseUrl")}
|
||||
</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">
|
||||
arrow_forward
|
||||
@@ -378,14 +383,14 @@ export default function ClaudeToolCard({
|
||||
type="text"
|
||||
value={getDisplayUrl()}
|
||||
onChange={(e) => setCustomBaseUrl(e.target.value)}
|
||||
placeholder="https://.../v1"
|
||||
placeholder={t("baseUrlPlaceholder")}
|
||||
className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
/>
|
||||
{customBaseUrl && customBaseUrl !== baseUrl && (
|
||||
<button
|
||||
onClick={() => setCustomBaseUrl("")}
|
||||
className="p-1 text-text-muted hover:text-primary rounded transition-colors"
|
||||
title="Reset to default"
|
||||
title={t("resetToDefault")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">restart_alt</span>
|
||||
</button>
|
||||
@@ -395,7 +400,7 @@ export default function ClaudeToolCard({
|
||||
{/* API Key */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">
|
||||
API Key
|
||||
{t("apiKey")}
|
||||
</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">
|
||||
arrow_forward
|
||||
@@ -414,9 +419,7 @@ export default function ClaudeToolCard({
|
||||
</select>
|
||||
) : (
|
||||
<span className="flex-1 text-xs text-text-muted px-2 py-1.5">
|
||||
{cloudEnabled
|
||||
? "No API keys - Create one in Keys page"
|
||||
: "sk_omniroute (default)"}
|
||||
{cloudEnabled ? t("noApiKeysCreateOne") : t("defaultOmnirouteKey")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -434,7 +437,7 @@ export default function ClaudeToolCard({
|
||||
type="text"
|
||||
value={modelMappings[model.alias] || ""}
|
||||
onChange={(e) => onModelMappingChange(model.alias, e.target.value)}
|
||||
placeholder="provider/model-id"
|
||||
placeholder={t("providerModelPlaceholder")}
|
||||
className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
/>
|
||||
<button
|
||||
@@ -442,13 +445,13 @@ export default function ClaudeToolCard({
|
||||
disabled={!hasActiveProviders}
|
||||
className={`px-2 py-1.5 rounded border text-xs transition-colors shrink-0 whitespace-nowrap ${hasActiveProviders ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}
|
||||
>
|
||||
Select Model
|
||||
{t("selectModel")}
|
||||
</button>
|
||||
{modelMappings[model.alias] && (
|
||||
<button
|
||||
onClick={() => onModelMappingChange(model.alias, "")}
|
||||
className="p-1 text-text-muted hover:text-red-500 rounded transition-colors"
|
||||
title="Clear"
|
||||
title={t("clear")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">close</span>
|
||||
</button>
|
||||
@@ -476,7 +479,8 @@ export default function ClaudeToolCard({
|
||||
disabled={!hasActiveProviders}
|
||||
loading={applying}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">save</span>Apply
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">save</span>
|
||||
{t("apply")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -485,11 +489,12 @@ export default function ClaudeToolCard({
|
||||
disabled={!claudeStatus?.hasOmniRoute}
|
||||
loading={restoring}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">restore</span>Reset
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">restore</span>
|
||||
{t("reset")}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => setShowManualConfigModal(true)}>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">content_copy</span>
|
||||
Manual Config
|
||||
{t("manualConfig")}
|
||||
</Button>
|
||||
<div className="flex-1" />
|
||||
<Button
|
||||
@@ -500,7 +505,8 @@ export default function ClaudeToolCard({
|
||||
if (!showBackups) fetchBackups();
|
||||
}}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">history</span>Backups
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">history</span>
|
||||
{t("backups")}
|
||||
{backups.length > 0 && ` (${backups.length})`}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -510,12 +516,10 @@ export default function ClaudeToolCard({
|
||||
<div className="mt-2 p-3 bg-surface border border-border rounded-lg">
|
||||
<h4 className="text-xs font-semibold text-text-main mb-2 flex items-center gap-1">
|
||||
<span className="material-symbols-outlined text-[14px]">history</span>
|
||||
Config Backups
|
||||
{t("configBackups")}
|
||||
</h4>
|
||||
{backups.length === 0 ? (
|
||||
<p className="text-xs text-text-muted">
|
||||
No backups yet. Backups are created automatically before each Apply or Reset.
|
||||
</p>
|
||||
<p className="text-xs text-text-muted">{t("noBackupsYet")}</p>
|
||||
) : (
|
||||
<div className="space-y-1.5">
|
||||
{backups.map((b) => (
|
||||
@@ -537,7 +541,7 @@ export default function ClaudeToolCard({
|
||||
disabled={restoringBackup === b.id}
|
||||
className="px-2 py-0.5 bg-primary/10 text-primary rounded text-[10px] font-medium hover:bg-primary/20 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{restoringBackup === b.id ? "..." : "Restore"}
|
||||
{restoringBackup === b.id ? "..." : t("restore")}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
@@ -557,13 +561,13 @@ export default function ClaudeToolCard({
|
||||
selectedModel={currentEditingAlias ? modelMappings[currentEditingAlias] : null}
|
||||
activeProviders={activeProviders}
|
||||
modelAliases={modelAliases}
|
||||
title={`Select model for ${currentEditingAlias}`}
|
||||
title={t("selectModelForAlias", { alias: currentEditingAlias || "" })}
|
||||
/>
|
||||
|
||||
<ManualConfigModal
|
||||
isOpen={showManualConfigModal}
|
||||
onClose={() => setShowManualConfigModal(false)}
|
||||
title="Claude CLI - Manual Configuration"
|
||||
title={t("claudeManualConfiguration")}
|
||||
configs={getManualConfigs()}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
import { useLocale, useTranslations } from "next-intl";
|
||||
|
||||
/**
|
||||
* Shared status badge for CLI tool cards.
|
||||
@@ -7,28 +8,31 @@
|
||||
* Optionally shows last-configured relative timestamp.
|
||||
*/
|
||||
|
||||
function formatRelativeTime(isoDate: string): string {
|
||||
function formatRelativeTime(
|
||||
isoDate: string,
|
||||
t: (key: string, values?: Record<string, unknown>) => string
|
||||
): string {
|
||||
const now = Date.now();
|
||||
const then = new Date(isoDate).getTime();
|
||||
const diffMs = now - then;
|
||||
if (diffMs < 0) return "just now";
|
||||
if (diffMs < 0) return t("justNow");
|
||||
|
||||
const seconds = Math.floor(diffMs / 1000);
|
||||
if (seconds < 60) return "just now";
|
||||
if (seconds < 60) return t("justNow");
|
||||
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
if (minutes < 60) return `${minutes}m ago`;
|
||||
if (minutes < 60) return t("minutesAgoShort", { count: minutes });
|
||||
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 24) return `${hours}h ago`;
|
||||
if (hours < 24) return t("hoursAgoShort", { count: hours });
|
||||
|
||||
const days = Math.floor(hours / 24);
|
||||
if (days < 30) return `${days}d ago`;
|
||||
if (days < 30) return t("daysAgoShort", { count: days });
|
||||
|
||||
const months = Math.floor(days / 30);
|
||||
if (months < 12) return `${months}mo ago`;
|
||||
if (months < 12) return t("monthsAgoShort", { count: months });
|
||||
|
||||
return `${Math.floor(months / 12)}y ago`;
|
||||
return t("yearsAgoShort", { count: Math.floor(months / 12) });
|
||||
}
|
||||
|
||||
export default function CliStatusBadge({
|
||||
@@ -36,6 +40,8 @@ export default function CliStatusBadge({
|
||||
batchStatus,
|
||||
lastConfiguredAt = null,
|
||||
}) {
|
||||
const t = useTranslations("cliTools");
|
||||
const locale = useLocale();
|
||||
// Determine badge from effectiveConfigStatus or batchStatus
|
||||
const status = effectiveConfigStatus || batchStatus?.configStatus || null;
|
||||
|
||||
@@ -43,27 +49,27 @@ export default function CliStatusBadge({
|
||||
configured: {
|
||||
dotClass: "bg-green-500",
|
||||
badgeClass: "bg-green-500/10 text-green-600 dark:text-green-400",
|
||||
text: "Configured",
|
||||
text: t("configured"),
|
||||
},
|
||||
not_configured: {
|
||||
dotClass: "bg-yellow-500",
|
||||
badgeClass: "bg-yellow-500/10 text-yellow-600 dark:text-yellow-400",
|
||||
text: "Not configured",
|
||||
text: t("notConfigured"),
|
||||
},
|
||||
not_installed: {
|
||||
dotClass: "bg-zinc-400 dark:bg-zinc-500",
|
||||
badgeClass: "bg-zinc-500/10 text-zinc-500 dark:text-zinc-400",
|
||||
text: "Not installed",
|
||||
text: t("notInstalled"),
|
||||
},
|
||||
other: {
|
||||
dotClass: "bg-blue-500",
|
||||
badgeClass: "bg-blue-500/10 text-blue-600 dark:text-blue-400",
|
||||
text: "Custom",
|
||||
text: t("custom"),
|
||||
},
|
||||
unknown: {
|
||||
dotClass: "bg-zinc-400 dark:bg-zinc-500",
|
||||
badgeClass: "bg-zinc-500/10 text-zinc-500 dark:text-zinc-400",
|
||||
text: "Unknown",
|
||||
text: t("unknown"),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -82,15 +88,15 @@ export default function CliStatusBadge({
|
||||
{lastConfiguredAt ? (
|
||||
<span
|
||||
className="inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] text-text-muted"
|
||||
title={`Last saved: ${new Date(lastConfiguredAt).toLocaleString()}`}
|
||||
title={t("lastSavedAt", { date: new Date(lastConfiguredAt).toLocaleString(locale) })}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[12px]">schedule</span>
|
||||
{formatRelativeTime(lastConfiguredAt)}
|
||||
{formatRelativeTime(lastConfiguredAt, t)}
|
||||
</span>
|
||||
) : status && status !== "not_installed" ? (
|
||||
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] text-text-muted">
|
||||
<span className="material-symbols-outlined text-[12px]">schedule</span>
|
||||
Never
|
||||
{t("never")}
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState, useEffect, useRef } from "react";
|
||||
import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components";
|
||||
import Image from "next/image";
|
||||
import CliStatusBadge from "./CliStatusBadge";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL;
|
||||
|
||||
@@ -19,6 +20,7 @@ export default function ClineToolCard({
|
||||
batchStatus,
|
||||
lastConfiguredAt,
|
||||
}) {
|
||||
const t = useTranslations("cliTools");
|
||||
const [clineStatus, setClineStatus] = useState(null);
|
||||
const [checkingCline, setCheckingCline] = useState(false);
|
||||
const [applying, setApplying] = useState(false);
|
||||
@@ -109,12 +111,12 @@ export default function ClineToolCard({
|
||||
body: JSON.stringify({ tool: "cline", backupId }),
|
||||
});
|
||||
if (res.ok) {
|
||||
setMessage({ type: "success", text: "Backup restored! Reloading status..." });
|
||||
setMessage({ type: "success", text: t("backupRestoredReloading") });
|
||||
await checkClineStatus();
|
||||
await fetchBackups();
|
||||
} else {
|
||||
const data = await res.json();
|
||||
setMessage({ type: "error", text: data.error || "Failed to restore backup" });
|
||||
setMessage({ type: "error", text: data.error || t("failedRestoreBackup") });
|
||||
}
|
||||
} catch (e) {
|
||||
setMessage({ type: "error", text: e.message });
|
||||
@@ -161,11 +163,11 @@ export default function ClineToolCard({
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
setMessage({ type: "success", text: data.message || "Applied!" });
|
||||
setMessage({ type: "success", text: data.message || t("applied") });
|
||||
await checkClineStatus();
|
||||
await fetchBackups();
|
||||
} else {
|
||||
setMessage({ type: "error", text: data.error || "Failed" });
|
||||
setMessage({ type: "error", text: data.error || t("failed") });
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage({ type: "error", text: error.message });
|
||||
@@ -181,13 +183,13 @@ export default function ClineToolCard({
|
||||
const res = await fetch("/api/cli-tools/cline-settings", { method: "DELETE" });
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
setMessage({ type: "success", text: data.message || "Reset!" });
|
||||
setMessage({ type: "success", text: data.message || t("resetDone") });
|
||||
setSelectedModel("");
|
||||
hasInitializedModel.current = false;
|
||||
await checkClineStatus();
|
||||
await fetchBackups();
|
||||
} else {
|
||||
setMessage({ type: "error", text: data.error || "Failed" });
|
||||
setMessage({ type: "error", text: data.error || t("failed") });
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage({ type: "error", text: error.message });
|
||||
@@ -240,7 +242,7 @@ export default function ClineToolCard({
|
||||
lastConfiguredAt={lastConfiguredAt}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-text-muted truncate">{tool.description}</p>
|
||||
<p className="text-xs text-text-muted truncate">{t("toolDescriptions.cline")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
@@ -257,7 +259,7 @@ export default function ClineToolCard({
|
||||
<span className="material-symbols-outlined animate-spin text-base">
|
||||
progress_activity
|
||||
</span>
|
||||
<span>Checking Cline CLI...</span>
|
||||
<span>{t("checkingCli", { tool: "Cline" })}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -273,14 +275,14 @@ export default function ClineToolCard({
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-sm font-medium">
|
||||
{cliReady
|
||||
? "Cline CLI detected and ready"
|
||||
? t("cliDetectedReady", { tool: "Cline" })
|
||||
: clineStatus.installed
|
||||
? "Cline CLI installed but not runnable"
|
||||
: "Cline CLI not detected"}
|
||||
? t("cliNotRunnable", { tool: "Cline" })
|
||||
: t("cliNotDetected", { tool: "Cline" })}
|
||||
</p>
|
||||
{clineStatus.commandPath && (
|
||||
<p className="text-xs text-text-muted">
|
||||
Binary:{" "}
|
||||
{t("binary")}:{" "}
|
||||
<code className="px-1 py-0.5 rounded bg-black/5 dark:bg-white/10">
|
||||
{clineStatus.commandPath}
|
||||
</code>
|
||||
@@ -288,7 +290,7 @@ export default function ClineToolCard({
|
||||
)}
|
||||
{clineStatus.globalStatePath && (
|
||||
<p className="text-xs text-text-muted">
|
||||
Config:{" "}
|
||||
{t("configPathShort")}:{" "}
|
||||
<code className="px-1 py-0.5 rounded bg-black/5 dark:bg-white/10">
|
||||
{clineStatus.globalStatePath}
|
||||
</code>
|
||||
@@ -307,10 +309,10 @@ export default function ClineToolCard({
|
||||
</span>
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-sm text-green-700 dark:text-green-300">
|
||||
OmniRoute is configured as OpenAI-compatible provider
|
||||
{t("omnirouteConfiguredOpenAiCompatible")}
|
||||
</p>
|
||||
<p className="text-xs text-text-muted">
|
||||
Provider: <strong>openai</strong> • Model:{" "}
|
||||
{t("provider")}: <strong>openai</strong> • {t("model")}:{" "}
|
||||
<strong>{clineStatus.settings?.openAiModelId || "—"}</strong>
|
||||
</p>
|
||||
</div>
|
||||
@@ -319,13 +321,13 @@ export default function ClineToolCard({
|
||||
|
||||
{/* Model selection */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm text-text-muted">Model</label>
|
||||
<label className="text-sm text-text-muted">{t("model")}</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={selectedModel}
|
||||
onChange={(e) => setSelectedModel(e.target.value)}
|
||||
placeholder="provider/model-id"
|
||||
placeholder={t("providerModelPlaceholder")}
|
||||
className="flex-1 px-3 py-2 bg-bg-secondary rounded-lg text-sm border border-border focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
/>
|
||||
<Button
|
||||
@@ -334,7 +336,7 @@ export default function ClineToolCard({
|
||||
onClick={() => setModalOpen(true)}
|
||||
disabled={!hasActiveProviders}
|
||||
>
|
||||
Select
|
||||
{t("select")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -348,7 +350,7 @@ export default function ClineToolCard({
|
||||
|
||||
{/* API Key selection */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm text-text-muted">API Key</label>
|
||||
<label className="text-sm text-text-muted">{t("apiKey")}</label>
|
||||
{apiKeys && apiKeys.length > 0 ? (
|
||||
<select
|
||||
value={selectedApiKey}
|
||||
@@ -363,7 +365,7 @@ export default function ClineToolCard({
|
||||
</select>
|
||||
) : (
|
||||
<p className="text-sm text-text-muted">
|
||||
{cloudEnabled ? "No API keys available" : "Using default: sk_omniroute"}
|
||||
{cloudEnabled ? t("noApiKeysAvailable") : t("usingDefaultOmniroute")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -378,14 +380,14 @@ export default function ClineToolCard({
|
||||
loading={applying}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">save</span>
|
||||
{configStatus === "configured" ? "Update Config" : "Apply Config"}
|
||||
{configStatus === "configured" ? t("updateConfig") : t("applyConfig")}
|
||||
</Button>
|
||||
{configStatus === "configured" && (
|
||||
<Button variant="outline" size="sm" onClick={handleReset} loading={restoring}>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">
|
||||
restart_alt
|
||||
</span>
|
||||
Reset
|
||||
{t("reset")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -414,7 +416,7 @@ export default function ClineToolCard({
|
||||
chevron_right
|
||||
</span>
|
||||
<span className="material-symbols-outlined text-[16px]">backup</span>
|
||||
Backups {backups.length > 0 && `(${backups.length})`}
|
||||
{t("backups")} {backups.length > 0 && `(${backups.length})`}
|
||||
</button>
|
||||
{showBackups && backups.length > 0 && (
|
||||
<div className="mt-2 flex flex-col gap-1.5 pl-6">
|
||||
@@ -435,14 +437,14 @@ export default function ClineToolCard({
|
||||
onClick={() => handleRestoreBackup(b.id)}
|
||||
loading={restoringBackup === b.id}
|
||||
>
|
||||
Restore
|
||||
{t("restore")}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{showBackups && backups.length === 0 && (
|
||||
<p className="mt-2 pl-6 text-xs text-text-muted">No backups available.</p>
|
||||
<p className="mt-2 pl-6 text-xs text-text-muted">{t("noBackupsAvailable")}</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
@@ -458,13 +460,13 @@ export default function ClineToolCard({
|
||||
onSelect={handleSelectModel}
|
||||
selectedModel={selectedModel}
|
||||
activeProviders={activeProviders}
|
||||
title="Select Model for Cline"
|
||||
title={t("selectModelForTool", { tool: "Cline" })}
|
||||
/>
|
||||
{showManualConfigModal && (
|
||||
<ManualConfigModal
|
||||
isOpen={showManualConfigModal}
|
||||
onClose={() => setShowManualConfigModal(false)}
|
||||
title="Cline Manual Configuration"
|
||||
title={t("clineManualConfiguration")}
|
||||
{...({
|
||||
onApply: handleManualConfig,
|
||||
currentConfig: {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState, useEffect } from "react";
|
||||
import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components";
|
||||
import Image from "next/image";
|
||||
import CliStatusBadge from "./CliStatusBadge";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function CodexToolCard({
|
||||
tool,
|
||||
@@ -16,6 +17,7 @@ export default function CodexToolCard({
|
||||
batchStatus,
|
||||
lastConfiguredAt,
|
||||
}) {
|
||||
const t = useTranslations("cliTools");
|
||||
const [codexStatus, setCodexStatus] = useState(null);
|
||||
const [checkingCodex, setCheckingCodex] = useState(false);
|
||||
const [applying, setApplying] = useState(false);
|
||||
@@ -132,10 +134,10 @@ export default function CodexToolCard({
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
setMessage({ type: "success", text: "Settings applied successfully!" });
|
||||
setMessage({ type: "success", text: t("settingsApplied") });
|
||||
checkCodexStatus();
|
||||
} else {
|
||||
setMessage({ type: "error", text: data.error || "Failed to apply settings" });
|
||||
setMessage({ type: "error", text: data.error || t("failedApplySettings") });
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage({ type: "error", text: error.message });
|
||||
@@ -151,11 +153,11 @@ export default function CodexToolCard({
|
||||
const res = await fetch("/api/cli-tools/codex-settings", { method: "DELETE" });
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
setMessage({ type: "success", text: "Settings reset successfully!" });
|
||||
setMessage({ type: "success", text: t("settingsReset") });
|
||||
setSelectedModel("");
|
||||
checkCodexStatus();
|
||||
} else {
|
||||
setMessage({ type: "error", text: data.error || "Failed to reset settings" });
|
||||
setMessage({ type: "error", text: data.error || t("failedResetSettings") });
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage({ type: "error", text: error.message });
|
||||
@@ -192,11 +194,11 @@ export default function CodexToolCard({
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
setMessage({ type: "success", text: `Profile "${newProfileName}" saved!` });
|
||||
setMessage({ type: "success", text: t("profileSaved", { name: newProfileName }) });
|
||||
setNewProfileName("");
|
||||
fetchProfiles();
|
||||
} else {
|
||||
setMessage({ type: "error", text: data.error || "Failed to save profile" });
|
||||
setMessage({ type: "error", text: data.error || t("failedSaveProfile") });
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage({ type: "error", text: error.message });
|
||||
@@ -216,11 +218,11 @@ export default function CodexToolCard({
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
setMessage({ type: "success", text: data.message || "Profile activated!" });
|
||||
setMessage({ type: "success", text: data.message || t("profileActivated") });
|
||||
checkCodexStatus();
|
||||
fetchBackups();
|
||||
} else {
|
||||
setMessage({ type: "error", text: data.error || "Failed to activate profile" });
|
||||
setMessage({ type: "error", text: data.error || t("failedActivateProfile") });
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage({ type: "error", text: error.message });
|
||||
@@ -264,11 +266,11 @@ export default function CodexToolCard({
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
setMessage({ type: "success", text: "Backup restored!" });
|
||||
setMessage({ type: "success", text: t("backupRestored") });
|
||||
checkCodexStatus();
|
||||
fetchBackups();
|
||||
} else {
|
||||
setMessage({ type: "error", text: data.error || "Failed to restore" });
|
||||
setMessage({ type: "error", text: data.error || t("failedRestore") });
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage({ type: "error", text: error.message });
|
||||
@@ -341,7 +343,7 @@ wire_api = "responses"
|
||||
lastConfiguredAt={lastConfiguredAt}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-text-muted truncate">{tool.description}</p>
|
||||
<p className="text-xs text-text-muted truncate">{t("toolDescriptions.codex")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
@@ -356,7 +358,7 @@ wire_api = "responses"
|
||||
{checkingCodex && (
|
||||
<div className="flex items-center gap-2 text-text-muted">
|
||||
<span className="material-symbols-outlined animate-spin">progress_activity</span>
|
||||
<span>Checking Codex CLI...</span>
|
||||
<span>{t("checkingCli", { tool: "Codex" })}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -366,12 +368,17 @@ wire_api = "responses"
|
||||
<span className="material-symbols-outlined text-yellow-500">warning</span>
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-yellow-600 dark:text-yellow-400">
|
||||
{codexStatus.installed ? "Codex CLI not runnable" : "Codex CLI not installed"}
|
||||
{codexStatus.installed
|
||||
? t("cliNotRunnable", { tool: "Codex" })
|
||||
: t("cliNotInstalled", { tool: "Codex" })}
|
||||
</p>
|
||||
<p className="text-sm text-text-muted">
|
||||
{codexStatus.installed
|
||||
? `Codex CLI was found but failed runtime healthcheck${codexStatus.reason ? ` (${codexStatus.reason})` : ""}.`
|
||||
: "Please install Codex CLI to use auto-apply feature."}
|
||||
? t("cliFoundFailedHealthcheck", {
|
||||
tool: "Codex",
|
||||
reason: codexStatus.reason ? ` (${codexStatus.reason})` : "",
|
||||
})
|
||||
: t("installCodexPrompt")}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
@@ -382,35 +389,35 @@ wire_api = "responses"
|
||||
<span className="material-symbols-outlined text-[18px] mr-1">
|
||||
{showInstallGuide ? "expand_less" : "help"}
|
||||
</span>
|
||||
{showInstallGuide ? "Hide" : "How to Install"}
|
||||
{showInstallGuide ? t("hide") : t("howToInstall")}
|
||||
</Button>
|
||||
</div>
|
||||
{showInstallGuide && (
|
||||
<div className="p-4 bg-surface border border-border rounded-lg">
|
||||
<h4 className="font-medium mb-3">Installation Guide</h4>
|
||||
<h4 className="font-medium mb-3">{t("installationGuide")}</h4>
|
||||
<div className="space-y-3 text-sm">
|
||||
<div>
|
||||
<p className="text-text-muted mb-1">macOS / Linux / Windows:</p>
|
||||
<p className="text-text-muted mb-1">{t("platforms")}</p>
|
||||
<code className="block px-3 py-2 bg-black/5 dark:bg-white/5 rounded font-mono text-xs">
|
||||
npm install -g @openai/codex
|
||||
</code>
|
||||
</div>
|
||||
<p className="text-text-muted">
|
||||
After installation, run{" "}
|
||||
<code className="px-1 bg-black/5 dark:bg-white/5 rounded">codex</code> to
|
||||
verify.
|
||||
{t("afterInstallationRun")}{" "}
|
||||
<code className="px-1 bg-black/5 dark:bg-white/5 rounded">codex</code>{" "}
|
||||
{t("toVerify")}
|
||||
</p>
|
||||
<div className="pt-2 border-t border-border">
|
||||
<p className="text-text-muted text-xs">
|
||||
Codex uses{" "}
|
||||
{t("codexAuthNotePrefix")}{" "}
|
||||
<code className="px-1 bg-black/5 dark:bg-white/5 rounded">
|
||||
~/.codex/auth.json
|
||||
</code>{" "}
|
||||
with{" "}
|
||||
{t("codexAuthNoteMiddle")}{" "}
|
||||
<code className="px-1 bg-black/5 dark:bg-white/5 rounded">
|
||||
OPENAI_API_KEY
|
||||
</code>
|
||||
. Click "Apply" to auto-configure.
|
||||
. {t("codexAuthNoteSuffix")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -430,7 +437,7 @@ wire_api = "responses"
|
||||
return currentBaseUrl ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">
|
||||
Current
|
||||
{t("current")}
|
||||
</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">
|
||||
arrow_forward
|
||||
@@ -445,7 +452,7 @@ wire_api = "responses"
|
||||
{/* Base URL */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">
|
||||
Base URL
|
||||
{t("baseUrl")}
|
||||
</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">
|
||||
arrow_forward
|
||||
@@ -454,14 +461,14 @@ wire_api = "responses"
|
||||
type="text"
|
||||
value={getDisplayUrl()}
|
||||
onChange={(e) => setCustomBaseUrl(e.target.value)}
|
||||
placeholder="https://.../v1"
|
||||
placeholder={t("baseUrlPlaceholder")}
|
||||
className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
/>
|
||||
{customBaseUrl && customBaseUrl !== `${baseUrl}/v1` && (
|
||||
<button
|
||||
onClick={() => setCustomBaseUrl("")}
|
||||
className="p-1 text-text-muted hover:text-primary rounded transition-colors"
|
||||
title="Reset to default"
|
||||
title={t("resetToDefault")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">restart_alt</span>
|
||||
</button>
|
||||
@@ -471,7 +478,7 @@ wire_api = "responses"
|
||||
{/* API Key */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">
|
||||
API Key
|
||||
{t("apiKey")}
|
||||
</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">
|
||||
arrow_forward
|
||||
@@ -490,9 +497,7 @@ wire_api = "responses"
|
||||
</select>
|
||||
) : (
|
||||
<span className="flex-1 text-xs text-text-muted px-2 py-1.5">
|
||||
{cloudEnabled
|
||||
? "No API keys - Create one in Keys page"
|
||||
: "sk_omniroute (default)"}
|
||||
{cloudEnabled ? t("noApiKeysCreateOne") : t("defaultOmnirouteKey")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -500,7 +505,7 @@ wire_api = "responses"
|
||||
{/* Model */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">
|
||||
Model
|
||||
{t("model")}
|
||||
</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">
|
||||
arrow_forward
|
||||
@@ -509,7 +514,7 @@ wire_api = "responses"
|
||||
type="text"
|
||||
value={selectedModel}
|
||||
onChange={(e) => setSelectedModel(e.target.value)}
|
||||
placeholder="provider/model-id"
|
||||
placeholder={t("providerModelPlaceholder")}
|
||||
className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
/>
|
||||
<button
|
||||
@@ -517,13 +522,13 @@ wire_api = "responses"
|
||||
disabled={!activeProviders?.length}
|
||||
className={`px-2 py-1.5 rounded border text-xs transition-colors shrink-0 whitespace-nowrap ${activeProviders?.length ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}
|
||||
>
|
||||
Select Model
|
||||
{t("selectModel")}
|
||||
</button>
|
||||
{selectedModel && (
|
||||
<button
|
||||
onClick={() => setSelectedModel("")}
|
||||
className="p-1 text-text-muted hover:text-red-500 rounded transition-colors"
|
||||
title="Clear"
|
||||
title={t("clear")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">close</span>
|
||||
</button>
|
||||
@@ -550,7 +555,8 @@ wire_api = "responses"
|
||||
disabled={!selectedApiKey || !selectedModel}
|
||||
loading={applying}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">save</span>Apply
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">save</span>
|
||||
{t("apply")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -559,11 +565,12 @@ wire_api = "responses"
|
||||
disabled={!codexStatus.hasOmniRoute}
|
||||
loading={restoring}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">restore</span>Reset
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">restore</span>
|
||||
{t("reset")}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => setShowManualConfigModal(true)}>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">content_copy</span>
|
||||
Manual Config
|
||||
{t("manualConfig")}
|
||||
</Button>
|
||||
<div className="flex-1" />
|
||||
<Button
|
||||
@@ -577,7 +584,7 @@ wire_api = "responses"
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">
|
||||
manage_accounts
|
||||
</span>
|
||||
Profiles
|
||||
{t("profiles")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -587,7 +594,8 @@ wire_api = "responses"
|
||||
if (!showBackups) fetchBackups();
|
||||
}}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">history</span>Backups
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">history</span>
|
||||
{t("backups")}
|
||||
{backups.length > 0 && ` (${backups.length})`}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -597,12 +605,10 @@ wire_api = "responses"
|
||||
<div className="mt-2 p-3 bg-surface border border-border rounded-lg">
|
||||
<h4 className="text-xs font-semibold text-text-main mb-2 flex items-center gap-1">
|
||||
<span className="material-symbols-outlined text-[14px]">manage_accounts</span>
|
||||
Saved Profiles
|
||||
{t("savedProfiles")}
|
||||
</h4>
|
||||
{profiles.length === 0 ? (
|
||||
<p className="text-xs text-text-muted">
|
||||
No profiles saved yet. Save current config as a profile below.
|
||||
</p>
|
||||
<p className="text-xs text-text-muted">{t("noProfilesYet")}</p>
|
||||
) : (
|
||||
<div className="space-y-1.5 mb-3">
|
||||
{profiles.map((p) => (
|
||||
@@ -625,12 +631,12 @@ wire_api = "responses"
|
||||
disabled={activatingProfile === p.id}
|
||||
className="px-2 py-0.5 bg-primary/10 text-primary rounded text-[10px] font-medium hover:bg-primary/20 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{activatingProfile === p.id ? "..." : "Activate"}
|
||||
{activatingProfile === p.id ? "..." : t("activate")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteProfile(p.id)}
|
||||
className="p-0.5 text-text-muted hover:text-red-500 transition-colors"
|
||||
title="Delete profile"
|
||||
title={t("deleteProfile")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">delete</span>
|
||||
</button>
|
||||
@@ -643,7 +649,7 @@ wire_api = "responses"
|
||||
type="text"
|
||||
value={newProfileName}
|
||||
onChange={(e) => setNewProfileName(e.target.value)}
|
||||
placeholder="Profile name (e.g. Personal Account)"
|
||||
placeholder={t("profileNamePlaceholder")}
|
||||
className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
onKeyDown={(e) => e.key === "Enter" && handleSaveProfile()}
|
||||
/>
|
||||
@@ -654,8 +660,8 @@ wire_api = "responses"
|
||||
disabled={!newProfileName.trim()}
|
||||
loading={savingProfile}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">save</span>Save
|
||||
Current
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">save</span>
|
||||
{t("saveCurrent")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -666,12 +672,10 @@ wire_api = "responses"
|
||||
<div className="mt-2 p-3 bg-surface border border-border rounded-lg">
|
||||
<h4 className="text-xs font-semibold text-text-main mb-2 flex items-center gap-1">
|
||||
<span className="material-symbols-outlined text-[14px]">history</span>
|
||||
Config Backups
|
||||
{t("configBackups")}
|
||||
</h4>
|
||||
{backups.length === 0 ? (
|
||||
<p className="text-xs text-text-muted">
|
||||
No backups yet. Backups are created automatically before each Apply or Reset.
|
||||
</p>
|
||||
<p className="text-xs text-text-muted">{t("noBackupsYet")}</p>
|
||||
) : (
|
||||
<div className="space-y-1.5">
|
||||
{backups.map((b) => (
|
||||
@@ -693,7 +697,7 @@ wire_api = "responses"
|
||||
disabled={restoringBackup === b.id}
|
||||
className="px-2 py-0.5 bg-primary/10 text-primary rounded text-[10px] font-medium hover:bg-primary/20 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{restoringBackup === b.id ? "..." : "Restore"}
|
||||
{restoringBackup === b.id ? "..." : t("restore")}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
@@ -713,13 +717,13 @@ wire_api = "responses"
|
||||
selectedModel={selectedModel}
|
||||
activeProviders={activeProviders}
|
||||
modelAliases={modelAliases}
|
||||
title="Select Model for Codex"
|
||||
title={t("selectModelForTool", { tool: "Codex" })}
|
||||
/>
|
||||
|
||||
<ManualConfigModal
|
||||
isOpen={showManualConfigModal}
|
||||
onClose={() => setShowManualConfigModal(false)}
|
||||
title="Codex CLI - Manual Configuration"
|
||||
title={t("codexManualConfiguration")}
|
||||
configs={getManualConfigs()}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useEffect, useRef, useState, useCallback } from "react";
|
||||
import { Card, Button, ModelSelectModal } from "@/shared/components";
|
||||
import Image from "next/image";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function DefaultToolCard({
|
||||
toolId,
|
||||
@@ -15,6 +16,17 @@ export default function DefaultToolCard({
|
||||
cloudEnabled = false,
|
||||
batchStatus,
|
||||
}) {
|
||||
const t = useTranslations("cliTools");
|
||||
const translateOrFallback = useCallback(
|
||||
(key, fallback, values) => {
|
||||
try {
|
||||
return t(key, values);
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
},
|
||||
[t]
|
||||
);
|
||||
const [copiedField, setCopiedField] = useState(null);
|
||||
const [showModelModal, setShowModelModal] = useState(false);
|
||||
const [modelValue, setModelValue] = useState("");
|
||||
@@ -65,7 +77,7 @@ export default function DefaultToolCard({
|
||||
fetch(`/api/cli-tools/runtime/${toolId}`)
|
||||
.then((res) => res.json())
|
||||
.then((data) => setRuntimeStatus(data))
|
||||
.catch((error) => setRuntimeStatus({ error: error?.message || "runtime_check_failed" }));
|
||||
.catch((error) => setRuntimeStatus({ error: error?.message || t("runtimeCheckFailed") }));
|
||||
}, [isExpanded, runtimeStatus, toolId]);
|
||||
|
||||
const replaceVars = (text) => {
|
||||
@@ -74,7 +86,7 @@ export default function DefaultToolCard({
|
||||
? selectedApiKey
|
||||
: !cloudEnabled
|
||||
? "sk_omniroute"
|
||||
: "your-api-key";
|
||||
: t("yourApiKeyPlaceholder");
|
||||
|
||||
const normalizedBaseUrl = baseUrl || "http://localhost:20128";
|
||||
const baseUrlWithV1 = normalizedBaseUrl.endsWith("/v1")
|
||||
@@ -84,7 +96,7 @@ export default function DefaultToolCard({
|
||||
return text
|
||||
.replace(/\{\{baseUrl\}\}/g, baseUrlWithV1)
|
||||
.replace(/\{\{apiKey\}\}/g, keyToUse)
|
||||
.replace(/\{\{model\}\}/g, modelValue || "provider/model-id");
|
||||
.replace(/\{\{model\}\}/g, modelValue || t("modelPlaceholder"));
|
||||
};
|
||||
|
||||
const handleCopy = async (text, field) => {
|
||||
@@ -128,9 +140,9 @@ export default function DefaultToolCard({
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
setMessage({ type: "success", text: data.message || "Configuration saved!" });
|
||||
setMessage({ type: "success", text: data.message || t("configurationSaved") });
|
||||
} else {
|
||||
setMessage({ type: "error", text: data.error || "Failed to save" });
|
||||
setMessage({ type: "error", text: data.error || t("failedToSave") });
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage({ type: "error", text: error.message });
|
||||
@@ -169,7 +181,7 @@ export default function DefaultToolCard({
|
||||
</>
|
||||
) : (
|
||||
<span className="text-sm text-text-muted">
|
||||
{cloudEnabled ? "No API keys - Create one in Keys page" : "sk_omniroute"}
|
||||
{cloudEnabled ? t("noApiKeysCreateOne") : "sk_omniroute"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -183,7 +195,7 @@ export default function DefaultToolCard({
|
||||
type="text"
|
||||
value={modelValue}
|
||||
onChange={(e) => handleModelChange(e.target.value)}
|
||||
placeholder="provider/model-id"
|
||||
placeholder={t("modelPlaceholder")}
|
||||
className="flex-1 px-3 py-2 bg-bg-secondary rounded-lg text-sm border border-border focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
/>
|
||||
<button
|
||||
@@ -195,7 +207,7 @@ export default function DefaultToolCard({
|
||||
: "opacity-50 cursor-not-allowed border-border"
|
||||
}`}
|
||||
>
|
||||
Select Model
|
||||
{t("selectModel")}
|
||||
</button>
|
||||
{modelValue && (
|
||||
<>
|
||||
@@ -210,7 +222,7 @@ export default function DefaultToolCard({
|
||||
<button
|
||||
onClick={() => handleModelChange("")}
|
||||
className="p-2 text-text-muted hover:text-red-500 rounded transition-colors"
|
||||
title="Clear"
|
||||
title={t("clear")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-lg">close</span>
|
||||
</button>
|
||||
@@ -251,7 +263,9 @@ export default function DefaultToolCard({
|
||||
return (
|
||||
<div key={index} className={`flex items-start gap-3 p-3 rounded-lg border ${bgClass}`}>
|
||||
<span className={`material-symbols-outlined text-lg ${iconClass}`}>{icon}</span>
|
||||
<p className={`text-sm ${textClass}`}>{note.text}</p>
|
||||
<p className={`text-sm ${textClass}`}>
|
||||
{translateOrFallback(`guides.${toolId}.notes.${index}`, note.text)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -265,7 +279,7 @@ export default function DefaultToolCard({
|
||||
};
|
||||
|
||||
const renderGuideSteps = () => {
|
||||
if (!tool.guideSteps) return <p className="text-text-muted text-sm">Coming soon...</p>;
|
||||
if (!tool.guideSteps) return <p className="text-text-muted text-sm">{t("comingSoon")}</p>;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
@@ -274,7 +288,7 @@ export default function DefaultToolCard({
|
||||
<span className="material-symbols-outlined animate-spin text-base">
|
||||
progress_activity
|
||||
</span>
|
||||
<span>Checking runtime...</span>
|
||||
<span>{t("checkingRuntime")}</span>
|
||||
</div>
|
||||
)}
|
||||
{!checkingRuntime && runtimeStatus && !runtimeStatus.error && (
|
||||
@@ -289,16 +303,18 @@ export default function DefaultToolCard({
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-sm text-blue-700 dark:text-blue-300">
|
||||
{runtimeStatus.reason === "not_required"
|
||||
? "Guide-only integration: no local binary required"
|
||||
? t("guideOnlyIntegration")
|
||||
: runtimeStatus.installed && runtimeStatus.runnable
|
||||
? "CLI runtime detected and healthy"
|
||||
? t("cliRuntimeDetected")
|
||||
: runtimeStatus.installed
|
||||
? `CLI found but not runnable${runtimeStatus.reason ? ` (${runtimeStatus.reason})` : ""}`
|
||||
: "CLI runtime not detected"}
|
||||
? t("cliFoundNotRunnable", {
|
||||
reason: runtimeStatus.reason ? `: ${runtimeStatus.reason}` : "",
|
||||
})
|
||||
: t("cliRuntimeNotDetected")}
|
||||
</p>
|
||||
{runtimeStatus.commandPath && (
|
||||
<p className="text-xs text-text-muted">
|
||||
Binary:{" "}
|
||||
{t("binary")}:{" "}
|
||||
<code className="px-1 py-0.5 rounded bg-black/5 dark:bg-white/10">
|
||||
{runtimeStatus.commandPath}
|
||||
</code>
|
||||
@@ -306,7 +322,7 @@ export default function DefaultToolCard({
|
||||
)}
|
||||
{runtimeStatus.configPath && (
|
||||
<p className="text-xs text-text-muted">
|
||||
Config path:{" "}
|
||||
{t("configPath")}:{" "}
|
||||
<code className="px-1 py-0.5 rounded bg-black/5 dark:bg-white/10">
|
||||
{runtimeStatus.configPath}
|
||||
</code>
|
||||
@@ -319,7 +335,7 @@ export default function DefaultToolCard({
|
||||
<div className="flex items-start gap-3 p-3 bg-red-500/10 border border-red-500/30 rounded-lg">
|
||||
<span className="material-symbols-outlined text-red-500 text-lg">error</span>
|
||||
<p className="text-sm text-red-600 dark:text-red-400">
|
||||
Failed to check runtime status.
|
||||
{t("failedCheckRuntimeStatus")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -334,8 +350,14 @@ export default function DefaultToolCard({
|
||||
{item.step}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium text-text">{item.title}</p>
|
||||
{item.desc && <p className="text-sm text-text-muted mt-0.5">{item.desc}</p>}
|
||||
<p className="font-medium text-text">
|
||||
{translateOrFallback(`guides.${toolId}.steps.${item.step}.title`, item.title)}
|
||||
</p>
|
||||
{item.desc && (
|
||||
<p className="text-sm text-text-muted mt-0.5">
|
||||
{translateOrFallback(`guides.${toolId}.steps.${item.step}.desc`, item.desc)}
|
||||
</p>
|
||||
)}
|
||||
{item.type === "apiKeySelector" && renderApiKeySelector()}
|
||||
{item.type === "modelSelector" && renderModelSelector()}
|
||||
{item.value && (
|
||||
@@ -372,7 +394,7 @@ export default function DefaultToolCard({
|
||||
<span className="material-symbols-outlined text-sm">
|
||||
{copiedField === "codeblock" ? "check" : "content_copy"}
|
||||
</span>
|
||||
{copiedField === "codeblock" ? "Copied!" : "Copy"}
|
||||
{copiedField === "codeblock" ? t("copied") : t("copy")}
|
||||
</button>
|
||||
</div>
|
||||
<pre className="p-4 bg-bg-secondary rounded-lg border border-border overflow-x-auto">
|
||||
@@ -405,8 +427,8 @@ export default function DefaultToolCard({
|
||||
disabled={!modelValue}
|
||||
loading={saving}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">save</span>Save
|
||||
Config
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">save</span>
|
||||
{t("saveConfig")}
|
||||
</Button>
|
||||
)}
|
||||
{tool.codeBlock && (
|
||||
@@ -418,7 +440,7 @@ export default function DefaultToolCard({
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">
|
||||
{copiedField === "codeblock" ? "check" : "content_copy"}
|
||||
</span>
|
||||
{copiedField === "codeblock" ? "Copied!" : "Copy Config"}
|
||||
{copiedField === "codeblock" ? t("copied") : t("copyConfig")}
|
||||
</Button>
|
||||
)}
|
||||
{modelValue && (
|
||||
@@ -426,7 +448,7 @@ export default function DefaultToolCard({
|
||||
<span className="material-symbols-outlined text-[14px] text-green-500">
|
||||
check_circle
|
||||
</span>
|
||||
Selection saved
|
||||
{t("selectionSaved")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -496,7 +518,7 @@ export default function DefaultToolCard({
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1.5 px-2 py-0.5 text-[11px] font-medium rounded-full bg-blue-500/10 text-blue-600 dark:text-blue-400">
|
||||
<span className="size-1.5 rounded-full bg-blue-500" />
|
||||
Guide
|
||||
{t("guide")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -504,7 +526,7 @@ export default function DefaultToolCard({
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1.5 px-2 py-0.5 text-[11px] font-medium rounded-full bg-green-500/10 text-green-600 dark:text-green-400">
|
||||
<span className="size-1.5 rounded-full bg-green-500" />
|
||||
Detected
|
||||
{t("detected")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -512,7 +534,7 @@ export default function DefaultToolCard({
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1.5 px-2 py-0.5 text-[11px] font-medium rounded-full bg-zinc-500/10 text-zinc-500 dark:text-zinc-400">
|
||||
<span className="size-1.5 rounded-full bg-zinc-400 dark:bg-zinc-500" />
|
||||
Not installed
|
||||
{t("notInstalled")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -520,14 +542,16 @@ export default function DefaultToolCard({
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1.5 px-2 py-0.5 text-[11px] font-medium rounded-full bg-yellow-500/10 text-yellow-600 dark:text-yellow-400">
|
||||
<span className="size-1.5 rounded-full bg-yellow-500" />
|
||||
Not ready
|
||||
{t("notReady")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
</div>
|
||||
<p className="text-xs text-text-muted truncate">{tool.description}</p>
|
||||
<p className="text-xs text-text-muted truncate">
|
||||
{translateOrFallback(`toolDescriptions.${toolId}`, tool.description)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
@@ -545,7 +569,7 @@ export default function DefaultToolCard({
|
||||
onSelect={handleSelectModel}
|
||||
selectedModel={modelValue}
|
||||
activeProviders={activeProviders}
|
||||
title="Select Model"
|
||||
title={t("selectModel")}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState, useEffect, useRef } from "react";
|
||||
import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components";
|
||||
import Image from "next/image";
|
||||
import CliStatusBadge from "./CliStatusBadge";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL;
|
||||
|
||||
@@ -19,6 +20,7 @@ export default function DroidToolCard({
|
||||
batchStatus,
|
||||
lastConfiguredAt,
|
||||
}) {
|
||||
const t = useTranslations("cliTools");
|
||||
const [droidStatus, setDroidStatus] = useState(null);
|
||||
const [checkingDroid, setCheckingDroid] = useState(false);
|
||||
const [applying, setApplying] = useState(false);
|
||||
@@ -137,10 +139,10 @@ export default function DroidToolCard({
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
setMessage({ type: "success", text: "Settings applied successfully!" });
|
||||
setMessage({ type: "success", text: t("settingsApplied") });
|
||||
checkDroidStatus();
|
||||
} else {
|
||||
setMessage({ type: "error", text: data.error || "Failed to apply settings" });
|
||||
setMessage({ type: "error", text: data.error || t("failedApplySettings") });
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage({ type: "error", text: error.message });
|
||||
@@ -156,12 +158,12 @@ export default function DroidToolCard({
|
||||
const res = await fetch("/api/cli-tools/droid-settings", { method: "DELETE" });
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
setMessage({ type: "success", text: "Settings reset successfully!" });
|
||||
setMessage({ type: "success", text: t("settingsReset") });
|
||||
setSelectedModel("");
|
||||
setSelectedApiKey("");
|
||||
checkDroidStatus();
|
||||
} else {
|
||||
setMessage({ type: "error", text: data.error || "Failed to reset settings" });
|
||||
setMessage({ type: "error", text: data.error || t("failedResetSettings") });
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage({ type: "error", text: error.message });
|
||||
@@ -197,11 +199,11 @@ export default function DroidToolCard({
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
setMessage({ type: "success", text: "Backup restored!" });
|
||||
setMessage({ type: "success", text: t("backupRestored") });
|
||||
checkDroidStatus();
|
||||
fetchBackups();
|
||||
} else {
|
||||
setMessage({ type: "error", text: data.error || "Failed to restore" });
|
||||
setMessage({ type: "error", text: data.error || t("failedRestore") });
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage({ type: "error", text: error.message });
|
||||
@@ -274,7 +276,7 @@ export default function DroidToolCard({
|
||||
lastConfiguredAt={lastConfiguredAt}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-text-muted truncate">{tool.description}</p>
|
||||
<p className="text-xs text-text-muted truncate">{t("toolDescriptions.droid")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
@@ -289,7 +291,7 @@ export default function DroidToolCard({
|
||||
{checkingDroid && (
|
||||
<div className="flex items-center gap-2 text-text-muted">
|
||||
<span className="material-symbols-outlined animate-spin">progress_activity</span>
|
||||
<span>Checking Factory Droid CLI...</span>
|
||||
<span>{t("checkingCli", { tool: "Factory Droid" })}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -299,13 +301,16 @@ export default function DroidToolCard({
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-yellow-600 dark:text-yellow-400">
|
||||
{droidStatus.installed
|
||||
? "Factory Droid CLI not runnable"
|
||||
: "Factory Droid CLI not installed"}
|
||||
? t("cliNotRunnable", { tool: "Factory Droid" })
|
||||
: t("cliNotInstalled", { tool: "Factory Droid" })}
|
||||
</p>
|
||||
<p className="text-sm text-text-muted">
|
||||
{droidStatus.installed
|
||||
? `Factory Droid CLI was found but failed runtime healthcheck${droidStatus.reason ? ` (${droidStatus.reason})` : ""}.`
|
||||
: "Please install Factory Droid CLI to use this feature."}
|
||||
? t("cliFoundFailedHealthcheck", {
|
||||
tool: "Factory Droid",
|
||||
reason: droidStatus.reason ? ` (${droidStatus.reason})` : "",
|
||||
})
|
||||
: t("installCliPrompt", { tool: "Factory Droid" })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -319,7 +324,7 @@ export default function DroidToolCard({
|
||||
?.baseUrl && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">
|
||||
Current
|
||||
{t("current")}
|
||||
</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">
|
||||
arrow_forward
|
||||
@@ -336,7 +341,7 @@ export default function DroidToolCard({
|
||||
{/* Base URL */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">
|
||||
Base URL
|
||||
{t("baseUrl")}
|
||||
</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">
|
||||
arrow_forward
|
||||
@@ -345,14 +350,14 @@ export default function DroidToolCard({
|
||||
type="text"
|
||||
value={getDisplayUrl()}
|
||||
onChange={(e) => setCustomBaseUrl(e.target.value)}
|
||||
placeholder="https://.../v1"
|
||||
placeholder={t("baseUrlPlaceholder")}
|
||||
className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
/>
|
||||
{customBaseUrl && customBaseUrl !== baseUrl && (
|
||||
<button
|
||||
onClick={() => setCustomBaseUrl("")}
|
||||
className="p-1 text-text-muted hover:text-primary rounded transition-colors"
|
||||
title="Reset to default"
|
||||
title={t("resetToDefault")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">restart_alt</span>
|
||||
</button>
|
||||
@@ -362,7 +367,7 @@ export default function DroidToolCard({
|
||||
{/* API Key */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">
|
||||
API Key
|
||||
{t("apiKey")}
|
||||
</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">
|
||||
arrow_forward
|
||||
@@ -381,9 +386,7 @@ export default function DroidToolCard({
|
||||
</select>
|
||||
) : (
|
||||
<span className="flex-1 text-xs text-text-muted px-2 py-1.5">
|
||||
{cloudEnabled
|
||||
? "No API keys - Create one in Keys page"
|
||||
: "sk_omniroute (default)"}
|
||||
{cloudEnabled ? t("noApiKeysCreateOne") : t("defaultOmnirouteKey")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -391,7 +394,7 @@ export default function DroidToolCard({
|
||||
{/* Model */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">
|
||||
Model
|
||||
{t("model")}
|
||||
</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">
|
||||
arrow_forward
|
||||
@@ -400,7 +403,7 @@ export default function DroidToolCard({
|
||||
type="text"
|
||||
value={selectedModel}
|
||||
onChange={(e) => setSelectedModel(e.target.value)}
|
||||
placeholder="provider/model-id"
|
||||
placeholder={t("providerModelPlaceholder")}
|
||||
className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
/>
|
||||
<button
|
||||
@@ -408,13 +411,13 @@ export default function DroidToolCard({
|
||||
disabled={!hasActiveProviders}
|
||||
className={`px-2 py-1.5 rounded border text-xs transition-colors shrink-0 whitespace-nowrap ${hasActiveProviders ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}
|
||||
>
|
||||
Select Model
|
||||
{t("selectModel")}
|
||||
</button>
|
||||
{selectedModel && (
|
||||
<button
|
||||
onClick={() => setSelectedModel("")}
|
||||
className="p-1 text-text-muted hover:text-red-500 rounded transition-colors"
|
||||
title="Clear"
|
||||
title={t("clear")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">close</span>
|
||||
</button>
|
||||
@@ -441,7 +444,8 @@ export default function DroidToolCard({
|
||||
disabled={!selectedModel}
|
||||
loading={applying}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">save</span>Apply
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">save</span>
|
||||
{t("apply")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -450,11 +454,12 @@ export default function DroidToolCard({
|
||||
disabled={!droidStatus?.hasOmniRoute}
|
||||
loading={restoring}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">restore</span>Reset
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">restore</span>
|
||||
{t("reset")}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => setShowManualConfigModal(true)}>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">content_copy</span>
|
||||
Manual Config
|
||||
{t("manualConfig")}
|
||||
</Button>
|
||||
<div className="flex-1" />
|
||||
<Button
|
||||
@@ -465,7 +470,8 @@ export default function DroidToolCard({
|
||||
if (!showBackups) fetchBackups();
|
||||
}}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">history</span>Backups
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">history</span>
|
||||
{t("backups")}
|
||||
{backups.length > 0 && ` (${backups.length})`}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -474,12 +480,10 @@ export default function DroidToolCard({
|
||||
<div className="mt-2 p-3 bg-surface border border-border rounded-lg">
|
||||
<h4 className="text-xs font-semibold text-text-main mb-2 flex items-center gap-1">
|
||||
<span className="material-symbols-outlined text-[14px]">history</span>
|
||||
Config Backups
|
||||
{t("configBackups")}
|
||||
</h4>
|
||||
{backups.length === 0 ? (
|
||||
<p className="text-xs text-text-muted">
|
||||
No backups yet. Backups are created automatically before each Apply or Reset.
|
||||
</p>
|
||||
<p className="text-xs text-text-muted">{t("noBackupsYet")}</p>
|
||||
) : (
|
||||
<div className="space-y-1.5">
|
||||
{backups.map((b) => (
|
||||
@@ -501,7 +505,7 @@ export default function DroidToolCard({
|
||||
disabled={restoringBackup === b.id}
|
||||
className="px-2 py-0.5 bg-primary/10 text-primary rounded text-[10px] font-medium hover:bg-primary/20 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{restoringBackup === b.id ? "..." : "Restore"}
|
||||
{restoringBackup === b.id ? "..." : t("restore")}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
@@ -521,13 +525,13 @@ export default function DroidToolCard({
|
||||
selectedModel={selectedModel}
|
||||
activeProviders={activeProviders}
|
||||
modelAliases={modelAliases}
|
||||
title="Select Model for Factory Droid"
|
||||
title={t("selectModelForTool", { tool: "Factory Droid" })}
|
||||
/>
|
||||
|
||||
<ManualConfigModal
|
||||
isOpen={showManualConfigModal}
|
||||
onClose={() => setShowManualConfigModal(false)}
|
||||
title="Factory Droid - Manual Configuration"
|
||||
title={t("droidManualConfiguration")}
|
||||
configs={getManualConfigs()}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState, useEffect, useRef } from "react";
|
||||
import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components";
|
||||
import Image from "next/image";
|
||||
import CliStatusBadge from "./CliStatusBadge";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL;
|
||||
|
||||
@@ -19,6 +20,7 @@ export default function KiloToolCard({
|
||||
batchStatus,
|
||||
lastConfiguredAt,
|
||||
}) {
|
||||
const t = useTranslations("cliTools");
|
||||
const [kiloStatus, setKiloStatus] = useState(null);
|
||||
const [checkingKilo, setCheckingKilo] = useState(false);
|
||||
const [applying, setApplying] = useState(false);
|
||||
@@ -95,12 +97,12 @@ export default function KiloToolCard({
|
||||
body: JSON.stringify({ tool: "kilo", backupId }),
|
||||
});
|
||||
if (res.ok) {
|
||||
setMessage({ type: "success", text: "Backup restored! Reloading status..." });
|
||||
setMessage({ type: "success", text: t("backupRestoredReloading") });
|
||||
await checkKiloStatus();
|
||||
await fetchBackups();
|
||||
} else {
|
||||
const data = await res.json();
|
||||
setMessage({ type: "error", text: data.error || "Failed to restore backup" });
|
||||
setMessage({ type: "error", text: data.error || t("failedRestoreBackup") });
|
||||
}
|
||||
} catch (e) {
|
||||
setMessage({ type: "error", text: e.message });
|
||||
@@ -147,11 +149,11 @@ export default function KiloToolCard({
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
setMessage({ type: "success", text: data.message || "Applied!" });
|
||||
setMessage({ type: "success", text: data.message || t("applied") });
|
||||
await checkKiloStatus();
|
||||
await fetchBackups();
|
||||
} else {
|
||||
setMessage({ type: "error", text: data.error || "Failed" });
|
||||
setMessage({ type: "error", text: data.error || t("failed") });
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage({ type: "error", text: error.message });
|
||||
@@ -167,13 +169,13 @@ export default function KiloToolCard({
|
||||
const res = await fetch("/api/cli-tools/kilo-settings", { method: "DELETE" });
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
setMessage({ type: "success", text: data.message || "Reset!" });
|
||||
setMessage({ type: "success", text: data.message || t("resetDone") });
|
||||
setSelectedModel("");
|
||||
hasInitializedModel.current = false;
|
||||
await checkKiloStatus();
|
||||
await fetchBackups();
|
||||
} else {
|
||||
setMessage({ type: "error", text: data.error || "Failed" });
|
||||
setMessage({ type: "error", text: data.error || t("failed") });
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage({ type: "error", text: error.message });
|
||||
@@ -226,7 +228,7 @@ export default function KiloToolCard({
|
||||
lastConfiguredAt={lastConfiguredAt}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-text-muted truncate">{tool.description}</p>
|
||||
<p className="text-xs text-text-muted truncate">{t("toolDescriptions.kilo")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
@@ -243,7 +245,7 @@ export default function KiloToolCard({
|
||||
<span className="material-symbols-outlined animate-spin text-base">
|
||||
progress_activity
|
||||
</span>
|
||||
<span>Checking Kilo Code CLI...</span>
|
||||
<span>{t("checkingCli", { tool: "Kilo Code" })}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -259,14 +261,14 @@ export default function KiloToolCard({
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-sm font-medium">
|
||||
{cliReady
|
||||
? "Kilo Code CLI detected and ready"
|
||||
? t("cliDetectedReady", { tool: "Kilo Code" })
|
||||
: kiloStatus.installed
|
||||
? "Kilo Code CLI installed but not runnable"
|
||||
: "Kilo Code CLI not detected"}
|
||||
? t("cliNotRunnable", { tool: "Kilo Code" })
|
||||
: t("cliNotDetected", { tool: "Kilo Code" })}
|
||||
</p>
|
||||
{kiloStatus.commandPath && (
|
||||
<p className="text-xs text-text-muted">
|
||||
Binary:{" "}
|
||||
{t("binary")}:{" "}
|
||||
<code className="px-1 py-0.5 rounded bg-black/5 dark:bg-white/10">
|
||||
{kiloStatus.commandPath}
|
||||
</code>
|
||||
@@ -274,7 +276,7 @@ export default function KiloToolCard({
|
||||
)}
|
||||
{kiloStatus.authPath && (
|
||||
<p className="text-xs text-text-muted">
|
||||
Auth:{" "}
|
||||
{t("auth")}:{" "}
|
||||
<code className="px-1 py-0.5 rounded bg-black/5 dark:bg-white/10">
|
||||
{kiloStatus.authPath}
|
||||
</code>
|
||||
@@ -293,10 +295,11 @@ export default function KiloToolCard({
|
||||
</span>
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-sm text-green-700 dark:text-green-300">
|
||||
OmniRoute is configured as OpenAI-compatible provider
|
||||
{t("omnirouteConfiguredOpenAiCompatible")}
|
||||
</p>
|
||||
<p className="text-xs text-text-muted">
|
||||
Providers: <strong>{kiloStatus.settings?.auth?.join(", ") || "—"}</strong>
|
||||
{t("providers")}:{" "}
|
||||
<strong>{kiloStatus.settings?.auth?.join(", ") || "—"}</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -304,13 +307,13 @@ export default function KiloToolCard({
|
||||
|
||||
{/* Model selection */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm text-text-muted">Model</label>
|
||||
<label className="text-sm text-text-muted">{t("model")}</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={selectedModel}
|
||||
onChange={(e) => setSelectedModel(e.target.value)}
|
||||
placeholder="provider/model-id"
|
||||
placeholder={t("providerModelPlaceholder")}
|
||||
className="flex-1 px-3 py-2 bg-bg-secondary rounded-lg text-sm border border-border focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
/>
|
||||
<Button
|
||||
@@ -319,7 +322,7 @@ export default function KiloToolCard({
|
||||
onClick={() => setModalOpen(true)}
|
||||
disabled={!hasActiveProviders}
|
||||
>
|
||||
Select
|
||||
{t("select")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -333,7 +336,7 @@ export default function KiloToolCard({
|
||||
|
||||
{/* API Key selection */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm text-text-muted">API Key</label>
|
||||
<label className="text-sm text-text-muted">{t("apiKey")}</label>
|
||||
{apiKeys && apiKeys.length > 0 ? (
|
||||
<select
|
||||
value={selectedApiKey}
|
||||
@@ -348,7 +351,7 @@ export default function KiloToolCard({
|
||||
</select>
|
||||
) : (
|
||||
<p className="text-sm text-text-muted">
|
||||
{cloudEnabled ? "No API keys available" : "Using default: sk_omniroute"}
|
||||
{cloudEnabled ? t("noApiKeysAvailable") : t("usingDefaultOmniroute")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -363,14 +366,14 @@ export default function KiloToolCard({
|
||||
loading={applying}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">save</span>
|
||||
{configStatus === "configured" ? "Update Config" : "Apply Config"}
|
||||
{configStatus === "configured" ? t("updateConfig") : t("applyConfig")}
|
||||
</Button>
|
||||
{configStatus === "configured" && (
|
||||
<Button variant="outline" size="sm" onClick={handleReset} loading={restoring}>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">
|
||||
restart_alt
|
||||
</span>
|
||||
Reset
|
||||
{t("reset")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -399,7 +402,7 @@ export default function KiloToolCard({
|
||||
chevron_right
|
||||
</span>
|
||||
<span className="material-symbols-outlined text-[16px]">backup</span>
|
||||
Backups {backups.length > 0 && `(${backups.length})`}
|
||||
{t("backups")} {backups.length > 0 && `(${backups.length})`}
|
||||
</button>
|
||||
{showBackups && backups.length > 0 && (
|
||||
<div className="mt-2 flex flex-col gap-1.5 pl-6">
|
||||
@@ -420,14 +423,14 @@ export default function KiloToolCard({
|
||||
onClick={() => handleRestoreBackup(b.id)}
|
||||
loading={restoringBackup === b.id}
|
||||
>
|
||||
Restore
|
||||
{t("restore")}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{showBackups && backups.length === 0 && (
|
||||
<p className="mt-2 pl-6 text-xs text-text-muted">No backups available.</p>
|
||||
<p className="mt-2 pl-6 text-xs text-text-muted">{t("noBackupsAvailable")}</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
@@ -443,13 +446,13 @@ export default function KiloToolCard({
|
||||
onSelect={handleSelectModel}
|
||||
selectedModel={selectedModel}
|
||||
activeProviders={activeProviders}
|
||||
title="Select Model for Kilo Code"
|
||||
title={t("selectModelForTool", { tool: "Kilo Code" })}
|
||||
/>
|
||||
{showManualConfigModal && (
|
||||
<ManualConfigModal
|
||||
isOpen={showManualConfigModal}
|
||||
onClose={() => setShowManualConfigModal(false)}
|
||||
title="Kilo Code Manual Configuration"
|
||||
title={t("kiloManualConfiguration")}
|
||||
{...({
|
||||
onApply: handleManualConfig,
|
||||
currentConfig: {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState, useEffect, useRef } from "react";
|
||||
import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components";
|
||||
import Image from "next/image";
|
||||
import CliStatusBadge from "./CliStatusBadge";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL;
|
||||
|
||||
@@ -19,6 +20,7 @@ export default function OpenClawToolCard({
|
||||
batchStatus,
|
||||
lastConfiguredAt,
|
||||
}) {
|
||||
const t = useTranslations("cliTools");
|
||||
const [openclawStatus, setOpenclawStatus] = useState(null);
|
||||
const [checkingOpenclaw, setCheckingOpenclaw] = useState(false);
|
||||
const [applying, setApplying] = useState(false);
|
||||
@@ -138,10 +140,10 @@ export default function OpenClawToolCard({
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
setMessage({ type: "success", text: "Settings applied successfully!" });
|
||||
setMessage({ type: "success", text: t("settingsApplied") });
|
||||
checkOpenclawStatus();
|
||||
} else {
|
||||
setMessage({ type: "error", text: data.error || "Failed to apply settings" });
|
||||
setMessage({ type: "error", text: data.error || t("failedApplySettings") });
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage({ type: "error", text: error.message });
|
||||
@@ -157,12 +159,12 @@ export default function OpenClawToolCard({
|
||||
const res = await fetch("/api/cli-tools/openclaw-settings", { method: "DELETE" });
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
setMessage({ type: "success", text: "Settings reset successfully!" });
|
||||
setMessage({ type: "success", text: t("settingsReset") });
|
||||
setSelectedModel("");
|
||||
setSelectedApiKey("");
|
||||
checkOpenclawStatus();
|
||||
} else {
|
||||
setMessage({ type: "error", text: data.error || "Failed to reset settings" });
|
||||
setMessage({ type: "error", text: data.error || t("failedResetSettings") });
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage({ type: "error", text: error.message });
|
||||
@@ -198,11 +200,11 @@ export default function OpenClawToolCard({
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
setMessage({ type: "success", text: "Backup restored!" });
|
||||
setMessage({ type: "success", text: t("backupRestored") });
|
||||
checkOpenclawStatus();
|
||||
fetchBackups();
|
||||
} else {
|
||||
setMessage({ type: "error", text: data.error || "Failed to restore" });
|
||||
setMessage({ type: "error", text: data.error || t("failedRestore") });
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage({ type: "error", text: error.message });
|
||||
@@ -278,7 +280,7 @@ export default function OpenClawToolCard({
|
||||
lastConfiguredAt={lastConfiguredAt}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-text-muted truncate">{tool.description}</p>
|
||||
<p className="text-xs text-text-muted truncate">{t("toolDescriptions.openclaw")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
@@ -293,7 +295,7 @@ export default function OpenClawToolCard({
|
||||
{checkingOpenclaw && (
|
||||
<div className="flex items-center gap-2 text-text-muted">
|
||||
<span className="material-symbols-outlined animate-spin">progress_activity</span>
|
||||
<span>Checking Open Claw CLI...</span>
|
||||
<span>{t("checkingCli", { tool: "Open Claw" })}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -303,13 +305,16 @@ export default function OpenClawToolCard({
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-yellow-600 dark:text-yellow-400">
|
||||
{openclawStatus.installed
|
||||
? "Open Claw CLI not runnable"
|
||||
: "Open Claw CLI not installed"}
|
||||
? t("cliNotRunnable", { tool: "Open Claw" })
|
||||
: t("cliNotInstalled", { tool: "Open Claw" })}
|
||||
</p>
|
||||
<p className="text-sm text-text-muted">
|
||||
{openclawStatus.installed
|
||||
? `Open Claw CLI was found but failed runtime healthcheck${openclawStatus.reason ? ` (${openclawStatus.reason})` : ""}.`
|
||||
: "Please install Open Claw CLI to use this feature."}
|
||||
? t("cliFoundFailedHealthcheck", {
|
||||
tool: "Open Claw",
|
||||
reason: openclawStatus.reason ? ` (${openclawStatus.reason})` : "",
|
||||
})
|
||||
: t("installCliPrompt", { tool: "Open Claw" })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -322,7 +327,7 @@ export default function OpenClawToolCard({
|
||||
{openclawStatus?.settings?.models?.providers?.["omniroute"]?.baseUrl && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">
|
||||
Current
|
||||
{t("current")}
|
||||
</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">
|
||||
arrow_forward
|
||||
@@ -336,7 +341,7 @@ export default function OpenClawToolCard({
|
||||
{/* Base URL */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">
|
||||
Base URL
|
||||
{t("baseUrl")}
|
||||
</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">
|
||||
arrow_forward
|
||||
@@ -345,14 +350,14 @@ export default function OpenClawToolCard({
|
||||
type="text"
|
||||
value={getDisplayUrl()}
|
||||
onChange={(e) => setCustomBaseUrl(e.target.value)}
|
||||
placeholder="https://.../v1"
|
||||
placeholder={t("baseUrlPlaceholder")}
|
||||
className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
/>
|
||||
{customBaseUrl && customBaseUrl !== baseUrl && (
|
||||
<button
|
||||
onClick={() => setCustomBaseUrl("")}
|
||||
className="p-1 text-text-muted hover:text-primary rounded transition-colors"
|
||||
title="Reset to default"
|
||||
title={t("resetToDefault")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">restart_alt</span>
|
||||
</button>
|
||||
@@ -362,7 +367,7 @@ export default function OpenClawToolCard({
|
||||
{/* API Key */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">
|
||||
API Key
|
||||
{t("apiKey")}
|
||||
</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">
|
||||
arrow_forward
|
||||
@@ -381,9 +386,7 @@ export default function OpenClawToolCard({
|
||||
</select>
|
||||
) : (
|
||||
<span className="flex-1 text-xs text-text-muted px-2 py-1.5">
|
||||
{cloudEnabled
|
||||
? "No API keys - Create one in Keys page"
|
||||
: "sk_omniroute (default)"}
|
||||
{cloudEnabled ? t("noApiKeysCreateOne") : t("defaultOmnirouteKey")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -391,7 +394,7 @@ export default function OpenClawToolCard({
|
||||
{/* Model */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">
|
||||
Model
|
||||
{t("model")}
|
||||
</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">
|
||||
arrow_forward
|
||||
@@ -400,7 +403,7 @@ export default function OpenClawToolCard({
|
||||
type="text"
|
||||
value={selectedModel}
|
||||
onChange={(e) => setSelectedModel(e.target.value)}
|
||||
placeholder="provider/model-id"
|
||||
placeholder={t("providerModelPlaceholder")}
|
||||
className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
/>
|
||||
<button
|
||||
@@ -408,13 +411,13 @@ export default function OpenClawToolCard({
|
||||
disabled={!hasActiveProviders}
|
||||
className={`px-2 py-1.5 rounded border text-xs transition-colors shrink-0 whitespace-nowrap ${hasActiveProviders ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}
|
||||
>
|
||||
Select Model
|
||||
{t("selectModel")}
|
||||
</button>
|
||||
{selectedModel && (
|
||||
<button
|
||||
onClick={() => setSelectedModel("")}
|
||||
className="p-1 text-text-muted hover:text-red-500 rounded transition-colors"
|
||||
title="Clear"
|
||||
title={t("clear")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">close</span>
|
||||
</button>
|
||||
@@ -441,7 +444,8 @@ export default function OpenClawToolCard({
|
||||
disabled={!selectedModel}
|
||||
loading={applying}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">save</span>Apply
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">save</span>
|
||||
{t("apply")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -450,11 +454,12 @@ export default function OpenClawToolCard({
|
||||
disabled={!openclawStatus?.hasOmniRoute}
|
||||
loading={restoring}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">restore</span>Reset
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">restore</span>
|
||||
{t("reset")}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => setShowManualConfigModal(true)}>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">content_copy</span>
|
||||
Manual Config
|
||||
{t("manualConfig")}
|
||||
</Button>
|
||||
<div className="flex-1" />
|
||||
<Button
|
||||
@@ -465,7 +470,8 @@ export default function OpenClawToolCard({
|
||||
if (!showBackups) fetchBackups();
|
||||
}}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">history</span>Backups
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">history</span>
|
||||
{t("backups")}
|
||||
{backups.length > 0 && ` (${backups.length})`}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -474,12 +480,10 @@ export default function OpenClawToolCard({
|
||||
<div className="mt-2 p-3 bg-surface border border-border rounded-lg">
|
||||
<h4 className="text-xs font-semibold text-text-main mb-2 flex items-center gap-1">
|
||||
<span className="material-symbols-outlined text-[14px]">history</span>
|
||||
Config Backups
|
||||
{t("configBackups")}
|
||||
</h4>
|
||||
{backups.length === 0 ? (
|
||||
<p className="text-xs text-text-muted">
|
||||
No backups yet. Backups are created automatically before each Apply or Reset.
|
||||
</p>
|
||||
<p className="text-xs text-text-muted">{t("noBackupsYet")}</p>
|
||||
) : (
|
||||
<div className="space-y-1.5">
|
||||
{backups.map((b) => (
|
||||
@@ -501,7 +505,7 @@ export default function OpenClawToolCard({
|
||||
disabled={restoringBackup === b.id}
|
||||
className="px-2 py-0.5 bg-primary/10 text-primary rounded text-[10px] font-medium hover:bg-primary/20 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{restoringBackup === b.id ? "..." : "Restore"}
|
||||
{restoringBackup === b.id ? "..." : t("restore")}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
@@ -521,13 +525,13 @@ export default function OpenClawToolCard({
|
||||
selectedModel={selectedModel}
|
||||
activeProviders={activeProviders}
|
||||
modelAliases={modelAliases}
|
||||
title="Select Model for Open Claw"
|
||||
title={t("selectModelForTool", { tool: "Open Claw" })}
|
||||
/>
|
||||
|
||||
<ManualConfigModal
|
||||
isOpen={showManualConfigModal}
|
||||
onClose={() => setShowManualConfigModal(false)}
|
||||
title="Open Claw - Manual Configuration"
|
||||
title={t("openClawManualConfiguration")}
|
||||
configs={getManualConfigs()}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
@@ -109,9 +109,9 @@ export default function APIPageClient({ machineId }) {
|
||||
return { ok: res.ok, status: res.status, data };
|
||||
} catch (error) {
|
||||
if (error?.name === "AbortError") {
|
||||
return { ok: false, status: 408, data: { error: "Cloud request timeout" } };
|
||||
return { ok: false, status: 408, data: { error: t("cloudRequestTimeout") } };
|
||||
}
|
||||
return { ok: false, status: 500, data: { error: error.message || "Cloud request failed" } };
|
||||
return { ok: false, status: 500, data: { error: error.message || t("cloudRequestFailed") } };
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
@@ -190,13 +190,13 @@ export default function APIPageClient({ machineId }) {
|
||||
setModalSuccess(false);
|
||||
|
||||
if (data.verified) {
|
||||
setCloudStatus({ type: "success", message: "Cloud Proxy connected and verified!" });
|
||||
setCloudStatus({ type: "success", message: t("cloudConnectedVerified") });
|
||||
} else {
|
||||
setCloudStatus({
|
||||
type: "warning",
|
||||
message: data.verifyError
|
||||
? `Connected — verification pending: ${data.verifyError}`
|
||||
: "Connected — verification pending",
|
||||
? t("connectedVerificationPendingWithError", { error: data.verifyError })
|
||||
: t("connectedVerificationPending"),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -208,16 +208,15 @@ export default function APIPageClient({ machineId }) {
|
||||
await loadCloudSettings();
|
||||
} else {
|
||||
// Sync failed — provide a helpful error message
|
||||
let errorMessage = data.error || "Failed to enable cloud";
|
||||
let errorMessage = data.error || t("failedEnable");
|
||||
if (status === 502 || status === 408) {
|
||||
errorMessage =
|
||||
"Could not reach cloud worker. Make sure the cloud service is running (npm run dev in /cloud).";
|
||||
errorMessage = t("cloudWorkerUnreachable");
|
||||
}
|
||||
setCloudStatus({ type: "error", message: errorMessage });
|
||||
setShowCloudModal(false);
|
||||
}
|
||||
} catch (error) {
|
||||
setCloudStatus({ type: "error", message: error.message || "Connection failed" });
|
||||
setCloudStatus({ type: "error", message: error.message || t("connectionFailed") });
|
||||
setShowCloudModal(false);
|
||||
} finally {
|
||||
setCloudSyncing(false);
|
||||
@@ -240,16 +239,16 @@ export default function APIPageClient({ machineId }) {
|
||||
|
||||
if (ok) {
|
||||
setCloudEnabled(false);
|
||||
setCloudStatus({ type: "success", message: "Cloud disabled successfully" });
|
||||
setCloudStatus({ type: "success", message: t("cloudDisabledSuccess") });
|
||||
setShowDisableModal(false);
|
||||
dispatchCloudChange();
|
||||
await loadCloudSettings();
|
||||
} else {
|
||||
setCloudStatus({ type: "error", message: data.error || "Failed to disable cloud" });
|
||||
setCloudStatus({ type: "error", message: data.error || t("failedDisable") });
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error disabling cloud:", error);
|
||||
setCloudStatus({ type: "error", message: "Failed to disable cloud" });
|
||||
setCloudStatus({ type: "error", message: t("failedDisable") });
|
||||
} finally {
|
||||
setCloudSyncing(false);
|
||||
setSyncStep("");
|
||||
@@ -263,12 +262,12 @@ export default function APIPageClient({ machineId }) {
|
||||
try {
|
||||
const { ok, data } = await postCloudAction("sync");
|
||||
if (ok) {
|
||||
setCloudStatus({ type: "success", message: "Synced successfully" });
|
||||
setCloudStatus({ type: "success", message: t("syncedSuccess") });
|
||||
} else {
|
||||
setCloudStatus({ type: "error", message: data.error });
|
||||
setCloudStatus({ type: "error", message: data.error || t("syncFailed") });
|
||||
}
|
||||
} catch (error) {
|
||||
setCloudStatus({ type: "error", message: error.message });
|
||||
setCloudStatus({ type: "error", message: error.message || t("syncFailed") });
|
||||
} finally {
|
||||
setCloudSyncing(false);
|
||||
}
|
||||
@@ -296,24 +295,6 @@ export default function APIPageClient({ machineId }) {
|
||||
// Use new format endpoint (machineId embedded in key)
|
||||
const currentEndpoint = cloudEnabled ? cloudEndpointNew : baseUrl;
|
||||
|
||||
const cloudBenefits = [
|
||||
{ icon: "public", title: "Access Anywhere", desc: "No port forwarding needed" },
|
||||
{ icon: "group", title: "Share Endpoint", desc: "Easy team collaboration" },
|
||||
{ icon: "schedule", title: "Always Online", desc: "24/7 availability" },
|
||||
{ icon: "speed", title: "Global Edge", desc: "Fast worldwide access" },
|
||||
];
|
||||
|
||||
const quickStartLinks = [
|
||||
{ label: "Documentation", href: "/docs" },
|
||||
{ label: "OpenAI API compatibility", href: "/docs#api-reference" },
|
||||
{ label: "Cherry/Codex compatibility", href: "/docs#client-compatibility" },
|
||||
{
|
||||
label: "Report issue",
|
||||
href: "https://github.com/diegosouzapw/OmniRoute/issues",
|
||||
external: true,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
{/* Endpoint Card */}
|
||||
@@ -407,10 +388,9 @@ export default function APIPageClient({ machineId }) {
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">{t("available")}</h2>
|
||||
<p className="text-sm text-text-muted">
|
||||
{Object.values(endpointData).reduce((acc, models) => acc + models.length, 0)} models
|
||||
across{" "}
|
||||
{
|
||||
[
|
||||
{t("modelsAcrossEndpoints", {
|
||||
models: Object.values(endpointData).reduce((acc, models) => acc + models.length, 0),
|
||||
endpoints: [
|
||||
endpointData.chat,
|
||||
endpointData.embeddings,
|
||||
endpointData.images,
|
||||
@@ -418,9 +398,8 @@ export default function APIPageClient({ machineId }) {
|
||||
endpointData.audioTranscription,
|
||||
endpointData.audioSpeech,
|
||||
endpointData.moderation,
|
||||
].filter((a) => a.length > 0).length
|
||||
}{" "}
|
||||
endpoints
|
||||
].filter((a) => a.length > 0).length,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -550,70 +529,6 @@ export default function APIPageClient({ machineId }) {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Cloud Proxy Card - Hidden */}
|
||||
{false && (
|
||||
<Card className={cloudEnabled ? "bg-primary/5" : ""}>
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={`p-2 rounded-lg ${cloudEnabled ? "bg-primary text-white" : "bg-sidebar text-text-muted"}`}
|
||||
>
|
||||
<span className="material-symbols-outlined text-xl">cloud</span>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">Cloud Proxy</h2>
|
||||
<p className="text-xs text-text-muted">
|
||||
{cloudEnabled ? "Connected & Ready" : "Access your API from anywhere"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{cloudEnabled ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon="cloud_off"
|
||||
onClick={() => handleCloudToggle(false)}
|
||||
disabled={cloudSyncing}
|
||||
className="bg-red-500/10! text-red-500! hover:bg-red-500/20! border-red-500/30!"
|
||||
>
|
||||
Disable
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="primary"
|
||||
icon="cloud_upload"
|
||||
onClick={() => handleCloudToggle(true)}
|
||||
disabled={cloudSyncing}
|
||||
className="bg-linear-to-r from-primary to-blue-500 hover:from-primary-hover hover:to-blue-600 px-6"
|
||||
>
|
||||
Enable Cloud
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Benefits Grid */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
{cloudBenefits.map((benefit) => (
|
||||
<div
|
||||
key={benefit.title}
|
||||
className="flex flex-col items-center text-center p-3 rounded-lg bg-sidebar/50"
|
||||
>
|
||||
<span className="material-symbols-outlined text-xl text-primary mb-1">
|
||||
{benefit.icon}
|
||||
</span>
|
||||
<p className="text-xs font-semibold">{benefit.title}</p>
|
||||
<p className="text-xs text-text-muted">{benefit.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Cloud Enable Modal */}
|
||||
<Modal
|
||||
isOpen={showCloudModal}
|
||||
@@ -634,13 +549,12 @@ export default function APIPageClient({ machineId }) {
|
||||
</div>
|
||||
|
||||
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4">
|
||||
<p className="text-sm text-yellow-800 dark:text-yellow-200 font-medium mb-1">Note</p>
|
||||
<p className="text-sm text-yellow-800 dark:text-yellow-200 font-medium mb-1">
|
||||
{tc("note")}
|
||||
</p>
|
||||
<ul className="text-sm text-yellow-700 dark:text-yellow-300 space-y-1">
|
||||
<li>
|
||||
• Cloud will keep your auth session for 1 day. If not used, it will be automatically
|
||||
deleted.
|
||||
</li>
|
||||
<li>• Cloud is currently unstable with Claude Code OAuth in some cases.</li>
|
||||
<li>• {t("cloudSessionNote")}</li>
|
||||
<li>• {t("cloudUnstableNote")}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -719,10 +633,10 @@ export default function APIPageClient({ machineId }) {
|
||||
warning
|
||||
</span>
|
||||
<div>
|
||||
<p className="text-sm text-red-800 dark:text-red-200 font-medium mb-1">Warning</p>
|
||||
<p className="text-sm text-red-700 dark:text-red-300">
|
||||
All auth sessions will be deleted from cloud.
|
||||
<p className="text-sm text-red-800 dark:text-red-200 font-medium mb-1">
|
||||
{tc("warning")}
|
||||
</p>
|
||||
<p className="text-sm text-red-700 dark:text-red-300">{t("disableWarning")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -794,6 +708,8 @@ APIPageClient.propTypes = {
|
||||
// -- Sub-component: Provider Models Modal ------------------------------------------
|
||||
|
||||
function ProviderModelsModal({ provider, models, copy, copied, onClose }) {
|
||||
const t = useTranslations("endpoint");
|
||||
const tc = useTranslations("common");
|
||||
// Get provider alias for matching models
|
||||
// Filter out parent models (models with parent field set) to avoid showing duplicates
|
||||
const providerAlias = provider.provider.alias || provider.id;
|
||||
@@ -826,13 +742,13 @@ function ProviderModelsModal({ provider, models, copy, copied, onClose }) {
|
||||
<code className="text-sm font-mono flex-1 truncate">{m.id}</code>
|
||||
{m.custom && (
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-primary/10 text-primary">
|
||||
custom
|
||||
{t("custom")}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={() => copy(m.id, copyKey)}
|
||||
className="p-1 hover:bg-sidebar rounded text-text-muted hover:text-primary opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
title="Copy model ID"
|
||||
title={tc("copy")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-sm">
|
||||
{copied === copyKey ? "check" : "content_copy"}
|
||||
@@ -847,17 +763,19 @@ function ProviderModelsModal({ provider, models, copy, copied, onClose }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen onClose={onClose} title={`${provider.provider.name} — Models`}>
|
||||
<Modal
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
title={t("providerModelsTitle", { provider: provider.provider.name })}
|
||||
>
|
||||
<div className="max-h-[60vh] overflow-y-auto">
|
||||
{providerModels.length === 0 ? (
|
||||
<p className="text-sm text-text-muted py-4 text-center">
|
||||
No models available for this provider.
|
||||
</p>
|
||||
<p className="text-sm text-text-muted py-4 text-center">{t("noModelsForProvider")}</p>
|
||||
) : (
|
||||
<>
|
||||
{renderModelGroup("Chat", "chat", chatModels)}
|
||||
{renderModelGroup("Embedding", "data_array", embeddingModels)}
|
||||
{renderModelGroup("Image", "image", imageModels)}
|
||||
{renderModelGroup(t("chat"), "chat", chatModels)}
|
||||
{renderModelGroup(t("embedding"), "data_array", embeddingModels)}
|
||||
{renderModelGroup(t("image"), "image", imageModels)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -889,6 +807,7 @@ function EndpointSection({
|
||||
copied,
|
||||
baseUrl,
|
||||
}) {
|
||||
const t = useTranslations("endpoint");
|
||||
const grouped = useMemo(() => {
|
||||
const map = {};
|
||||
for (const m of models) {
|
||||
@@ -920,7 +839,7 @@ function EndpointSection({
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-semibold text-sm">{title}</span>
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-surface text-text-muted font-medium">
|
||||
{models.length} {models.length === 1 ? "model" : "models"}
|
||||
{t("modelsCount", { count: models.length })}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-text-muted mt-0.5">{description}</p>
|
||||
|
||||
@@ -32,14 +32,16 @@ function formatBytes(bytes) {
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
const CB_COLORS = {
|
||||
CLOSED: { bg: "bg-green-500/10", text: "text-green-500", label: "Healthy" },
|
||||
OPEN: { bg: "bg-red-500/10", text: "text-red-500", label: "Open" },
|
||||
HALF_OPEN: { bg: "bg-amber-500/10", text: "text-amber-500", label: "Half-Open" },
|
||||
const CB_STYLES = {
|
||||
CLOSED: { bg: "bg-green-500/10", text: "text-green-500", labelKey: "healthy" },
|
||||
OPEN: { bg: "bg-red-500/10", text: "text-red-500", labelKey: "down" },
|
||||
HALF_OPEN: { bg: "bg-amber-500/10", text: "text-amber-500", labelKey: "recovering" },
|
||||
};
|
||||
|
||||
export default function HealthPage() {
|
||||
const t = useTranslations("health");
|
||||
const tc = useTranslations("common");
|
||||
const tp = useTranslations("providers");
|
||||
const [data, setData] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [lastRefresh, setLastRefresh] = useState(null);
|
||||
@@ -101,7 +103,8 @@ export default function HealthPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const fmtMs = (ms) => (ms != null ? `${Math.round(ms)}ms` : "—");
|
||||
const fmtMs = (ms) =>
|
||||
ms != null ? t("millisecondsShort", { value: Math.round(ms) }) : t("notAvailable");
|
||||
|
||||
if (!data && !error) {
|
||||
return (
|
||||
@@ -146,7 +149,7 @@ export default function HealthPage() {
|
||||
<div className="flex items-center gap-3">
|
||||
{lastRefresh && (
|
||||
<span className="text-xs text-text-muted">
|
||||
Updated {lastRefresh.toLocaleTimeString()}
|
||||
{t("updatedAt", { time: lastRefresh.toLocaleTimeString() })}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
@@ -155,7 +158,7 @@ export default function HealthPage() {
|
||||
fetchExtras();
|
||||
}}
|
||||
className="p-2 rounded-lg bg-surface hover:bg-surface/80 text-text-muted hover:text-text-main transition-colors"
|
||||
title="Refresh"
|
||||
title={tc("refresh")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[18px]">refresh</span>
|
||||
</button>
|
||||
@@ -204,7 +207,9 @@ export default function HealthPage() {
|
||||
<span className="text-sm text-text-muted">{t("version")}</span>
|
||||
</div>
|
||||
<p className="text-xl font-semibold text-text-main">v{system.version}</p>
|
||||
<p className="text-xs text-text-muted mt-1">Node {system.nodeVersion}</p>
|
||||
<p className="text-xs text-text-muted mt-1">
|
||||
{t("nodeVersion", { version: system.nodeVersion })}
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
@@ -228,11 +233,13 @@ export default function HealthPage() {
|
||||
<div className="flex items-center justify-center size-8 rounded-lg bg-amber-500/10 text-amber-500">
|
||||
<span className="material-symbols-outlined text-[18px]">dns</span>
|
||||
</div>
|
||||
<span className="text-sm text-text-muted">Providers</span>
|
||||
<span className="text-sm text-text-muted">{t("providers")}</span>
|
||||
</div>
|
||||
<p className="text-xl font-semibold text-text-main">{cbEntries.length}</p>
|
||||
<p className="text-xs text-text-muted mt-1">
|
||||
{cbEntries.filter(([, v]: [string, any]) => v.state === "CLOSED").length} healthy
|
||||
{t("healthyCount", {
|
||||
count: cbEntries.filter(([, v]: [string, any]) => v.state === "CLOSED").length,
|
||||
})}
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -248,15 +255,15 @@ export default function HealthPage() {
|
||||
{telemetry ? (
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-muted">p50</span>
|
||||
<span className="text-text-muted">{t("latencyP50")}</span>
|
||||
<span className="font-mono">{fmtMs(telemetry.p50)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-muted">p95</span>
|
||||
<span className="text-text-muted">{t("latencyP95")}</span>
|
||||
<span className="font-mono">{fmtMs(telemetry.p95)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-muted">p99</span>
|
||||
<span className="text-text-muted">{t("latencyP99")}</span>
|
||||
<span className="font-mono">{fmtMs(telemetry.p99)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-t border-border pt-2 mt-2">
|
||||
@@ -308,19 +315,23 @@ export default function HealthPage() {
|
||||
{signatureCache ? (
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{[
|
||||
{ label: "Defaults", value: signatureCache.defaultCount, color: "text-text-muted" },
|
||||
{
|
||||
label: "Tool",
|
||||
label: t("signatureDefaults"),
|
||||
value: signatureCache.defaultCount,
|
||||
color: "text-text-muted",
|
||||
},
|
||||
{
|
||||
label: t("signatureTool"),
|
||||
value: `${signatureCache.tool.entries}/${signatureCache.tool.patterns}`,
|
||||
color: "text-blue-400",
|
||||
},
|
||||
{
|
||||
label: "Family",
|
||||
label: t("signatureFamily"),
|
||||
value: `${signatureCache.family.entries}/${signatureCache.family.patterns}`,
|
||||
color: "text-purple-400",
|
||||
},
|
||||
{
|
||||
label: "Session",
|
||||
label: t("signatureSession"),
|
||||
value: `${signatureCache.session.entries}/${signatureCache.session.patterns}`,
|
||||
color: "text-cyan-400",
|
||||
},
|
||||
@@ -341,7 +352,7 @@ export default function HealthPage() {
|
||||
</div>
|
||||
|
||||
{/* Provider Health */}
|
||||
<Card className="p-5" role="region" aria-label="Provider health status">
|
||||
<Card className="p-5" role="region" aria-label={t("providerHealthStatusAria")}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold text-text-main flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-[20px] text-primary">
|
||||
@@ -359,7 +370,7 @@ export default function HealthPage() {
|
||||
? "bg-surface/50 text-text-muted cursor-wait"
|
||||
: "bg-red-500/10 text-red-400 hover:bg-red-500/20 hover:text-red-300 border border-red-500/20"
|
||||
}`}
|
||||
title="Reset all circuit breakers to healthy state"
|
||||
title={t("resetAllTitle")}
|
||||
>
|
||||
{resetting ? (
|
||||
<>
|
||||
@@ -406,7 +417,7 @@ export default function HealthPage() {
|
||||
{t("issuesLabel")}
|
||||
</p>
|
||||
{unhealthy.map(([provider, cb]: [string, any]) => {
|
||||
const style = CB_COLORS[cb.state] || CB_COLORS.OPEN;
|
||||
const style = CB_STYLES[cb.state] || CB_STYLES.OPEN;
|
||||
const providerInfo = AI_PROVIDERS[provider];
|
||||
const displayName = providerInfo?.name || provider;
|
||||
return (
|
||||
@@ -431,14 +442,17 @@ export default function HealthPage() {
|
||||
<span
|
||||
className={`text-xs font-semibold px-1.5 py-0.5 rounded ${style.bg} ${style.text}`}
|
||||
>
|
||||
{style.label}
|
||||
{t(style.labelKey)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-text-muted mt-0.5">
|
||||
{cb.failures} failure{cb.failures !== 1 ? "s" : ""}
|
||||
{cb.failures === 1
|
||||
? t("failures", { count: cb.failures })
|
||||
: t("failuresPlural", { count: cb.failures })}
|
||||
{cb.lastFailure && (
|
||||
<span className="ml-2">
|
||||
· Last: {new Date(cb.lastFailure).toLocaleTimeString()}
|
||||
· {t("lastFailure")}:{" "}
|
||||
{new Date(cb.lastFailure).toLocaleTimeString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -502,13 +516,13 @@ export default function HealthPage() {
|
||||
|
||||
if (providerId.startsWith("openai-compatible-")) {
|
||||
const customName = providerId.replace("openai-compatible-", "");
|
||||
displayName = `OpenAI Compatible`;
|
||||
displayName = tp("openaiCompatibleName");
|
||||
providerInfo = { color: "#10A37F", textIcon: "OC" };
|
||||
if (customName.length > 12) displayName += ` (${customName.slice(0, 8)}…)`;
|
||||
else if (customName) displayName += ` (${customName})`;
|
||||
} else if (providerId.startsWith("anthropic-compatible-")) {
|
||||
const customName = providerId.replace("anthropic-compatible-", "");
|
||||
displayName = `Anthropic Compatible`;
|
||||
displayName = tp("anthropicCompatibleName");
|
||||
providerInfo = { color: "#D97757", textIcon: "AC" };
|
||||
if (customName.length > 12) displayName += ` (${customName.slice(0, 8)}…)`;
|
||||
else if (customName) displayName += ` (${customName})`;
|
||||
@@ -544,7 +558,9 @@ export default function HealthPage() {
|
||||
{t("rateLimitStatus")}
|
||||
</h2>
|
||||
<span className="text-xs text-text-muted">
|
||||
{entries.length} active limiter{entries.length !== 1 ? "s" : ""}
|
||||
{entries.length === 1
|
||||
? t("activeLimiters", { count: entries.length })
|
||||
: t("activeLimitersPlural", { count: entries.length })}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
@@ -598,19 +614,19 @@ export default function HealthPage() {
|
||||
: "bg-green-500/10 text-green-400"
|
||||
}`}
|
||||
>
|
||||
{isQueued ? "Queued" : isActive ? "Active" : "OK"}
|
||||
{isQueued ? t("queued") : isActive ? tc("active") : t("ok")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-[11px] text-text-muted">
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="material-symbols-outlined text-[12px]">schedule</span>
|
||||
{status.queued || 0} queued
|
||||
{t("queuedCount", { count: status.queued || 0 })}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="material-symbols-outlined text-[12px]">
|
||||
play_arrow
|
||||
</span>
|
||||
{status.running || 0} running
|
||||
{t("runningCount", { count: status.running || 0 })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -643,7 +659,7 @@ export default function HealthPage() {
|
||||
</div>
|
||||
{lockout.until && (
|
||||
<span className="text-xs text-red-400">
|
||||
Until {new Date(lockout.until).toLocaleTimeString()}
|
||||
{t("until", { time: new Date(lockout.until).toLocaleTimeString() })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -47,11 +47,11 @@ export default function AuditLogTab() {
|
||||
setHasMore(data.length > PAGE_SIZE);
|
||||
setEntries(data.slice(0, PAGE_SIZE));
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Failed to fetch audit log");
|
||||
setError(err.message || t("failedFetchAuditLog"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [actionFilter, actorFilter, offset]);
|
||||
}, [actionFilter, actorFilter, offset, t]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchEntries();
|
||||
@@ -93,7 +93,7 @@ export default function AuditLogTab() {
|
||||
<button
|
||||
onClick={fetchEntries}
|
||||
disabled={loading}
|
||||
aria-label="Refresh audit log"
|
||||
aria-label={t("refreshAuditLogAria")}
|
||||
className="px-4 py-2 rounded-lg text-sm font-medium bg-[var(--color-surface)] border border-[var(--color-border)] text-[var(--color-text-main)] hover:bg-[var(--color-bg-alt)] transition-colors disabled:opacity-50"
|
||||
>
|
||||
{loading ? t("loading") : t("refresh")}
|
||||
@@ -104,7 +104,7 @@ export default function AuditLogTab() {
|
||||
<div
|
||||
className="flex flex-wrap gap-3 p-4 rounded-xl bg-[var(--color-surface)] border border-[var(--color-border)]"
|
||||
role="search"
|
||||
aria-label="Filter audit log entries"
|
||||
aria-label={t("filterEntriesAria")}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
@@ -112,7 +112,7 @@ export default function AuditLogTab() {
|
||||
value={actionFilter}
|
||||
onChange={(e) => setActionFilter(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
|
||||
aria-label="Filter by action type"
|
||||
aria-label={t("filterByActionTypeAria")}
|
||||
className="flex-1 min-w-[180px] px-3 py-2 rounded-lg text-sm bg-[var(--color-bg)] border border-[var(--color-border)] text-[var(--color-text-main)] placeholder:text-[var(--color-text-muted)] focus:outline-2 focus:outline-[var(--color-accent)]"
|
||||
/>
|
||||
<input
|
||||
@@ -121,7 +121,7 @@ export default function AuditLogTab() {
|
||||
value={actorFilter}
|
||||
onChange={(e) => setActorFilter(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
|
||||
aria-label="Filter by actor"
|
||||
aria-label={t("filterByActorAria")}
|
||||
className="flex-1 min-w-[180px] px-3 py-2 rounded-lg text-sm bg-[var(--color-bg)] border border-[var(--color-border)] text-[var(--color-text-main)] placeholder:text-[var(--color-text-muted)] focus:outline-2 focus:outline-[var(--color-accent)]"
|
||||
/>
|
||||
<button
|
||||
@@ -144,7 +144,7 @@ export default function AuditLogTab() {
|
||||
|
||||
{/* Table */}
|
||||
<div className="overflow-x-auto rounded-xl border border-[var(--color-border)]">
|
||||
<table className="w-full text-sm" role="table" aria-label="Audit log entries">
|
||||
<table className="w-full text-sm" role="table" aria-label={t("tableAria")}>
|
||||
<thead>
|
||||
<tr className="bg-[var(--color-bg-alt)] border-b border-[var(--color-border)]">
|
||||
<th className="text-left px-4 py-3 font-medium text-[var(--color-text-muted)]">
|
||||
@@ -162,7 +162,9 @@ export default function AuditLogTab() {
|
||||
<th className="text-left px-4 py-3 font-medium text-[var(--color-text-muted)]">
|
||||
{t("details")}
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 font-medium text-[var(--color-text-muted)]">IP</th>
|
||||
<th className="text-left px-4 py-3 font-medium text-[var(--color-text-muted)]">
|
||||
{t("ipAddress")}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -190,13 +192,13 @@ export default function AuditLogTab() {
|
||||
</td>
|
||||
<td className="px-4 py-3 text-[var(--color-text-main)]">{entry.actor}</td>
|
||||
<td className="px-4 py-3 text-[var(--color-text-muted)] max-w-[200px] truncate">
|
||||
{entry.target || "—"}
|
||||
{entry.target || t("notAvailable")}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-[var(--color-text-muted)] max-w-[300px] truncate font-mono text-xs">
|
||||
{entry.details ? JSON.stringify(entry.details) : "—"}
|
||||
{entry.details ? JSON.stringify(entry.details) : t("notAvailable")}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-[var(--color-text-muted)] font-mono text-xs whitespace-nowrap">
|
||||
{entry.ip_address || "—"}
|
||||
{entry.ip_address || t("notAvailable")}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
@@ -208,7 +210,7 @@ export default function AuditLogTab() {
|
||||
{/* Pagination */}
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-[var(--color-text-muted)]">
|
||||
Showing {entries.length} entries (offset {offset})
|
||||
{t("showing", { count: entries.length, offset })}
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
|
||||
@@ -237,10 +237,7 @@ export default function OnboardingWizard() {
|
||||
{/* Welcome */}
|
||||
{currentStep.id === "welcome" && (
|
||||
<div className="text-center space-y-4">
|
||||
<p className="text-text-muted">
|
||||
<strong className="text-text-main">OmniRoute</strong>{" "}
|
||||
{t("welcomeDesc").replace("OmniRoute is your local AI API proxy. ", "")}
|
||||
</p>
|
||||
<p className="text-text-muted">{t("welcomeDesc")}</p>
|
||||
<div className="grid grid-cols-3 gap-3 mt-6">
|
||||
{[
|
||||
{ icon: "swap_horiz", label: t("multiProvider") },
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { useState, useCallback, useEffect, useMemo } from "react";
|
||||
import { Card, Button, Select, Badge } from "@/shared/components";
|
||||
import { EXAMPLE_TEMPLATES, FORMAT_META, FORMAT_OPTIONS } from "../exampleTemplates";
|
||||
import { getExampleTemplates, FORMAT_META, FORMAT_OPTIONS } from "../exampleTemplates";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
const Editor = dynamic(() => import("@monaco-editor/react"), { ssr: false });
|
||||
@@ -20,6 +20,7 @@ export default function PlaygroundMode() {
|
||||
const [translating, setTranslating] = useState(false);
|
||||
const [detecting, setDetecting] = useState(false);
|
||||
const [activeTemplate, setActiveTemplate] = useState(null);
|
||||
const templates = useMemo(() => getExampleTemplates(t), [t]);
|
||||
|
||||
// Auto-detect format when input changes
|
||||
const detectFormatFromInput = useCallback(async (content) => {
|
||||
@@ -314,7 +315,7 @@ export default function PlaygroundMode() {
|
||||
<span className="text-xs text-text-muted">{t("exampleTemplatesHint")}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-2">
|
||||
{EXAMPLE_TEMPLATES.map((template) => (
|
||||
{templates.map((template) => (
|
||||
<button
|
||||
key={template.id}
|
||||
onClick={() => loadTemplate(template)}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { Card, Button, Select, Badge } from "@/shared/components";
|
||||
import { EXAMPLE_TEMPLATES, FORMAT_META, FORMAT_OPTIONS } from "../exampleTemplates";
|
||||
import { getExampleTemplates, FORMAT_META, FORMAT_OPTIONS } from "../exampleTemplates";
|
||||
import { useProviderOptions } from "../hooks/useProviderOptions";
|
||||
import { useAvailableModels } from "../hooks/useAvailableModels";
|
||||
|
||||
@@ -37,6 +37,7 @@ export default function TestBenchMode() {
|
||||
"system-prompt": t("scenarioSystemPrompt"),
|
||||
streaming: t("scenarioStreaming"),
|
||||
};
|
||||
const templates = useMemo(() => getExampleTemplates(t), [t]);
|
||||
const [sourceFormat, setSourceFormat] = useState("claude");
|
||||
const { provider, setProvider, providerOptions } = useProviderOptions("openai");
|
||||
const { model, setModel, availableModels, pickModelForFormat } = useAvailableModels();
|
||||
@@ -55,7 +56,7 @@ export default function TestBenchMode() {
|
||||
const start = Date.now();
|
||||
try {
|
||||
// Find template
|
||||
const template = EXAMPLE_TEMPLATES.find((t) => t.id === scenario.templateId);
|
||||
const template = templates.find((item) => item.id === scenario.templateId);
|
||||
const body = template?.formats[sourceFormat] || template?.formats.openai;
|
||||
|
||||
if (!body) {
|
||||
|
||||
@@ -4,293 +4,295 @@
|
||||
* quickly load a realistic payload and see how the translator converts it.
|
||||
*/
|
||||
|
||||
export const EXAMPLE_TEMPLATES = [
|
||||
{
|
||||
id: "simple-chat",
|
||||
name: "Simple Chat",
|
||||
icon: "chat",
|
||||
description: "Basic text message",
|
||||
formats: {
|
||||
openai: {
|
||||
model: "gpt-4o",
|
||||
messages: [
|
||||
{ role: "system", content: "You are a helpful assistant." },
|
||||
{ role: "user", content: "Hello! How are you today?" },
|
||||
],
|
||||
stream: true,
|
||||
},
|
||||
claude: {
|
||||
model: "claude-sonnet-4-20250514",
|
||||
system: "You are a helpful assistant.",
|
||||
max_tokens: 1024,
|
||||
messages: [{ role: "user", content: "Hello! How are you today?" }],
|
||||
stream: true,
|
||||
},
|
||||
gemini: {
|
||||
model: "gemini-2.5-flash",
|
||||
contents: [
|
||||
{
|
||||
role: "user",
|
||||
parts: [{ text: "Hello! How are you today?" }],
|
||||
type TranslatorMessage = (key: string) => string;
|
||||
|
||||
export function getExampleTemplates(t: TranslatorMessage) {
|
||||
const simpleChatSystem = t("templatePayloads.simpleChat.system");
|
||||
const simpleChatUser = t("templatePayloads.simpleChat.userGreeting");
|
||||
const toolUserWeather = t("templatePayloads.toolCalling.userWeather");
|
||||
const toolDescription = t("templatePayloads.toolCalling.toolDescription");
|
||||
const cityNameDescription = t("templatePayloads.toolCalling.cityNameDescription");
|
||||
const multiTurnSystem = t("templatePayloads.multiTurn.system");
|
||||
const multiTurnUserInitial = t("templatePayloads.multiTurn.userInitial");
|
||||
const multiTurnAssistantExample = t("templatePayloads.multiTurn.assistantExample");
|
||||
const multiTurnUserFollowUp = t("templatePayloads.multiTurn.userFollowUp");
|
||||
const thinkingQuestion = t("templatePayloads.thinking.question");
|
||||
const systemPromptInstruction = t("templatePayloads.systemPrompt.systemInstruction");
|
||||
const systemPromptQuestion = t("templatePayloads.systemPrompt.question");
|
||||
const streamingPrompt = t("templatePayloads.streaming.prompt");
|
||||
|
||||
return [
|
||||
{
|
||||
id: "simple-chat",
|
||||
name: t("templateNames.simple-chat"),
|
||||
icon: "chat",
|
||||
description: t("templateDescriptions.simple-chat"),
|
||||
formats: {
|
||||
openai: {
|
||||
model: "gpt-4o",
|
||||
messages: [
|
||||
{ role: "system", content: simpleChatSystem },
|
||||
{ role: "user", content: simpleChatUser },
|
||||
],
|
||||
stream: true,
|
||||
},
|
||||
claude: {
|
||||
model: "claude-sonnet-4-20250514",
|
||||
system: simpleChatSystem,
|
||||
max_tokens: 1024,
|
||||
messages: [{ role: "user", content: simpleChatUser }],
|
||||
stream: true,
|
||||
},
|
||||
gemini: {
|
||||
model: "gemini-2.5-flash",
|
||||
contents: [
|
||||
{
|
||||
role: "user",
|
||||
parts: [{ text: simpleChatUser }],
|
||||
},
|
||||
],
|
||||
systemInstruction: {
|
||||
parts: [{ text: simpleChatSystem }],
|
||||
},
|
||||
],
|
||||
systemInstruction: {
|
||||
parts: [{ text: "You are a helpful assistant." }],
|
||||
},
|
||||
"openai-responses": {
|
||||
model: "gpt-4o",
|
||||
input: simpleChatUser,
|
||||
instructions: simpleChatSystem,
|
||||
},
|
||||
},
|
||||
"openai-responses": {
|
||||
model: "gpt-4o",
|
||||
input: "Hello! How are you today?",
|
||||
instructions: "You are a helpful assistant.",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "tool-calling",
|
||||
name: "Tool Calling",
|
||||
icon: "build",
|
||||
description: "Function/tool invocation",
|
||||
formats: {
|
||||
openai: {
|
||||
model: "gpt-4o",
|
||||
messages: [{ role: "user", content: "What's the weather in São Paulo?" }],
|
||||
tools: [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_weather",
|
||||
description: "Get current weather for a location",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
location: { type: "string", description: "City name" },
|
||||
unit: { type: "string", enum: ["celsius", "fahrenheit"] },
|
||||
},
|
||||
required: ["location"],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
stream: true,
|
||||
},
|
||||
claude: {
|
||||
model: "claude-sonnet-4-20250514",
|
||||
max_tokens: 1024,
|
||||
messages: [{ role: "user", content: "What's the weather in São Paulo?" }],
|
||||
tools: [
|
||||
{
|
||||
name: "get_weather",
|
||||
description: "Get current weather for a location",
|
||||
input_schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
location: { type: "string", description: "City name" },
|
||||
unit: { type: "string", enum: ["celsius", "fahrenheit"] },
|
||||
},
|
||||
required: ["location"],
|
||||
},
|
||||
},
|
||||
],
|
||||
stream: true,
|
||||
},
|
||||
gemini: {
|
||||
model: "gemini-2.5-flash",
|
||||
contents: [
|
||||
{
|
||||
role: "user",
|
||||
parts: [{ text: "What's the weather in São Paulo?" }],
|
||||
},
|
||||
],
|
||||
tools: [
|
||||
{
|
||||
functionDeclarations: [
|
||||
{
|
||||
{
|
||||
id: "tool-calling",
|
||||
name: t("templateNames.tool-calling"),
|
||||
icon: "build",
|
||||
description: t("templateDescriptions.tool-calling"),
|
||||
formats: {
|
||||
openai: {
|
||||
model: "gpt-4o",
|
||||
messages: [{ role: "user", content: toolUserWeather }],
|
||||
tools: [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_weather",
|
||||
description: "Get current weather for a location",
|
||||
description: toolDescription,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
location: { type: "string", description: "City name" },
|
||||
location: { type: "string", description: cityNameDescription },
|
||||
unit: { type: "string", enum: ["celsius", "fahrenheit"] },
|
||||
},
|
||||
required: ["location"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "multi-turn",
|
||||
name: "Multi-turn",
|
||||
icon: "forum",
|
||||
description: "Conversation with history",
|
||||
formats: {
|
||||
openai: {
|
||||
model: "gpt-4o",
|
||||
messages: [
|
||||
{ role: "system", content: "You are a coding assistant." },
|
||||
{ role: "user", content: "Write a function to sort an array in Python." },
|
||||
{
|
||||
role: "assistant",
|
||||
content:
|
||||
"Here's a simple sort function:\n\n```python\ndef sort_array(arr):\n return sorted(arr)\n```",
|
||||
},
|
||||
{ role: "user", content: "Now make it sort in descending order." },
|
||||
],
|
||||
stream: true,
|
||||
},
|
||||
claude: {
|
||||
model: "claude-sonnet-4-20250514",
|
||||
system: "You are a coding assistant.",
|
||||
max_tokens: 1024,
|
||||
messages: [
|
||||
{ role: "user", content: "Write a function to sort an array in Python." },
|
||||
{
|
||||
role: "assistant",
|
||||
content:
|
||||
"Here's a simple sort function:\n\n```python\ndef sort_array(arr):\n return sorted(arr)\n```",
|
||||
},
|
||||
{ role: "user", content: "Now make it sort in descending order." },
|
||||
],
|
||||
stream: true,
|
||||
},
|
||||
gemini: {
|
||||
model: "gemini-2.5-flash",
|
||||
contents: [
|
||||
{ role: "user", parts: [{ text: "Write a function to sort an array in Python." }] },
|
||||
{
|
||||
role: "model",
|
||||
parts: [
|
||||
{
|
||||
text: "Here's a simple sort function:\n\n```python\ndef sort_array(arr):\n return sorted(arr)\n```",
|
||||
},
|
||||
],
|
||||
},
|
||||
{ role: "user", parts: [{ text: "Now make it sort in descending order." }] },
|
||||
],
|
||||
systemInstruction: {
|
||||
parts: [{ text: "You are a coding assistant." }],
|
||||
},
|
||||
],
|
||||
stream: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "thinking",
|
||||
name: "Thinking",
|
||||
icon: "psychology",
|
||||
description: "Extended thinking / reasoning",
|
||||
formats: {
|
||||
openai: {
|
||||
model: "o3-mini",
|
||||
messages: [{ role: "user", content: "What is the sum of the first 100 prime numbers?" }],
|
||||
stream: true,
|
||||
},
|
||||
claude: {
|
||||
model: "claude-sonnet-4-20250514",
|
||||
max_tokens: 16000,
|
||||
thinking: {
|
||||
type: "enabled",
|
||||
budget_tokens: 10000,
|
||||
},
|
||||
messages: [{ role: "user", content: "What is the sum of the first 100 prime numbers?" }],
|
||||
stream: true,
|
||||
},
|
||||
gemini: {
|
||||
model: "gemini-2.5-flash-thinking",
|
||||
contents: [
|
||||
{ role: "user", parts: [{ text: "What is the sum of the first 100 prime numbers?" }] },
|
||||
],
|
||||
generationConfig: {
|
||||
thinkingConfig: {
|
||||
thinkingBudget: 10000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "system-prompt",
|
||||
name: "System Prompt",
|
||||
icon: "settings",
|
||||
description: "Complex system instructions",
|
||||
formats: {
|
||||
openai: {
|
||||
model: "gpt-4o",
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content:
|
||||
"You are a senior software engineer specializing in distributed systems. Answer questions concisely using industry best practices. Always provide code examples when relevant. Format your responses using markdown.",
|
||||
},
|
||||
{ role: "user", content: "How do I implement a circuit breaker pattern?" },
|
||||
],
|
||||
temperature: 0.7,
|
||||
stream: true,
|
||||
},
|
||||
claude: {
|
||||
model: "claude-sonnet-4-20250514",
|
||||
system:
|
||||
"You are a senior software engineer specializing in distributed systems. Answer questions concisely using industry best practices. Always provide code examples when relevant. Format your responses using markdown.",
|
||||
max_tokens: 2048,
|
||||
messages: [{ role: "user", content: "How do I implement a circuit breaker pattern?" }],
|
||||
temperature: 0.7,
|
||||
stream: true,
|
||||
},
|
||||
gemini: {
|
||||
model: "gemini-2.5-flash",
|
||||
contents: [
|
||||
{ role: "user", parts: [{ text: "How do I implement a circuit breaker pattern?" }] },
|
||||
],
|
||||
systemInstruction: {
|
||||
parts: [
|
||||
claude: {
|
||||
model: "claude-sonnet-4-20250514",
|
||||
max_tokens: 1024,
|
||||
messages: [{ role: "user", content: toolUserWeather }],
|
||||
tools: [
|
||||
{
|
||||
text: "You are a senior software engineer specializing in distributed systems. Answer questions concisely using industry best practices. Always provide code examples when relevant. Format your responses using markdown.",
|
||||
name: "get_weather",
|
||||
description: toolDescription,
|
||||
input_schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
location: { type: "string", description: cityNameDescription },
|
||||
unit: { type: "string", enum: ["celsius", "fahrenheit"] },
|
||||
},
|
||||
required: ["location"],
|
||||
},
|
||||
},
|
||||
],
|
||||
stream: true,
|
||||
},
|
||||
gemini: {
|
||||
model: "gemini-2.5-flash",
|
||||
contents: [
|
||||
{
|
||||
role: "user",
|
||||
parts: [{ text: toolUserWeather }],
|
||||
},
|
||||
],
|
||||
tools: [
|
||||
{
|
||||
functionDeclarations: [
|
||||
{
|
||||
name: "get_weather",
|
||||
description: toolDescription,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
location: { type: "string", description: cityNameDescription },
|
||||
unit: { type: "string", enum: ["celsius", "fahrenheit"] },
|
||||
},
|
||||
required: ["location"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
generationConfig: {
|
||||
temperature: 0.7,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "multi-turn",
|
||||
name: t("templateNames.multi-turn"),
|
||||
icon: "forum",
|
||||
description: t("templateDescriptions.multi-turn"),
|
||||
formats: {
|
||||
openai: {
|
||||
model: "gpt-4o",
|
||||
messages: [
|
||||
{ role: "system", content: multiTurnSystem },
|
||||
{ role: "user", content: multiTurnUserInitial },
|
||||
{
|
||||
role: "assistant",
|
||||
content: multiTurnAssistantExample,
|
||||
},
|
||||
{ role: "user", content: multiTurnUserFollowUp },
|
||||
],
|
||||
stream: true,
|
||||
},
|
||||
claude: {
|
||||
model: "claude-sonnet-4-20250514",
|
||||
system: multiTurnSystem,
|
||||
max_tokens: 1024,
|
||||
messages: [
|
||||
{ role: "user", content: multiTurnUserInitial },
|
||||
{
|
||||
role: "assistant",
|
||||
content: multiTurnAssistantExample,
|
||||
},
|
||||
{ role: "user", content: multiTurnUserFollowUp },
|
||||
],
|
||||
stream: true,
|
||||
},
|
||||
gemini: {
|
||||
model: "gemini-2.5-flash",
|
||||
contents: [
|
||||
{ role: "user", parts: [{ text: multiTurnUserInitial }] },
|
||||
{
|
||||
role: "model",
|
||||
parts: [
|
||||
{
|
||||
text: multiTurnAssistantExample,
|
||||
},
|
||||
],
|
||||
},
|
||||
{ role: "user", parts: [{ text: multiTurnUserFollowUp }] },
|
||||
],
|
||||
systemInstruction: {
|
||||
parts: [{ text: multiTurnSystem }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "streaming",
|
||||
name: "Streaming",
|
||||
icon: "stream",
|
||||
description: "SSE streaming request",
|
||||
formats: {
|
||||
openai: {
|
||||
model: "gpt-4o",
|
||||
messages: [
|
||||
{ role: "user", content: "Tell me a short story about a robot learning to paint." },
|
||||
],
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
},
|
||||
claude: {
|
||||
model: "claude-sonnet-4-20250514",
|
||||
max_tokens: 1024,
|
||||
messages: [
|
||||
{ role: "user", content: "Tell me a short story about a robot learning to paint." },
|
||||
],
|
||||
stream: true,
|
||||
},
|
||||
gemini: {
|
||||
model: "gemini-2.5-flash",
|
||||
contents: [
|
||||
{
|
||||
role: "user",
|
||||
parts: [{ text: "Tell me a short story about a robot learning to paint." }],
|
||||
{
|
||||
id: "thinking",
|
||||
name: t("templateNames.thinking"),
|
||||
icon: "psychology",
|
||||
description: t("templateDescriptions.thinking"),
|
||||
formats: {
|
||||
openai: {
|
||||
model: "o3-mini",
|
||||
messages: [{ role: "user", content: thinkingQuestion }],
|
||||
stream: true,
|
||||
},
|
||||
claude: {
|
||||
model: "claude-sonnet-4-20250514",
|
||||
max_tokens: 16000,
|
||||
thinking: {
|
||||
type: "enabled",
|
||||
budget_tokens: 10000,
|
||||
},
|
||||
],
|
||||
messages: [{ role: "user", content: thinkingQuestion }],
|
||||
stream: true,
|
||||
},
|
||||
gemini: {
|
||||
model: "gemini-2.5-flash-thinking",
|
||||
contents: [{ role: "user", parts: [{ text: thinkingQuestion }] }],
|
||||
generationConfig: {
|
||||
thinkingConfig: {
|
||||
thinkingBudget: 10000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
{
|
||||
id: "system-prompt",
|
||||
name: t("templateNames.system-prompt"),
|
||||
icon: "settings",
|
||||
description: t("templateDescriptions.system-prompt"),
|
||||
formats: {
|
||||
openai: {
|
||||
model: "gpt-4o",
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: systemPromptInstruction,
|
||||
},
|
||||
{ role: "user", content: systemPromptQuestion },
|
||||
],
|
||||
temperature: 0.7,
|
||||
stream: true,
|
||||
},
|
||||
claude: {
|
||||
model: "claude-sonnet-4-20250514",
|
||||
system: systemPromptInstruction,
|
||||
max_tokens: 2048,
|
||||
messages: [{ role: "user", content: systemPromptQuestion }],
|
||||
temperature: 0.7,
|
||||
stream: true,
|
||||
},
|
||||
gemini: {
|
||||
model: "gemini-2.5-flash",
|
||||
contents: [{ role: "user", parts: [{ text: systemPromptQuestion }] }],
|
||||
systemInstruction: {
|
||||
parts: [{ text: systemPromptInstruction }],
|
||||
},
|
||||
generationConfig: {
|
||||
temperature: 0.7,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "streaming",
|
||||
name: t("templateNames.streaming"),
|
||||
icon: "stream",
|
||||
description: t("templateDescriptions.streaming"),
|
||||
formats: {
|
||||
openai: {
|
||||
model: "gpt-4o",
|
||||
messages: [{ role: "user", content: streamingPrompt }],
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
},
|
||||
claude: {
|
||||
model: "claude-sonnet-4-20250514",
|
||||
max_tokens: 1024,
|
||||
messages: [{ role: "user", content: streamingPrompt }],
|
||||
stream: true,
|
||||
},
|
||||
gemini: {
|
||||
model: "gemini-2.5-flash",
|
||||
contents: [
|
||||
{
|
||||
role: "user",
|
||||
parts: [{ text: streamingPrompt }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format metadata for display: colors, labels, icons
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import {
|
||||
AI_PROVIDERS,
|
||||
OPENAI_COMPATIBLE_PREFIX,
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
* @returns {{ provider: string, setProvider: Function, providerOptions: Array<{value: string, label: string}>, loading: boolean }}
|
||||
*/
|
||||
export function useProviderOptions(initialProvider = "openai") {
|
||||
const t = useTranslations("translator");
|
||||
const [provider, setProvider] = useState(initialProvider);
|
||||
const [providerOptions, setProviderOptions] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -38,9 +40,9 @@ export function useProviderOptions(initialProvider = "openai") {
|
||||
const node: any = nodeMap.get(pid);
|
||||
let label = info?.name || node?.name || pid;
|
||||
if (!info && (pid as string).startsWith(OPENAI_COMPATIBLE_PREFIX))
|
||||
label = node?.name || "OpenAI Compatible";
|
||||
label = node?.name || t("openaiCompatibleLabel");
|
||||
if (!info && (pid as string).startsWith(ANTHROPIC_COMPATIBLE_PREFIX))
|
||||
label = node?.name || "Anthropic Compatible";
|
||||
label = node?.name || t("anthropicCompatibleLabel");
|
||||
return { value: pid, label };
|
||||
})
|
||||
.sort((a, b) => a.label.localeCompare(b.label));
|
||||
@@ -48,11 +50,16 @@ export function useProviderOptions(initialProvider = "openai") {
|
||||
const nextOptions =
|
||||
options.length > 0
|
||||
? options
|
||||
: Object.entries(AI_PROVIDERS).map(([id, info]: [string, any]) => ({ value: id, label: info.name }));
|
||||
: Object.entries(AI_PROVIDERS).map(([id, info]: [string, any]) => ({
|
||||
value: id,
|
||||
label: info.name,
|
||||
}));
|
||||
setProviderOptions(nextOptions);
|
||||
if (nextOptions.length > 0) {
|
||||
setProvider((current: string): string =>
|
||||
nextOptions.some((opt: any) => opt.value === current) ? current : (nextOptions[0] as any).value as string
|
||||
nextOptions.some((opt: any) => opt.value === current)
|
||||
? current
|
||||
: ((nextOptions[0] as any).value as string)
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
@@ -73,7 +80,7 @@ export function useProviderOptions(initialProvider = "openai") {
|
||||
}
|
||||
};
|
||||
fetchProviders();
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
return { provider, setProvider, providerOptions, loading };
|
||||
}
|
||||
|
||||
@@ -36,15 +36,15 @@ export default function BudgetTelemetryCards() {
|
||||
{telemetry ? (
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-muted">p50</span>
|
||||
<span className="text-text-muted">{t("latencyP50")}</span>
|
||||
<span className="font-mono">{fmt(telemetry.p50)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-muted">p95</span>
|
||||
<span className="text-text-muted">{t("latencyP95")}</span>
|
||||
<span className="font-mono">{fmt(telemetry.p95)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-muted">p99</span>
|
||||
<span className="text-text-muted">{t("latencyP99")}</span>
|
||||
<span className="font-mono">{fmt(telemetry.p99)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-t border-border pt-2 mt-2">
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useSearchParams } from "next/navigation";
|
||||
function CallbackContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const [status, setStatus] = useState("processing");
|
||||
const t = useTranslations("auth");
|
||||
|
||||
useEffect(() => {
|
||||
const code = searchParams.get("code");
|
||||
@@ -110,9 +111,7 @@ function CallbackContent() {
|
||||
</div>
|
||||
<h1 className="text-xl font-semibold mb-2">{t("authSuccess")}</h1>
|
||||
<p className="text-text-muted">
|
||||
{status === "success"
|
||||
? "This window will close automatically..."
|
||||
: "You can close this tab now."}
|
||||
{status === "success" ? t("windowWillClose") : t("closeTabNow")}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
@@ -123,9 +122,7 @@ function CallbackContent() {
|
||||
<span className="material-symbols-outlined text-3xl text-yellow-600">info</span>
|
||||
</div>
|
||||
<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>
|
||||
<p className="text-text-muted mb-4">{t("copyUrlManual")}</p>
|
||||
<div className="bg-surface border border-border rounded-lg p-3 text-left">
|
||||
<code className="text-xs break-all">
|
||||
{typeof window !== "undefined" ? window.location.href : ""}
|
||||
@@ -154,7 +151,7 @@ export default function CallbackPage() {
|
||||
progress_activity
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-text-muted">Loading...</p>
|
||||
<p className="text-text-muted">{t("loading")}</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -29,8 +29,7 @@ export default function ForbiddenPage() {
|
||||
</div>
|
||||
<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.
|
||||
{t("accessDeniedDescription")}
|
||||
</p>
|
||||
<Link
|
||||
href="/dashboard"
|
||||
@@ -39,7 +38,7 @@ export default function ForbiddenPage() {
|
||||
background: "linear-gradient(135deg, #6366f1, #8b5cf6)",
|
||||
}}
|
||||
>
|
||||
Go to Dashboard
|
||||
{t("goToDashboard")}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -69,10 +69,10 @@ export default function LoginPage() {
|
||||
router.refresh();
|
||||
} else {
|
||||
const data = await res.json();
|
||||
setError(data.error || "Invalid password");
|
||||
setError(data.error || t("invalidPassword"));
|
||||
}
|
||||
} catch (err) {
|
||||
setError("An error occurred. Please try again.");
|
||||
setError(t("errorOccurredRetry"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -86,7 +86,7 @@ export default function LoginPage() {
|
||||
<div className="w-10 h-10 border-2 border-primary/20 rounded-full"></div>
|
||||
<div className="absolute inset-0 w-10 h-10 border-2 border-primary border-t-transparent rounded-full animate-spin"></div>
|
||||
</div>
|
||||
<span className="text-sm text-text-muted">Loading...</span>
|
||||
<span className="text-sm text-text-muted">{t("loading")}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -105,23 +105,18 @@ export default function LoginPage() {
|
||||
</span>
|
||||
</div>
|
||||
<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>
|
||||
<p className="text-text-muted mt-2">{t("configureInstance")}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-surface border border-border rounded-2xl p-8 shadow-soft">
|
||||
<div className="text-center">
|
||||
<p className="text-text-muted leading-relaxed mb-6">
|
||||
Run the onboarding wizard to set up your password and connect your first AI
|
||||
provider.
|
||||
</p>
|
||||
<p className="text-text-muted leading-relaxed mb-6">{t("runOnboardingWizard")}</p>
|
||||
<Button
|
||||
variant="primary"
|
||||
className="w-full h-11 text-sm font-medium"
|
||||
onClick={() => router.push("/dashboard/onboarding")}
|
||||
>
|
||||
Start Onboarding
|
||||
{t("startOnboarding")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -147,29 +142,26 @@ export default function LoginPage() {
|
||||
</span>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-text-main tracking-tight">
|
||||
Secure Your Instance
|
||||
{t("secureYourInstance")}
|
||||
</h1>
|
||||
<p className="text-text-muted mt-2">{t("passwordNotEnabled")}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-surface border border-border rounded-2xl p-8 shadow-soft">
|
||||
<div className="text-center">
|
||||
<p className="text-text-muted leading-relaxed mb-6">
|
||||
Set a password to protect your dashboard and secure your API endpoints from
|
||||
unauthorized access.
|
||||
</p>
|
||||
<p className="text-text-muted leading-relaxed mb-6">{t("setPasswordDescription")}</p>
|
||||
<Button
|
||||
variant="primary"
|
||||
className="w-full h-11 text-sm font-medium"
|
||||
onClick={() => router.push("/dashboard/settings?tab=security")}
|
||||
>
|
||||
Configure Password
|
||||
{t("configurePassword")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-center text-xs text-text-muted/60 mt-8">
|
||||
OmniRoute — Unified AI API Proxy
|
||||
OmniRoute — {t("unifiedAiApiProxy")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -198,7 +190,7 @@ export default function LoginPage() {
|
||||
<label className="text-sm font-medium text-text-main">{t("password")}</label>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Enter your password"
|
||||
placeholder={t("enterPassword")}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
@@ -219,7 +211,7 @@ export default function LoginPage() {
|
||||
className="w-full h-11 text-sm font-medium"
|
||||
loading={loading}
|
||||
>
|
||||
Continue
|
||||
{t("continue")}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
@@ -228,7 +220,7 @@ export default function LoginPage() {
|
||||
href="/forgot-password"
|
||||
className="text-sm text-text-muted hover:text-primary transition-colors"
|
||||
>
|
||||
Forgot your password?
|
||||
{t("forgotPassword")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -240,26 +232,27 @@ export default function LoginPage() {
|
||||
>
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-text-main mb-3">Unified AI API Proxy</h2>
|
||||
<p className="text-text-muted leading-relaxed">
|
||||
Route requests to multiple AI providers through a single endpoint. Load balancing,
|
||||
failover, and usage tracking built in.
|
||||
</p>
|
||||
<h2 className="text-2xl font-bold text-text-main mb-3">{t("unifiedAiApiProxy")}</h2>
|
||||
<p className="text-text-muted leading-relaxed">{t("unifiedAiApiProxyDesc")}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{[
|
||||
{
|
||||
icon: "swap_horiz",
|
||||
title: "Multi-Provider",
|
||||
desc: "OpenAI, Anthropic, Google, and more",
|
||||
title: t("featureMultiProviderTitle"),
|
||||
desc: t("featureMultiProviderDesc"),
|
||||
},
|
||||
{
|
||||
icon: "speed",
|
||||
title: "Load Balancing",
|
||||
desc: "Distribute requests intelligently",
|
||||
title: t("featureLoadBalancingTitle"),
|
||||
desc: t("featureLoadBalancingDesc"),
|
||||
},
|
||||
{
|
||||
icon: "analytics",
|
||||
title: t("featureUsageTrackingTitle"),
|
||||
desc: t("featureUsageTrackingDesc"),
|
||||
},
|
||||
{ icon: "analytics", title: "Usage Tracking", desc: "Monitor costs and tokens" },
|
||||
].map((item) => (
|
||||
<div
|
||||
key={item.icon}
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function PrivacyPage() {
|
||||
|
||||
<h1 className="text-3xl font-bold mb-2">{t("privacyPolicy")}</h1>
|
||||
<p className="text-sm text-text-muted mb-10">
|
||||
{t("lastUpdated", { date: "February 13, 2026" })}
|
||||
{t("lastUpdated", { date: t("policyLastUpdatedDate") })}
|
||||
</p>
|
||||
|
||||
<div className="space-y-8 text-text-muted leading-relaxed">
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function TermsPage() {
|
||||
|
||||
<h1 className="text-3xl font-bold mb-2">{t("termsOfService")}</h1>
|
||||
<p className="text-sm text-text-muted mb-10">
|
||||
{t("lastUpdated", { date: "February 13, 2026" })}
|
||||
{t("lastUpdated", { date: t("policyLastUpdatedDate") })}
|
||||
</p>
|
||||
|
||||
<div className="space-y-8 text-text-muted leading-relaxed">
|
||||
|
||||
@@ -117,13 +117,13 @@
|
||||
"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.",
|
||||
"step1Desc": "Go to <endpoint>Endpoint</endpoint> -> Registered Keys. Generate one key per environment.",
|
||||
"step2Title": "2. Connect providers",
|
||||
"step2Desc": "Add accounts in {providers}. Supports OAuth, API Key, and free tiers.",
|
||||
"step2Desc": "Add accounts in <providers>Providers</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}.",
|
||||
"step4Desc": "Track tokens, cost and errors in <logs>Request Logs</logs> and <analytics>Analytics</analytics>.",
|
||||
"providersOverview": "Providers Overview",
|
||||
"configuredOf": "{configured} configured of {total} available providers",
|
||||
"noModelsAvailable": "No models available for this provider.",
|
||||
@@ -137,7 +137,13 @@
|
||||
"documentation": "Documentation",
|
||||
"healthMonitor": "Health Monitor",
|
||||
"reportIssue": "Report issue",
|
||||
"activeError": "{active} active · {errors} error"
|
||||
"activeError": "{active} active · {errors} error",
|
||||
"oauthLabel": "OAuth",
|
||||
"apiKeyLabel": "API Key",
|
||||
"requestsShort": "{count} reqs",
|
||||
"providerModelsTitle": "{provider} - Models",
|
||||
"copiedModel": "Copied: {model}",
|
||||
"aliasLabel": "alias"
|
||||
},
|
||||
"analytics": {
|
||||
"title": "Analytics",
|
||||
@@ -243,6 +249,13 @@
|
||||
"noEntries": "No audit entries found",
|
||||
"filterByAction": "Filter by action...",
|
||||
"filterByActor": "Filter by actor...",
|
||||
"filterEntriesAria": "Filter audit log entries",
|
||||
"filterByActionTypeAria": "Filter by action type",
|
||||
"filterByActorAria": "Filter by actor",
|
||||
"refreshAuditLogAria": "Refresh audit log",
|
||||
"tableAria": "Audit log entries",
|
||||
"failedFetchAuditLog": "Failed to fetch audit log",
|
||||
"notAvailable": "—",
|
||||
"description": "Administrative actions and security events",
|
||||
"showing": "Showing {count} entries (offset {offset})",
|
||||
"previous": "Previous"
|
||||
@@ -258,7 +271,199 @@
|
||||
"instructions": "Instructions",
|
||||
"modelMapping": "Model Mapping",
|
||||
"baseUrl": "Base URL",
|
||||
"apiKey": "API Key"
|
||||
"apiKey": "API Key",
|
||||
"configured": "Configured",
|
||||
"notConfigured": "Not configured",
|
||||
"notInstalled": "Not installed",
|
||||
"custom": "Custom",
|
||||
"unknown": "Unknown",
|
||||
"lastSavedAt": "Last saved: {date}",
|
||||
"never": "Never",
|
||||
"justNow": "just now",
|
||||
"minutesAgoShort": "{count}m ago",
|
||||
"hoursAgoShort": "{count}h ago",
|
||||
"daysAgoShort": "{count}d ago",
|
||||
"monthsAgoShort": "{count}mo ago",
|
||||
"yearsAgoShort": "{count}y ago",
|
||||
"runtimeCheckFailed": "Runtime check failed",
|
||||
"yourApiKeyPlaceholder": "your-api-key",
|
||||
"modelPlaceholder": "provider/model-id",
|
||||
"configurationSaved": "Configuration saved successfully.",
|
||||
"failedToSave": "Failed to save configuration.",
|
||||
"noApiKeysCreateOne": "No API keys - Create one in Keys page",
|
||||
"defaultOmnirouteKey": "sk_omniroute (default)",
|
||||
"selectModel": "Select Model",
|
||||
"selectModelForAlias": "Select model for {alias}",
|
||||
"selectModelForTool": "Select Model for {tool}",
|
||||
"select": "Select",
|
||||
"clear": "Clear",
|
||||
"comingSoon": "Coming soon",
|
||||
"checkingRuntime": "Checking runtime status...",
|
||||
"guideOnlyIntegration": "Guide-only integration (no local runtime required)",
|
||||
"cliRuntimeDetected": "CLI runtime detected and ready",
|
||||
"cliFoundNotRunnable": "CLI found but not runnable{reason}",
|
||||
"cliRuntimeNotDetected": "CLI runtime not detected",
|
||||
"binary": "Binary",
|
||||
"configPath": "Config path",
|
||||
"configPathShort": "Config",
|
||||
"failedCheckRuntimeStatus": "Failed to check runtime status.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"copyConfig": "Copy Config",
|
||||
"saveConfig": "Save Config",
|
||||
"selectionSaved": "Selection saved",
|
||||
"guide": "Guide",
|
||||
"detected": "Detected",
|
||||
"notReady": "Not ready",
|
||||
"active": "Active",
|
||||
"inactive": "Inactive",
|
||||
"startMitm": "Start MITM",
|
||||
"stopMitm": "Stop MITM",
|
||||
"mitmStarted": "MITM started successfully!",
|
||||
"mitmStopped": "MITM stopped successfully!",
|
||||
"failedStart": "Failed to start MITM",
|
||||
"failedStop": "Failed to stop MITM",
|
||||
"saveMappings": "Save Mappings",
|
||||
"mappingsSaved": "Mappings saved!",
|
||||
"failedSaveMappings": "Failed to save mappings",
|
||||
"howItWorks": "How it works:",
|
||||
"antigravityHowWorksDesc": "Antigravity sends requests to Google's endpoint. MITM intercepts and redirects them to OmniRoute.",
|
||||
"antigravityStep1": "1. Start MITM to route requests through OmniRoute.",
|
||||
"antigravityStep2Prefix": "2. Add",
|
||||
"antigravityStep2Suffix": "to your hosts file as 127.0.0.1.",
|
||||
"antigravityStep3": "3. Open Antigravity and requests will be proxied.",
|
||||
"sudoPasswordRequiredTitle": "Sudo Password Required",
|
||||
"sudoPasswordHint": "Administrator password is required to modify hosts file and system proxy settings.",
|
||||
"enterSudoPassword": "Enter sudo password",
|
||||
"sudoPasswordRequiredError": "Sudo password is required.",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"settingsApplied": "Settings applied successfully!",
|
||||
"failedApplySettings": "Failed to apply settings",
|
||||
"settingsReset": "Settings reset successfully!",
|
||||
"failedResetSettings": "Failed to reset settings",
|
||||
"backupRestored": "Backup restored!",
|
||||
"failedRestore": "Failed to restore",
|
||||
"checkingCli": "Checking {tool} CLI...",
|
||||
"cliNotRunnable": "{tool} CLI installed but not runnable",
|
||||
"cliNotInstalled": "{tool} CLI not installed",
|
||||
"cliNotDetected": "{tool} CLI not detected",
|
||||
"cliDetectedReady": "{tool} CLI detected and ready",
|
||||
"cliFoundFailedHealthcheck": "{tool} CLI was found but failed runtime healthcheck{reason}.",
|
||||
"installCliPrompt": "Please install {tool} CLI to use this feature.",
|
||||
"installCodexPrompt": "Please install Codex CLI to use auto-apply feature.",
|
||||
"hide": "Hide",
|
||||
"howToInstall": "How to Install",
|
||||
"installationGuide": "Installation Guide",
|
||||
"platforms": "macOS / Linux / Windows:",
|
||||
"afterInstallationRun": "After installation, run",
|
||||
"toVerify": "to verify.",
|
||||
"current": "Current",
|
||||
"baseUrlPlaceholder": "https://.../v1",
|
||||
"resetToDefault": "Reset to default",
|
||||
"providerModelPlaceholder": "provider/model-id",
|
||||
"apply": "Apply",
|
||||
"reset": "Reset",
|
||||
"manualConfig": "Manual Config",
|
||||
"backups": "Backups",
|
||||
"configBackups": "Config Backups",
|
||||
"noBackupsYet": "No backups yet. Backups are created automatically before each Apply or Reset.",
|
||||
"restore": "Restore",
|
||||
"backupRestoredReloading": "Backup restored! Reloading status...",
|
||||
"failedRestoreBackup": "Failed to restore backup",
|
||||
"applied": "Applied!",
|
||||
"failed": "Failed",
|
||||
"resetDone": "Reset!",
|
||||
"omnirouteConfiguredOpenAiCompatible": "OmniRoute is configured as OpenAI-compatible provider",
|
||||
"provider": "Provider",
|
||||
"model": "Model",
|
||||
"providers": "Providers",
|
||||
"auth": "Auth",
|
||||
"noApiKeysAvailable": "No API keys available",
|
||||
"usingDefaultOmniroute": "Using default: sk_omniroute",
|
||||
"updateConfig": "Update Config",
|
||||
"applyConfig": "Apply Config",
|
||||
"noBackupsAvailable": "No backups available.",
|
||||
"profileSaved": "Profile \"{name}\" saved!",
|
||||
"failedSaveProfile": "Failed to save profile",
|
||||
"profileActivated": "Profile activated!",
|
||||
"failedActivateProfile": "Failed to activate profile",
|
||||
"profiles": "Profiles",
|
||||
"savedProfiles": "Saved Profiles",
|
||||
"noProfilesYet": "No profiles saved yet. Save current config as a profile below.",
|
||||
"activate": "Activate",
|
||||
"deleteProfile": "Delete profile",
|
||||
"profileNamePlaceholder": "Profile name (e.g. Personal Account)",
|
||||
"saveCurrent": "Save Current",
|
||||
"codexAuthNotePrefix": "Codex uses",
|
||||
"codexAuthNoteMiddle": "with",
|
||||
"codexAuthNoteSuffix": "Click \"Apply\" to auto-configure.",
|
||||
"claudeManualConfiguration": "Claude CLI - Manual Configuration",
|
||||
"codexManualConfiguration": "Codex CLI - Manual Configuration",
|
||||
"droidManualConfiguration": "Factory Droid - Manual Configuration",
|
||||
"openClawManualConfiguration": "Open Claw - Manual Configuration",
|
||||
"clineManualConfiguration": "Cline Manual Configuration",
|
||||
"kiloManualConfiguration": "Kilo Code Manual Configuration",
|
||||
"toolDescriptions": {
|
||||
"antigravity": "Google Antigravity IDE with MITM",
|
||||
"claude": "Anthropic Claude Code CLI",
|
||||
"codex": "OpenAI Codex CLI",
|
||||
"droid": "Factory Droid AI Assistant",
|
||||
"openclaw": "Open Claw AI Assistant",
|
||||
"cline": "Cline AI Coding Assistant CLI",
|
||||
"kilo": "Kilo Code AI Assistant CLI",
|
||||
"cursor": "Cursor AI Code Editor",
|
||||
"continue": "Continue AI Assistant"
|
||||
},
|
||||
"guides": {
|
||||
"cursor": {
|
||||
"notes": {
|
||||
"0": "Requires Cursor Pro account to use this feature.",
|
||||
"1": "Cursor routes requests through its own server, so local endpoint is not supported. Please enable Cloud Endpoint in Settings."
|
||||
},
|
||||
"steps": {
|
||||
"1": {
|
||||
"title": "Open Settings",
|
||||
"desc": "Go to Settings -> Models"
|
||||
},
|
||||
"2": {
|
||||
"title": "Enable OpenAI API",
|
||||
"desc": "Enable \"OpenAI API key\" option"
|
||||
},
|
||||
"3": {
|
||||
"title": "Base URL"
|
||||
},
|
||||
"4": {
|
||||
"title": "API Key"
|
||||
},
|
||||
"5": {
|
||||
"title": "Add Custom Model",
|
||||
"desc": "Click \"View All Model\" -> \"Add Custom Model\""
|
||||
},
|
||||
"6": {
|
||||
"title": "Select Model"
|
||||
}
|
||||
}
|
||||
},
|
||||
"continue": {
|
||||
"steps": {
|
||||
"1": {
|
||||
"title": "Open Config",
|
||||
"desc": "Open Continue configuration file"
|
||||
},
|
||||
"2": {
|
||||
"title": "API Key"
|
||||
},
|
||||
"3": {
|
||||
"title": "Select Model"
|
||||
},
|
||||
"4": {
|
||||
"title": "Add Model Config",
|
||||
"desc": "Add the following configuration to your models array:"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"combos": {
|
||||
"title": "Combos",
|
||||
@@ -390,10 +595,23 @@
|
||||
"disabling": "Disabling...",
|
||||
"cloudConnectedVerified": "Cloud Proxy connected and verified!",
|
||||
"connectedVerificationPending": "Connected \u2014 verification pending",
|
||||
"connectedVerificationPendingWithError": "Connected \u2014 verification pending: {error}",
|
||||
"cloudDisabledSuccess": "Cloud disabled successfully",
|
||||
"syncedSuccess": "Synced successfully",
|
||||
"failedDisable": "Failed to disable cloud",
|
||||
"failedEnable": "Failed to enable cloud"
|
||||
"failedEnable": "Failed to enable cloud",
|
||||
"cloudRequestTimeout": "Cloud request timeout",
|
||||
"cloudRequestFailed": "Cloud request failed",
|
||||
"cloudWorkerUnreachable": "Could not reach cloud worker. Make sure the cloud service is running (npm run dev in /cloud).",
|
||||
"connectionFailed": "Connection failed",
|
||||
"syncFailed": "Failed to sync cloud data",
|
||||
"providerModelsTitle": "{provider} \u2014 Models",
|
||||
"noModelsForProvider": "No models available for this provider.",
|
||||
"chat": "Chat",
|
||||
"embedding": "Embedding",
|
||||
"image": "Image",
|
||||
"custom": "custom",
|
||||
"modelsCount": "{count, plural, one {# model} other {# models}}"
|
||||
},
|
||||
"health": {
|
||||
"title": "System Health",
|
||||
@@ -419,7 +637,13 @@
|
||||
"retry": "Retry",
|
||||
"allOperational": "All systems operational",
|
||||
"issuesDetected": "System issues detected",
|
||||
"updatedAt": "Updated {time}",
|
||||
"latency": "Latency",
|
||||
"latencyP50": "p50",
|
||||
"latencyP95": "p95",
|
||||
"latencyP99": "p99",
|
||||
"millisecondsShort": "{value}ms",
|
||||
"notAvailable": "—",
|
||||
"totalRequests": "Total requests",
|
||||
"noDataYet": "No data yet",
|
||||
"promptCache": "Prompt Cache",
|
||||
@@ -427,10 +651,18 @@
|
||||
"hitRate": "Hit Rate",
|
||||
"hitsMisses": "Hits / Misses",
|
||||
"signatureCache": "Signature Cache",
|
||||
"signatureDefaults": "Defaults",
|
||||
"signatureTool": "Tool",
|
||||
"signatureFamily": "Family",
|
||||
"signatureSession": "Session",
|
||||
"recovering": "Recovering",
|
||||
"noCBData": "No circuit breaker data available. Make some requests first.",
|
||||
"providerHealthStatusAria": "Provider health status",
|
||||
"issuesLabel": "Issues Detected",
|
||||
"operational": "Operational",
|
||||
"providers": "Providers",
|
||||
"healthyCount": "{count} healthy",
|
||||
"nodeVersion": "Node {version}",
|
||||
"failures": "{count} failure",
|
||||
"failuresPlural": "{count} failures",
|
||||
"lastFailure": "Last",
|
||||
@@ -438,9 +670,13 @@
|
||||
"activeLimiters": "{count} active limiter",
|
||||
"activeLimitersPlural": "{count} active limiters",
|
||||
"queued": "Queued",
|
||||
"queuedCount": "{count} queued",
|
||||
"running": "running",
|
||||
"runningCount": "{count} running",
|
||||
"ok": "OK",
|
||||
"activeLockouts": "Active Lockouts",
|
||||
"resetConfirm": "Reset all circuit breakers to healthy state? This will clear all failure counts and restore all providers to operational status.",
|
||||
"resetAllTitle": "Reset all circuit breakers to healthy state",
|
||||
"resetting": "Resetting...",
|
||||
"resetAll": "Reset All",
|
||||
"until": "Until {time}"
|
||||
@@ -464,12 +700,21 @@
|
||||
"refresh": "Refresh",
|
||||
"filterByAction": "Filter by action...",
|
||||
"filterByActor": "Filter by actor...",
|
||||
"filterEntriesAria": "Filter audit log entries",
|
||||
"filterByActionTypeAria": "Filter by action type",
|
||||
"filterByActorAria": "Filter by actor",
|
||||
"refreshAuditLogAria": "Refresh audit log",
|
||||
"tableAria": "Audit log entries",
|
||||
"failedFetchAuditLog": "Failed to fetch audit log",
|
||||
"showing": "Showing {count} entries (offset {offset})",
|
||||
"search": "Search",
|
||||
"timestamp": "Timestamp",
|
||||
"action": "Action",
|
||||
"actor": "Actor",
|
||||
"target": "Target",
|
||||
"details": "Details",
|
||||
"ipAddress": "IP Address",
|
||||
"notAvailable": "—",
|
||||
"noEntries": "No audit log entries found",
|
||||
"previous": "Previous",
|
||||
"next": "Next"
|
||||
@@ -1206,6 +1451,51 @@
|
||||
"scenarioThinking": "Thinking",
|
||||
"scenarioSystemPrompt": "System Prompt",
|
||||
"scenarioStreaming": "Streaming",
|
||||
"templateNames": {
|
||||
"simple-chat": "Simple Chat",
|
||||
"tool-calling": "Tool Calling",
|
||||
"multi-turn": "Multi-turn",
|
||||
"thinking": "Thinking",
|
||||
"system-prompt": "System Prompt",
|
||||
"streaming": "Streaming"
|
||||
},
|
||||
"templateDescriptions": {
|
||||
"simple-chat": "Basic text message",
|
||||
"tool-calling": "Function/tool invocation",
|
||||
"multi-turn": "Conversation with history",
|
||||
"thinking": "Extended thinking / reasoning",
|
||||
"system-prompt": "Complex system instructions",
|
||||
"streaming": "SSE streaming request"
|
||||
},
|
||||
"templatePayloads": {
|
||||
"simpleChat": {
|
||||
"system": "You are a helpful assistant.",
|
||||
"userGreeting": "Hello! How are you today?"
|
||||
},
|
||||
"toolCalling": {
|
||||
"userWeather": "What's the weather in São Paulo?",
|
||||
"toolDescription": "Get current weather for a location",
|
||||
"cityNameDescription": "City name"
|
||||
},
|
||||
"multiTurn": {
|
||||
"system": "You are a coding assistant.",
|
||||
"userInitial": "Write a function to sort an array in Python.",
|
||||
"assistantExample": "Here's a simple sort function:\n\n```python\ndef sort_array(arr):\n return sorted(arr)\n```",
|
||||
"userFollowUp": "Now make it sort in descending order."
|
||||
},
|
||||
"thinking": {
|
||||
"question": "What is the sum of the first 100 prime numbers?"
|
||||
},
|
||||
"systemPrompt": {
|
||||
"systemInstruction": "You are a senior software engineer specializing in distributed systems. Answer questions concisely using industry best practices. Always provide code examples when relevant. Format your responses using markdown.",
|
||||
"question": "How do I implement a circuit breaker pattern?"
|
||||
},
|
||||
"streaming": {
|
||||
"prompt": "Tell me a short story about a robot learning to paint."
|
||||
}
|
||||
},
|
||||
"openaiCompatibleLabel": "OpenAI Compatible",
|
||||
"anthropicCompatibleLabel": "Anthropic Compatible",
|
||||
"noTemplateForFormat": "No template for this format",
|
||||
"translationFailed": "Translation failed: {error}",
|
||||
"pipelineDebugger": "Pipeline Debugger",
|
||||
@@ -1270,6 +1560,9 @@
|
||||
"totalRequests": "Total requests",
|
||||
"noDataYet": "No data yet",
|
||||
"latency": "Latency",
|
||||
"latencyP50": "p50",
|
||||
"latencyP95": "p95",
|
||||
"latencyP99": "p99",
|
||||
"promptCache": "Prompt Cache",
|
||||
"systemHealth": "System Health",
|
||||
"entries": "Entries",
|
||||
@@ -1457,7 +1750,30 @@
|
||||
"enterPassword": "Enter your password to continue",
|
||||
"password": "Password",
|
||||
"unifiedProxy": "Unified AI API Proxy",
|
||||
"unifiedAiApiProxy": "Unified AI API Proxy",
|
||||
"unifiedAiApiProxyDesc": "Route requests to multiple AI providers through a single endpoint. Load balancing, failover, and usage tracking built in.",
|
||||
"passwordNotEnabled": "Password protection is not enabled",
|
||||
"loading": "Loading...",
|
||||
"invalidPassword": "Invalid password",
|
||||
"errorOccurredRetry": "An error occurred. Please try again.",
|
||||
"configureInstance": "Let's get your OmniRoute instance configured",
|
||||
"runOnboardingWizard": "Run the onboarding wizard to set up your password and connect your first AI provider.",
|
||||
"startOnboarding": "Start Onboarding",
|
||||
"secureYourInstance": "Secure Your Instance",
|
||||
"setPasswordDescription": "Set a password to protect your dashboard and secure your API endpoints from unauthorized access.",
|
||||
"configurePassword": "Configure Password",
|
||||
"continue": "Continue",
|
||||
"windowWillClose": "This window will close automatically...",
|
||||
"closeTabNow": "You can close this tab now.",
|
||||
"copyUrlManual": "Please copy the URL from the address bar and paste it in the application.",
|
||||
"accessDeniedDescription": "You don't have permission to access this resource. Check your API key or contact the administrator.",
|
||||
"goToDashboard": "Go to Dashboard",
|
||||
"featureMultiProviderTitle": "Multi-Provider",
|
||||
"featureMultiProviderDesc": "OpenAI, Anthropic, Google, and more",
|
||||
"featureLoadBalancingTitle": "Load Balancing",
|
||||
"featureLoadBalancingDesc": "Distribute requests intelligently",
|
||||
"featureUsageTrackingTitle": "Usage Tracking",
|
||||
"featureUsageTrackingDesc": "Monitor costs and tokens",
|
||||
"resetPassword": "Reset Password",
|
||||
"resetDescription": "Choose a method to recover access to your dashboard",
|
||||
"stopServer": "Stop the OmniRoute server",
|
||||
@@ -1678,6 +1994,7 @@
|
||||
"termsMetadataDescription": "Terms of service for the OmniRoute AI API proxy router.",
|
||||
"backToHome": "Back to home",
|
||||
"lastUpdated": "Last updated: {date}",
|
||||
"policyLastUpdatedDate": "February 13, 2026",
|
||||
"listSeparator": "-",
|
||||
"questionsVisit": "Questions? Visit our",
|
||||
"githubRepository": "GitHub repository",
|
||||
|
||||
@@ -117,13 +117,13 @@
|
||||
"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.",
|
||||
"step1Desc": "Vá em <endpoint>Endpoint</endpoint> -> Chaves Registradas. Gere uma chave por ambiente.",
|
||||
"step2Title": "2. Conectar provedores",
|
||||
"step2Desc": "Adicione contas em {providers}. Suporta OAuth, API Key e planos gratuitos.",
|
||||
"step2Desc": "Adicione contas em <providers>Provedores</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}.",
|
||||
"step4Desc": "Acompanhe tokens, custos e erros em <logs>Logs de Requisições</logs> e <analytics>Análises</analytics>.",
|
||||
"providersOverview": "Visão Geral dos Provedores",
|
||||
"configuredOf": "{configured} configurados de {total} provedores disponíveis",
|
||||
"noModelsAvailable": "Nenhum modelo disponível para este provedor.",
|
||||
@@ -137,7 +137,13 @@
|
||||
"documentation": "Documentação",
|
||||
"healthMonitor": "Monitor de Saúde",
|
||||
"reportIssue": "Reportar problema",
|
||||
"activeError": "{active} ativo · {errors} erro"
|
||||
"activeError": "{active} ativo · {errors} erro",
|
||||
"oauthLabel": "OAuth",
|
||||
"apiKeyLabel": "Chave de API",
|
||||
"requestsShort": "{count} reqs",
|
||||
"providerModelsTitle": "{provider} - Modelos",
|
||||
"copiedModel": "Copiado: {model}",
|
||||
"aliasLabel": "alias"
|
||||
},
|
||||
"analytics": {
|
||||
"title": "Análises",
|
||||
@@ -243,6 +249,13 @@
|
||||
"noEntries": "Nenhum registro de auditoria",
|
||||
"filterByAction": "Filtrar por ação...",
|
||||
"filterByActor": "Filtrar por autor...",
|
||||
"filterEntriesAria": "Filtrar entradas do log de auditoria",
|
||||
"filterByActionTypeAria": "Filtrar por tipo de ação",
|
||||
"filterByActorAria": "Filtrar por autor",
|
||||
"refreshAuditLogAria": "Atualizar log de auditoria",
|
||||
"tableAria": "Entradas do log de auditoria",
|
||||
"failedFetchAuditLog": "Falha ao carregar log de auditoria",
|
||||
"notAvailable": "—",
|
||||
"description": "Ações administrativas e eventos de segurança",
|
||||
"showing": "Mostrando {count} entradas (offset {offset})",
|
||||
"previous": "Anterior"
|
||||
@@ -258,7 +271,199 @@
|
||||
"instructions": "Instruções",
|
||||
"modelMapping": "Mapeamento de Modelos",
|
||||
"baseUrl": "URL Base",
|
||||
"apiKey": "Chave de API"
|
||||
"apiKey": "Chave de API",
|
||||
"configured": "Configurado",
|
||||
"notConfigured": "Não configurado",
|
||||
"notInstalled": "Não instalado",
|
||||
"custom": "Customizado",
|
||||
"unknown": "Desconhecido",
|
||||
"lastSavedAt": "Último salvamento: {date}",
|
||||
"never": "Nunca",
|
||||
"justNow": "agora mesmo",
|
||||
"minutesAgoShort": "{count} min atrás",
|
||||
"hoursAgoShort": "{count} h atrás",
|
||||
"daysAgoShort": "{count} d atrás",
|
||||
"monthsAgoShort": "{count} mês(es) atrás",
|
||||
"yearsAgoShort": "{count} ano(s) atrás",
|
||||
"runtimeCheckFailed": "Falha ao verificar runtime",
|
||||
"yourApiKeyPlaceholder": "sua-api-key",
|
||||
"modelPlaceholder": "provedor/modelo-id",
|
||||
"configurationSaved": "Configuração salva com sucesso.",
|
||||
"failedToSave": "Falha ao salvar configuração.",
|
||||
"noApiKeysCreateOne": "Sem chaves de API - crie uma na página Chaves",
|
||||
"defaultOmnirouteKey": "sk_omniroute (padrão)",
|
||||
"selectModel": "Selecionar Modelo",
|
||||
"selectModelForAlias": "Selecionar modelo para {alias}",
|
||||
"selectModelForTool": "Selecionar Modelo para {tool}",
|
||||
"select": "Selecionar",
|
||||
"clear": "Limpar",
|
||||
"comingSoon": "Em breve",
|
||||
"checkingRuntime": "Verificando status de runtime...",
|
||||
"guideOnlyIntegration": "Integração apenas por guia (não requer runtime local)",
|
||||
"cliRuntimeDetected": "Runtime de CLI detectado e pronto",
|
||||
"cliFoundNotRunnable": "CLI encontrado, mas não executável{reason}",
|
||||
"cliRuntimeNotDetected": "Runtime de CLI não detectado",
|
||||
"binary": "Binário",
|
||||
"configPath": "Caminho de config",
|
||||
"configPathShort": "Config",
|
||||
"failedCheckRuntimeStatus": "Falha ao verificar status de runtime.",
|
||||
"copy": "Copiar",
|
||||
"copied": "Copiado",
|
||||
"copyConfig": "Copiar Config",
|
||||
"saveConfig": "Salvar Config",
|
||||
"selectionSaved": "Seleção salva",
|
||||
"guide": "Guia",
|
||||
"detected": "Detectado",
|
||||
"notReady": "Não pronto",
|
||||
"active": "Ativo",
|
||||
"inactive": "Inativo",
|
||||
"startMitm": "Iniciar MITM",
|
||||
"stopMitm": "Parar MITM",
|
||||
"mitmStarted": "MITM iniciado com sucesso!",
|
||||
"mitmStopped": "MITM parado com sucesso!",
|
||||
"failedStart": "Falha ao iniciar MITM",
|
||||
"failedStop": "Falha ao parar MITM",
|
||||
"saveMappings": "Salvar Mapeamentos",
|
||||
"mappingsSaved": "Mapeamentos salvos!",
|
||||
"failedSaveMappings": "Falha ao salvar mapeamentos",
|
||||
"howItWorks": "Como funciona:",
|
||||
"antigravityHowWorksDesc": "O Antigravity envia requisições para o endpoint do Google. O MITM intercepta e redireciona para o OmniRoute.",
|
||||
"antigravityStep1": "1. Inicie o MITM para rotear as requisições pelo OmniRoute.",
|
||||
"antigravityStep2Prefix": "2. Adicione",
|
||||
"antigravityStep2Suffix": "ao arquivo hosts como 127.0.0.1.",
|
||||
"antigravityStep3": "3. Abra o Antigravity e as requisições serão proxyadas.",
|
||||
"sudoPasswordRequiredTitle": "Senha sudo necessária",
|
||||
"sudoPasswordHint": "A senha de administrador é necessária para modificar hosts e configurações de proxy do sistema.",
|
||||
"enterSudoPassword": "Digite a senha sudo",
|
||||
"sudoPasswordRequiredError": "A senha sudo é obrigatória.",
|
||||
"cancel": "Cancelar",
|
||||
"confirm": "Confirmar",
|
||||
"settingsApplied": "Configurações aplicadas com sucesso!",
|
||||
"failedApplySettings": "Falha ao aplicar configurações",
|
||||
"settingsReset": "Configurações resetadas com sucesso!",
|
||||
"failedResetSettings": "Falha ao resetar configurações",
|
||||
"backupRestored": "Backup restaurado!",
|
||||
"failedRestore": "Falha ao restaurar",
|
||||
"checkingCli": "Verificando CLI {tool}...",
|
||||
"cliNotRunnable": "CLI {tool} instalado, mas não executável",
|
||||
"cliNotInstalled": "CLI {tool} não instalado",
|
||||
"cliNotDetected": "CLI {tool} não detectado",
|
||||
"cliDetectedReady": "CLI {tool} detectado e pronto",
|
||||
"cliFoundFailedHealthcheck": "CLI {tool} foi encontrado, mas falhou no healthcheck de runtime{reason}.",
|
||||
"installCliPrompt": "Instale o CLI {tool} para usar este recurso.",
|
||||
"installCodexPrompt": "Instale o Codex CLI para usar a aplicação automática.",
|
||||
"hide": "Ocultar",
|
||||
"howToInstall": "Como instalar",
|
||||
"installationGuide": "Guia de instalação",
|
||||
"platforms": "macOS / Linux / Windows:",
|
||||
"afterInstallationRun": "Após a instalação, execute",
|
||||
"toVerify": "para verificar.",
|
||||
"current": "Atual",
|
||||
"baseUrlPlaceholder": "https://.../v1",
|
||||
"resetToDefault": "Redefinir para padrão",
|
||||
"providerModelPlaceholder": "provedor/modelo-id",
|
||||
"apply": "Aplicar",
|
||||
"reset": "Resetar",
|
||||
"manualConfig": "Configuração manual",
|
||||
"backups": "Backups",
|
||||
"configBackups": "Backups de configuração",
|
||||
"noBackupsYet": "Ainda não há backups. Backups são criados automaticamente antes de cada Aplicar ou Resetar.",
|
||||
"restore": "Restaurar",
|
||||
"backupRestoredReloading": "Backup restaurado! Recarregando status...",
|
||||
"failedRestoreBackup": "Falha ao restaurar backup",
|
||||
"applied": "Aplicado!",
|
||||
"failed": "Falhou",
|
||||
"resetDone": "Resetado!",
|
||||
"omnirouteConfiguredOpenAiCompatible": "OmniRoute está configurado como provedor compatível com OpenAI",
|
||||
"provider": "Provedor",
|
||||
"model": "Modelo",
|
||||
"providers": "Provedores",
|
||||
"auth": "Autenticação",
|
||||
"noApiKeysAvailable": "Nenhuma chave de API disponível",
|
||||
"usingDefaultOmniroute": "Usando padrão: sk_omniroute",
|
||||
"updateConfig": "Atualizar config",
|
||||
"applyConfig": "Aplicar config",
|
||||
"noBackupsAvailable": "Nenhum backup disponível.",
|
||||
"profileSaved": "Perfil \"{name}\" salvo!",
|
||||
"failedSaveProfile": "Falha ao salvar perfil",
|
||||
"profileActivated": "Perfil ativado!",
|
||||
"failedActivateProfile": "Falha ao ativar perfil",
|
||||
"profiles": "Perfis",
|
||||
"savedProfiles": "Perfis salvos",
|
||||
"noProfilesYet": "Nenhum perfil salvo ainda. Salve a configuração atual como perfil abaixo.",
|
||||
"activate": "Ativar",
|
||||
"deleteProfile": "Excluir perfil",
|
||||
"profileNamePlaceholder": "Nome do perfil (ex: Conta Pessoal)",
|
||||
"saveCurrent": "Salvar atual",
|
||||
"codexAuthNotePrefix": "Codex usa",
|
||||
"codexAuthNoteMiddle": "com",
|
||||
"codexAuthNoteSuffix": "Clique em \"Aplicar\" para configurar automaticamente.",
|
||||
"claudeManualConfiguration": "Claude CLI - Configuração manual",
|
||||
"codexManualConfiguration": "Codex CLI - Configuração manual",
|
||||
"droidManualConfiguration": "Factory Droid - Configuração manual",
|
||||
"openClawManualConfiguration": "Open Claw - Configuração manual",
|
||||
"clineManualConfiguration": "Configuração manual do Cline",
|
||||
"kiloManualConfiguration": "Configuração manual do Kilo Code",
|
||||
"toolDescriptions": {
|
||||
"antigravity": "Google Antigravity IDE com MITM",
|
||||
"claude": "CLI Claude Code da Anthropic",
|
||||
"codex": "CLI Codex da OpenAI",
|
||||
"droid": "Assistente de IA Factory Droid",
|
||||
"openclaw": "Assistente de IA Open Claw",
|
||||
"cline": "CLI assistente de codificação Cline",
|
||||
"kilo": "CLI assistente de IA Kilo Code",
|
||||
"cursor": "Editor de código com IA Cursor",
|
||||
"continue": "Assistente de IA Continue"
|
||||
},
|
||||
"guides": {
|
||||
"cursor": {
|
||||
"notes": {
|
||||
"0": "Requer conta Cursor Pro para usar este recurso.",
|
||||
"1": "O Cursor roteia requisições pelo próprio servidor, então endpoint local não é suportado. Ative o Cloud Endpoint em Configurações."
|
||||
},
|
||||
"steps": {
|
||||
"1": {
|
||||
"title": "Abrir Configurações",
|
||||
"desc": "Vá em Configurações -> Modelos"
|
||||
},
|
||||
"2": {
|
||||
"title": "Ativar OpenAI API",
|
||||
"desc": "Ative a opção \"OpenAI API key\""
|
||||
},
|
||||
"3": {
|
||||
"title": "URL Base"
|
||||
},
|
||||
"4": {
|
||||
"title": "Chave de API"
|
||||
},
|
||||
"5": {
|
||||
"title": "Adicionar Modelo Customizado",
|
||||
"desc": "Clique em \"View All Model\" -> \"Add Custom Model\""
|
||||
},
|
||||
"6": {
|
||||
"title": "Selecionar Modelo"
|
||||
}
|
||||
}
|
||||
},
|
||||
"continue": {
|
||||
"steps": {
|
||||
"1": {
|
||||
"title": "Abrir Config",
|
||||
"desc": "Abra o arquivo de configuração do Continue"
|
||||
},
|
||||
"2": {
|
||||
"title": "Chave de API"
|
||||
},
|
||||
"3": {
|
||||
"title": "Selecionar Modelo"
|
||||
},
|
||||
"4": {
|
||||
"title": "Adicionar Config de Modelo",
|
||||
"desc": "Adicione a configuração abaixo ao array de modelos:"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"combos": {
|
||||
"title": "Combos",
|
||||
@@ -390,10 +595,23 @@
|
||||
"disabling": "Desativando...",
|
||||
"cloudConnectedVerified": "Proxy na Nuvem conectado e verificado!",
|
||||
"connectedVerificationPending": "Conectado — verificação pendente",
|
||||
"connectedVerificationPendingWithError": "Conectado — verificação pendente: {error}",
|
||||
"cloudDisabledSuccess": "Nuvem desativada com sucesso",
|
||||
"syncedSuccess": "Sincronizado com sucesso",
|
||||
"failedDisable": "Falha ao desativar nuvem",
|
||||
"failedEnable": "Falha ao ativar nuvem"
|
||||
"failedEnable": "Falha ao ativar nuvem",
|
||||
"cloudRequestTimeout": "Tempo limite da requisição em nuvem",
|
||||
"cloudRequestFailed": "Falha na requisição em nuvem",
|
||||
"cloudWorkerUnreachable": "Não foi possível alcançar o worker de nuvem. Verifique se o serviço cloud está rodando (npm run dev em /cloud).",
|
||||
"connectionFailed": "Falha na conexão",
|
||||
"syncFailed": "Falha ao sincronizar dados da nuvem",
|
||||
"providerModelsTitle": "{provider} — Modelos",
|
||||
"noModelsForProvider": "Nenhum modelo disponível para este provedor.",
|
||||
"chat": "Chat",
|
||||
"embedding": "Embedding",
|
||||
"image": "Imagem",
|
||||
"custom": "custom",
|
||||
"modelsCount": "{count, plural, one {# modelo} other {# modelos}}"
|
||||
},
|
||||
"health": {
|
||||
"title": "Saúde do Sistema",
|
||||
@@ -419,7 +637,13 @@
|
||||
"retry": "Tentar Novamente",
|
||||
"allOperational": "Todos os sistemas operacionais",
|
||||
"issuesDetected": "Problemas detectados no sistema",
|
||||
"updatedAt": "Atualizado {time}",
|
||||
"latency": "Latência",
|
||||
"latencyP50": "p50",
|
||||
"latencyP95": "p95",
|
||||
"latencyP99": "p99",
|
||||
"millisecondsShort": "{value}ms",
|
||||
"notAvailable": "—",
|
||||
"totalRequests": "Total de requisições",
|
||||
"noDataYet": "Sem dados ainda",
|
||||
"promptCache": "Cache de Prompt",
|
||||
@@ -427,10 +651,18 @@
|
||||
"hitRate": "Taxa de Acerto",
|
||||
"hitsMisses": "Acertos / Erros",
|
||||
"signatureCache": "Cache de Assinatura",
|
||||
"signatureDefaults": "Padrões",
|
||||
"signatureTool": "Ferramenta",
|
||||
"signatureFamily": "Família",
|
||||
"signatureSession": "Sessão",
|
||||
"recovering": "Recuperando",
|
||||
"noCBData": "Nenhum dado de circuit breaker disponível. Faça algumas requisições primeiro.",
|
||||
"providerHealthStatusAria": "Status de saúde dos provedores",
|
||||
"issuesLabel": "Problemas Detectados",
|
||||
"operational": "Operacional",
|
||||
"providers": "Provedores",
|
||||
"healthyCount": "{count} saudáveis",
|
||||
"nodeVersion": "Node {version}",
|
||||
"failures": "{count} falha",
|
||||
"failuresPlural": "{count} falhas",
|
||||
"lastFailure": "Última",
|
||||
@@ -438,9 +670,13 @@
|
||||
"activeLimiters": "{count} limitador ativo",
|
||||
"activeLimitersPlural": "{count} limitadores ativos",
|
||||
"queued": "Na Fila",
|
||||
"queuedCount": "{count} na fila",
|
||||
"running": "executando",
|
||||
"runningCount": "{count} executando",
|
||||
"ok": "OK",
|
||||
"activeLockouts": "Bloqueios Ativos",
|
||||
"resetConfirm": "Resetar todos os circuit breakers para estado saudável? Isso limpará todos os contadores de falha e restaurará todos os provedores ao status operacional.",
|
||||
"resetAllTitle": "Resetar todos os circuit breakers para estado saudável",
|
||||
"resetting": "Resetando...",
|
||||
"resetAll": "Resetar Tudo",
|
||||
"until": "Até {time}"
|
||||
@@ -464,12 +700,21 @@
|
||||
"refresh": "Atualizar",
|
||||
"filterByAction": "Filtrar por ação...",
|
||||
"filterByActor": "Filtrar por ator...",
|
||||
"filterEntriesAria": "Filtrar entradas do log de auditoria",
|
||||
"filterByActionTypeAria": "Filtrar por tipo de ação",
|
||||
"filterByActorAria": "Filtrar por ator",
|
||||
"refreshAuditLogAria": "Atualizar log de auditoria",
|
||||
"tableAria": "Entradas do log de auditoria",
|
||||
"failedFetchAuditLog": "Falha ao carregar log de auditoria",
|
||||
"showing": "Mostrando {count} entradas (offset {offset})",
|
||||
"search": "Buscar",
|
||||
"timestamp": "Data/Hora",
|
||||
"action": "Ação",
|
||||
"actor": "Ator",
|
||||
"target": "Alvo",
|
||||
"details": "Detalhes",
|
||||
"ipAddress": "Endereço IP",
|
||||
"notAvailable": "—",
|
||||
"noEntries": "Nenhuma entrada de log de auditoria encontrada",
|
||||
"previous": "Anterior",
|
||||
"next": "Próximo"
|
||||
@@ -1206,6 +1451,51 @@
|
||||
"scenarioThinking": "Raciocínio",
|
||||
"scenarioSystemPrompt": "Prompt de Sistema",
|
||||
"scenarioStreaming": "Streaming",
|
||||
"templateNames": {
|
||||
"simple-chat": "Chat Simples",
|
||||
"tool-calling": "Chamada de Ferramenta",
|
||||
"multi-turn": "Multiturno",
|
||||
"thinking": "Raciocínio",
|
||||
"system-prompt": "Prompt de Sistema",
|
||||
"streaming": "Streaming"
|
||||
},
|
||||
"templateDescriptions": {
|
||||
"simple-chat": "Mensagem de texto básica",
|
||||
"tool-calling": "Invocação de função/ferramenta",
|
||||
"multi-turn": "Conversa com histórico",
|
||||
"thinking": "Raciocínio estendido",
|
||||
"system-prompt": "Instruções de sistema complexas",
|
||||
"streaming": "Requisição de streaming SSE"
|
||||
},
|
||||
"templatePayloads": {
|
||||
"simpleChat": {
|
||||
"system": "Você é um assistente prestativo.",
|
||||
"userGreeting": "Olá! Como você está hoje?"
|
||||
},
|
||||
"toolCalling": {
|
||||
"userWeather": "Como está o tempo em São Paulo?",
|
||||
"toolDescription": "Obtém o clima atual para uma localidade",
|
||||
"cityNameDescription": "Nome da cidade"
|
||||
},
|
||||
"multiTurn": {
|
||||
"system": "Você é um assistente de programação.",
|
||||
"userInitial": "Escreva uma função para ordenar um array em Python.",
|
||||
"assistantExample": "Aqui está uma função simples de ordenação:\n\n```python\ndef sort_array(arr):\n return sorted(arr)\n```",
|
||||
"userFollowUp": "Agora faça para ordenar em ordem decrescente."
|
||||
},
|
||||
"thinking": {
|
||||
"question": "Qual é a soma dos 100 primeiros números primos?"
|
||||
},
|
||||
"systemPrompt": {
|
||||
"systemInstruction": "Você é um engenheiro de software sênior especializado em sistemas distribuídos. Responda de forma concisa usando boas práticas de mercado. Sempre forneça exemplos de código quando relevante. Formate suas respostas usando markdown.",
|
||||
"question": "Como implemento o padrão circuit breaker?"
|
||||
},
|
||||
"streaming": {
|
||||
"prompt": "Conte uma história curta sobre um robô aprendendo a pintar."
|
||||
}
|
||||
},
|
||||
"openaiCompatibleLabel": "Compatível com OpenAI",
|
||||
"anthropicCompatibleLabel": "Compatível com Anthropic",
|
||||
"noTemplateForFormat": "Sem modelo para este formato",
|
||||
"translationFailed": "Falha na tradução: {error}",
|
||||
"pipelineDebugger": "Depurador de Pipeline",
|
||||
@@ -1270,6 +1560,9 @@
|
||||
"totalRequests": "Total de requisições",
|
||||
"noDataYet": "Sem dados ainda",
|
||||
"latency": "Latência",
|
||||
"latencyP50": "p50",
|
||||
"latencyP95": "p95",
|
||||
"latencyP99": "p99",
|
||||
"promptCache": "Cache de Prompt",
|
||||
"systemHealth": "Saúde do Sistema",
|
||||
"entries": "Entradas",
|
||||
@@ -1457,7 +1750,30 @@
|
||||
"enterPassword": "Digite sua senha para continuar",
|
||||
"password": "Senha",
|
||||
"unifiedProxy": "Proxy Unificado de API de IA",
|
||||
"unifiedAiApiProxy": "Proxy Unificado de API de IA",
|
||||
"unifiedAiApiProxyDesc": "Roteie requisições para múltiplos provedores de IA por um único endpoint. Balanceamento de carga, failover e rastreamento de uso integrados.",
|
||||
"passwordNotEnabled": "Proteção por senha não está ativada",
|
||||
"loading": "Carregando...",
|
||||
"invalidPassword": "Senha inválida",
|
||||
"errorOccurredRetry": "Ocorreu um erro. Tente novamente.",
|
||||
"configureInstance": "Vamos configurar sua instância OmniRoute",
|
||||
"runOnboardingWizard": "Execute o assistente de onboarding para definir sua senha e conectar seu primeiro provedor de IA.",
|
||||
"startOnboarding": "Iniciar Onboarding",
|
||||
"secureYourInstance": "Proteja sua Instância",
|
||||
"setPasswordDescription": "Defina uma senha para proteger seu painel e garantir que seus endpoints de API não sejam acessados sem autorização.",
|
||||
"configurePassword": "Configurar Senha",
|
||||
"continue": "Continuar",
|
||||
"windowWillClose": "Esta janela será fechada automaticamente...",
|
||||
"closeTabNow": "Você já pode fechar esta aba.",
|
||||
"copyUrlManual": "Copie a URL da barra de endereços e cole no aplicativo.",
|
||||
"accessDeniedDescription": "Você não tem permissão para acessar este recurso. Verifique sua chave de API ou contate o administrador.",
|
||||
"goToDashboard": "Ir para o Painel",
|
||||
"featureMultiProviderTitle": "Multi-Provedor",
|
||||
"featureMultiProviderDesc": "OpenAI, Anthropic, Google e outros",
|
||||
"featureLoadBalancingTitle": "Balanceamento de Carga",
|
||||
"featureLoadBalancingDesc": "Distribua requisições de forma inteligente",
|
||||
"featureUsageTrackingTitle": "Rastreamento de Uso",
|
||||
"featureUsageTrackingDesc": "Monitore custos e tokens",
|
||||
"resetPassword": "Redefinir Senha",
|
||||
"resetDescription": "Escolha um método para recuperar acesso ao painel",
|
||||
"stopServer": "Pare o servidor OmniRoute",
|
||||
@@ -1678,6 +1994,7 @@
|
||||
"termsMetadataDescription": "Termos de serviço do roteador proxy de API de IA OmniRoute.",
|
||||
"backToHome": "Voltar para a home",
|
||||
"lastUpdated": "Última atualização: {date}",
|
||||
"policyLastUpdatedDate": "13 de fevereiro de 2026",
|
||||
"listSeparator": "-",
|
||||
"questionsVisit": "Dúvidas? Visite nosso",
|
||||
"githubRepository": "repositório no GitHub",
|
||||
|
||||
Reference in New Issue
Block a user