mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-04 14:22:09 +03:00
chore(i18n): replace hardcoded UI text with t() calls across dashboard (round 6 — 10 parallel agents)
Round-6 dispatched 10 parallel subagents covering all 57 remaining dashboard files. Each agent worked on a disjoint file set to avoid en.json race conditions. Added ~60 new i18n keys across 9 namespaces covering small UI labels, table headers, search placeholders, and empty-state messages. Major changes: - analytics: SearchAnalyticsTab, ProviderUtilizationTab, DiversityScoreCard, CompressionAnalyticsTab (new useTranslations + keys) - batch: BatchDetailModal, BatchListTab, FileDetailModal, FilesListTab (new useTranslations + keys) - settings: CliproxyapiSettingsTab, PayloadRulesTab, ModelCooldownsCard, AppearanceTab, PricingTab (mostly new useTranslations) - endpoint: TokenSaverCard, ApiEndpointsTab, EndpointPageClient - cache: CachePerformance, IdempotencyLayer, ReasoningCacheTab, MediaPageClient, page - combos: IntelligentComboPanel, page - playground: ChatPlayground, SearchPlayground - providers: ProviderCard - onboarding: TierFlowDiagram - changelog: ChangelogViewer - home: ProviderTopology, TierCoverageWidget, BootstrapBanner, BadgeToast - usage: BudgetTab, BudgetTelemetryCards, QuotaTable - quotaShare: QuotaSharePageClient - profile: page - leaderboard: page - skills: page Hardcoded total: 131 → 60. Real missing keys: 0 plus 1 false-positive for combos.modePack (lookup via prop-passed t).
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
/**
|
||||
* Shown when OmniRoute was started with auto-generated secrets (zero-config mode).
|
||||
* The banner is dismissable and persists only for the current session.
|
||||
*/
|
||||
export default function BootstrapBanner() {
|
||||
const t = useTranslations("common");
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
|
||||
if (dismissed) return null;
|
||||
@@ -46,7 +48,7 @@ export default function BootstrapBanner() {
|
||||
<button
|
||||
onClick={() => setDismissed(true)}
|
||||
className="shrink-0 text-amber-600/60 hover:text-amber-700 dark:text-amber-400/60 dark:hover:text-amber-300 transition-colors ml-1"
|
||||
aria-label="Dismiss"
|
||||
aria-label={t("bootstrapBannerDismiss")}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
|
||||
@@ -65,8 +65,8 @@ export function TierCoverageWidget() {
|
||||
<div className="rounded-xl border border-white/[0.06] bg-surface p-5">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="font-semibold text-sm">Tier coverage</h3>
|
||||
<p className="text-xs text-text-muted mt-0.5">Providers configured per fallback tier</p>
|
||||
<h3 className="font-semibold text-sm">{t("tierCoverageTitle")}</h3>
|
||||
<p className="text-xs text-text-muted mt-0.5">{t("tierCoverageSubtitle")}</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/dashboard/providers"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
CartesianGrid,
|
||||
@@ -89,6 +90,7 @@ function getLatestPoints(points: ProviderUtilizationPoint[]) {
|
||||
}
|
||||
|
||||
export default function ProviderUtilizationTab() {
|
||||
const t = useTranslations("analytics");
|
||||
const [range, setRange] = useState<UtilizationTimeRange>("24h");
|
||||
const [aggregateBy, setAggregateBy] = useState<"provider" | "connection">("provider");
|
||||
const [data, setData] = useState<ProviderUtilizationResponse | null>(null);
|
||||
@@ -191,7 +193,7 @@ export default function ProviderUtilizationTab() {
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<Card
|
||||
title="Provider utilization"
|
||||
title={t("providerUtilizationTitle")}
|
||||
subtitle={RANGE_LABELS[range]}
|
||||
icon="monitoring"
|
||||
action={
|
||||
@@ -236,7 +238,9 @@ export default function ProviderUtilizationTab() {
|
||||
<div className="flex min-h-80 flex-col items-center justify-center gap-4 text-center">
|
||||
<span className="material-symbols-outlined text-[32px] text-error">error</span>
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-sm font-medium text-text-main">Failed to load utilization data</p>
|
||||
<p className="text-sm font-medium text-text-main">
|
||||
{t("providerUtilizationFailedToLoad")}
|
||||
</p>
|
||||
<p className="text-sm text-text-muted">{error}</p>
|
||||
</div>
|
||||
<button
|
||||
@@ -266,13 +270,15 @@ export default function ProviderUtilizationTab() {
|
||||
timeline
|
||||
</span>
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm font-medium text-text-main">No utilization data available</p>
|
||||
<p className="text-sm font-medium text-text-main">{t("providerUtilizationNoData")}</p>
|
||||
<p className="max-w-md text-sm text-text-muted">
|
||||
Provider quota snapshots will appear here after utilization data is collected.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-black/5 bg-black/[0.02] p-4 dark:border-white/5 dark:bg-white/[0.02]">
|
||||
<p className="text-xs font-medium text-text-main">Getting started</p>
|
||||
<p className="text-xs font-medium text-text-main">
|
||||
{t("providerUtilizationGettingStarted")}
|
||||
</p>
|
||||
<ul className="mt-2 text-left text-xs text-text-muted">
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="material-symbols-outlined text-[14px] text-primary">
|
||||
@@ -369,7 +375,9 @@ export default function ProviderUtilizationTab() {
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-text-main">{point.provider}</p>
|
||||
<p className="text-xs text-text-muted">Latest quota snapshot</p>
|
||||
<p className="text-xs text-text-muted">
|
||||
{t("providerUtilizationLatestSnapshot")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
@@ -390,7 +398,9 @@ export default function ProviderUtilizationTab() {
|
||||
<p className="text-3xl font-bold text-text-main">
|
||||
{point.remainingPct.toFixed(point.remainingPct < 10 ? 1 : 0)}%
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-text-muted">Remaining capacity</p>
|
||||
<p className="mt-1 text-xs text-text-muted">
|
||||
{t("providerUtilizationRemainingCapacity")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right text-xs text-text-muted">
|
||||
<p>{formatTooltipTimestamp(point.timestamp, range)}</p>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface SearchStats {
|
||||
@@ -76,6 +77,7 @@ function ProviderBar({
|
||||
}
|
||||
|
||||
export default function SearchAnalyticsTab() {
|
||||
const t = useTranslations("analytics");
|
||||
const [stats, setStats] = useState<SearchStats | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -122,25 +124,25 @@ export default function SearchAnalyticsTab() {
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<StatCard
|
||||
icon="manage_search"
|
||||
label="Total Searches"
|
||||
label={t("searchAnalyticsTotalSearches")}
|
||||
value={stats.total.toLocaleString()}
|
||||
sub={`${stats.today} today`}
|
||||
/>
|
||||
<StatCard
|
||||
icon="cached"
|
||||
label="Cache Hit Rate"
|
||||
label={t("searchAnalyticsCacheHitRate")}
|
||||
value={`${stats.cacheHitRate}%`}
|
||||
sub={`${stats.cached} cached requests`}
|
||||
/>
|
||||
<StatCard
|
||||
icon="attach_money"
|
||||
label="Total Cost"
|
||||
label={t("searchAnalyticsTotalCost")}
|
||||
value={`$${stats.totalCostUsd.toFixed(4)}`}
|
||||
sub="search API costs"
|
||||
/>
|
||||
<StatCard
|
||||
icon="timer"
|
||||
label="Avg Response"
|
||||
label={t("searchAnalyticsAvgResponse")}
|
||||
value={`${stats.avgDurationMs}ms`}
|
||||
sub={stats.errors > 0 ? `${stats.errors} errors` : "No errors"}
|
||||
/>
|
||||
@@ -173,7 +175,7 @@ export default function SearchAnalyticsTab() {
|
||||
<span className="material-symbols-outlined text-[48px] mb-3 block text-primary opacity-50">
|
||||
travel_explore
|
||||
</span>
|
||||
<p className="font-medium text-text">No searches yet</p>
|
||||
<p className="font-medium text-text">{t("searchAnalyticsNoSearchesYet")}</p>
|
||||
<p className="text-sm mt-1">
|
||||
Use <code className="bg-bg-muted px-1 rounded">POST /v1/search</code> to start routing
|
||||
web searches.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Card } from "@/shared/components";
|
||||
|
||||
@@ -15,6 +16,7 @@ interface DiversityReport {
|
||||
}
|
||||
|
||||
export default function DiversityScoreCard() {
|
||||
const t = useTranslations("analytics");
|
||||
const [data, setData] = useState<DiversityReport | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@@ -82,7 +84,7 @@ export default function DiversityScoreCard() {
|
||||
<div className="flex items-center justify-between gap-3 mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-[20px] text-primary">pie_chart</span>
|
||||
<h3 className="font-semibold text-text-main">Provider Diversity</h3>
|
||||
<h3 className="font-semibold text-text-main">{t("diversityScoreTitle")}</h3>
|
||||
<span className="text-xs text-text-muted hidden sm:inline">
|
||||
— Provider concentration snapshot for the recent traffic window.
|
||||
</span>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
function relativeTime(ts: number): string {
|
||||
const diffMs = Date.now() - ts * 1000;
|
||||
@@ -135,6 +136,7 @@ function formatTs(ts: number | null | undefined): string {
|
||||
}
|
||||
|
||||
export default function BatchDetailModal({ batch, files, onClose }: BatchDetailModalProps) {
|
||||
const t = useTranslations("common");
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") onClose();
|
||||
@@ -182,7 +184,7 @@ export default function BatchDetailModal({ batch, files, onClose }: BatchDetailM
|
||||
navigator.clipboard.writeText(batch.id);
|
||||
}}
|
||||
className="text-[var(--color-text-muted)] hover:text-[var(--color-text-main)] transition-colors"
|
||||
title="Copy ID"
|
||||
title={t("batchDetailCopyId")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[12px]">content_copy</span>
|
||||
</button>
|
||||
@@ -191,7 +193,7 @@ export default function BatchDetailModal({ batch, files, onClose }: BatchDetailM
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
aria-label="Close"
|
||||
aria-label={t("batchDetailClose")}
|
||||
className="p-1.5 rounded-lg text-[var(--color-text-muted)] hover:bg-[var(--color-bg-alt)] transition-colors"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[20px]">close</span>
|
||||
@@ -208,11 +210,11 @@ export default function BatchDetailModal({ batch, files, onClose }: BatchDetailM
|
||||
</span>
|
||||
<StatusBadge batch={batch} />
|
||||
</div>
|
||||
<Field label="Endpoint" value={batch.endpoint} />
|
||||
{batch.model && <Field label="Model" value={batch.model} />}
|
||||
<Field label="Window" value={batch.completionWindow} />
|
||||
<Field label={t("batchDetailEndpoint")} value={batch.endpoint} />
|
||||
{batch.model && <Field label={t("batchDetailModel")} value={batch.model} />}
|
||||
<Field label={t("batchDetailWindow")} value={batch.completionWindow} />
|
||||
<Field
|
||||
label="Created"
|
||||
label={t("batchDetailCreated")}
|
||||
value={<span title={formatTs(batch.createdAt)}>{relativeTime(batch.createdAt)}</span>}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import BatchDetailModal from "./BatchDetailModal";
|
||||
|
||||
function relativeTime(ts: number): string {
|
||||
@@ -131,6 +132,7 @@ export default function BatchListTab({
|
||||
loading,
|
||||
onRefresh,
|
||||
}: Readonly<BatchListTabProps>) {
|
||||
const t = useTranslations("common");
|
||||
const [selectedBatch, setSelectedBatch] = useState<BatchRecord | null>(null);
|
||||
const [statusFilter, setStatusFilter] = useState("all");
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
@@ -199,7 +201,7 @@ export default function BatchListTab({
|
||||
<div className="flex flex-wrap gap-3 p-4 rounded-xl bg-[var(--color-surface)] border border-[var(--color-border)]">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by ID, endpoint, model…"
|
||||
placeholder={t("batchListSearchPlaceholder")}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="flex-1 min-w-[200px] 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)]"
|
||||
@@ -219,7 +221,7 @@ export default function BatchListTab({
|
||||
onClick={handleRemoveCompleted}
|
||||
disabled={removingCompleted}
|
||||
className="flex items-center gap-1.5 px-3 py-2 text-sm rounded-lg bg-red-500/10 border border-red-500/25 text-red-400 hover:text-red-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap"
|
||||
title="Delete all completed batches"
|
||||
title={t("batchListDeleteAllCompletedTitle")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">
|
||||
{removingCompleted ? "hourglass_empty" : "delete_sweep"}
|
||||
@@ -230,7 +232,7 @@ export default function BatchListTab({
|
||||
|
||||
{/* Table */}
|
||||
<div className="overflow-x-auto overflow-y-hidden rounded-xl border border-[var(--color-border)]">
|
||||
<table className="w-full text-sm" role="table" aria-label="Batches">
|
||||
<table className="w-full text-sm" role="table" aria-label={t("batchListBatchesTable")}>
|
||||
<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)] uppercase text-xs tracking-wider">
|
||||
@@ -338,7 +340,7 @@ export default function BatchListTab({
|
||||
onClick={(e) => handleDeleteBatch(e, batch)}
|
||||
disabled={deletingId === batch.id}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs rounded bg-red-500/10 border border-red-500/25 text-red-400 hover:text-red-300 transition-colors whitespace-nowrap disabled:opacity-50"
|
||||
title="Delete batch and its files"
|
||||
title={t("batchListDeleteBatchTitle")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[13px]">
|
||||
{deletingId === batch.id ? "hourglass_empty" : "delete"}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Button } from "@/shared/components";
|
||||
|
||||
function relativeTime(ts: number): string {
|
||||
@@ -69,6 +70,7 @@ export default function FileDetailModal({
|
||||
batches,
|
||||
onClose,
|
||||
}: Readonly<FileDetailModalProps>) {
|
||||
const t = useTranslations("common");
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const relatedBatches = (batches ?? []).filter(
|
||||
@@ -140,7 +142,7 @@ export default function FileDetailModal({
|
||||
navigator.clipboard.writeText(file.id);
|
||||
}}
|
||||
className="text-[var(--color-text-muted)] hover:text-[var(--color-text-main)] transition-colors"
|
||||
title="Copy ID"
|
||||
title={t("batchFileDetailCopyId")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[12px]">content_copy</span>
|
||||
</button>
|
||||
@@ -149,7 +151,7 @@ export default function FileDetailModal({
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
aria-label="Close"
|
||||
aria-label={t("batchFileDetailClose")}
|
||||
className="p-1.5 rounded-lg text-[var(--color-text-muted)] hover:bg-[var(--color-bg-alt)] transition-colors"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[20px]">close</span>
|
||||
@@ -266,7 +268,7 @@ export default function FileDetailModal({
|
||||
<span className="material-symbols-outlined text-[40px] mb-2 opacity-20">
|
||||
find_in_page
|
||||
</span>
|
||||
<p className="text-sm">Failed to load file contents</p>
|
||||
<p className="text-sm">{t("batchFileDetailFailedToLoad")}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import FileDetailModal from "./FileDetailModal";
|
||||
|
||||
function relativeTime(ts: number): string {
|
||||
@@ -84,6 +85,7 @@ export default function FilesListTab({
|
||||
onRefresh,
|
||||
batches,
|
||||
}: Readonly<FilesListTabProps>) {
|
||||
const t = useTranslations("common");
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [purposeFilter, setPurposeFilter] = useState("all");
|
||||
const [selectedFileId, setSelectedFileId] = useState<string | null>(null);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Card } from "@/shared/components";
|
||||
|
||||
interface CachePerformanceProps {
|
||||
@@ -68,6 +69,7 @@ export default function CachePerformance({
|
||||
onRetry,
|
||||
stats,
|
||||
}: CachePerformanceProps) {
|
||||
const t = useTranslations("cache");
|
||||
// Parse hitRate string (e.g. "85.0%") to number for the bar
|
||||
const hitRateNum = hitRate ? parseFloat(hitRate) : 0;
|
||||
|
||||
@@ -87,7 +89,7 @@ export default function CachePerformance({
|
||||
<button
|
||||
onClick={onRetry}
|
||||
className="self-start text-xs px-3 py-1.5 rounded bg-surface border border-border/50 hover:bg-surface/80 transition-colors"
|
||||
aria-label="Retry"
|
||||
aria-label={t("cachePerformanceRetry")}
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
@@ -117,7 +119,9 @@ export default function CachePerformance({
|
||||
{!loading && !error && stats !== null && (
|
||||
<>
|
||||
{/* Hit rate bar */}
|
||||
{hitRate !== undefined && <HitRateBar hitRate={hitRateNum} label="Hit Rate" />}
|
||||
{hitRate !== undefined && (
|
||||
<HitRateBar hitRate={hitRateNum} label={t("cachePerformanceHitRate")} />
|
||||
)}
|
||||
|
||||
{/* Hit / Miss / Total breakdown */}
|
||||
<div className="grid grid-cols-3 gap-4 pt-3 border-t border-border/30 text-center">
|
||||
@@ -141,13 +145,17 @@ export default function CachePerformance({
|
||||
{avgLatencyMs !== undefined && (
|
||||
<div>
|
||||
<div className="text-lg font-semibold tabular-nums">{avgLatencyMs}</div>
|
||||
<div className="text-xs text-text-muted mt-0.5">Avg Latency (ms)</div>
|
||||
<div className="text-xs text-text-muted mt-0.5">
|
||||
{t("cachePerformanceAvgLatency")}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{p95LatencyMs !== undefined && (
|
||||
<div>
|
||||
<div className="text-lg font-semibold tabular-nums">{p95LatencyMs}</div>
|
||||
<div className="text-xs text-text-muted mt-0.5">P95 Latency (ms)</div>
|
||||
<div className="text-xs text-text-muted mt-0.5">
|
||||
{t("cachePerformanceP95Latency")}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import ReactMarkdown, { type Components } from "react-markdown";
|
||||
import { Button } from "@/shared/components";
|
||||
import {
|
||||
@@ -80,6 +81,7 @@ const markdownComponents: Components = {
|
||||
};
|
||||
|
||||
export default function ChangelogViewer() {
|
||||
const t = useTranslations("common");
|
||||
const [markdown, setMarkdown] = useState("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(false);
|
||||
@@ -109,7 +111,7 @@ export default function ChangelogViewer() {
|
||||
<span className="material-symbols-outlined animate-spin text-[32px] text-text-muted/50">
|
||||
sync
|
||||
</span>
|
||||
<p className="text-sm text-text-muted">Loading changelog from GitHub...</p>
|
||||
<p className="text-sm text-text-muted">{t("changelogViewerLoading")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -649,8 +649,8 @@ openai_base_url = "${getEffectiveBaseUrl()}"
|
||||
onChange={(e) => setWireApi(e.target.value)}
|
||||
className="flex-1 px-2 py-1.5 bg-surface rounded text-xs border border-border focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
>
|
||||
<option value="chat">Chat Completions (/chat/completions)</option>
|
||||
<option value="responses">Responses API (/responses)</option>
|
||||
<option value="chat">{t("wireApiChatCompletions")}</option>
|
||||
<option value="responses">{t("wireApiResponses")}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -287,7 +287,9 @@ export default function IntelligentComboPanel({
|
||||
|
||||
<div className="mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<div className="rounded-lg border border-black/8 bg-white/60 p-3 dark:border-white/8 dark:bg-white/[0.03]">
|
||||
<p className="text-[11px] uppercase tracking-wide text-text-muted">Mode Pack</p>
|
||||
<p className="text-[11px] uppercase tracking-wide text-text-muted">
|
||||
{t("modePack")}
|
||||
</p>
|
||||
<p className="mt-1 text-sm font-semibold text-text-main">
|
||||
{normalizedConfig.modePack}
|
||||
</p>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface BadgeUnlockEvent {
|
||||
badgeId: string;
|
||||
@@ -20,6 +21,7 @@ const RECONNECT_BASE_MS = 1000;
|
||||
const RECONNECT_MAX_MS = 30000;
|
||||
|
||||
export function BadgeToast({ apiKeyId }: { apiKeyId: string }) {
|
||||
const t = useTranslations("common");
|
||||
const [toasts, setToasts] = useState<BadgeUnlockEvent[]>([]);
|
||||
const timeoutIds = useRef<Set<ReturnType<typeof setTimeout>>>(new Set());
|
||||
|
||||
@@ -92,7 +94,7 @@ export function BadgeToast({ apiKeyId }: { apiKeyId: string }) {
|
||||
>
|
||||
<span className="text-2xl">🏆</span>
|
||||
<div>
|
||||
<div className="font-semibold text-white">Badge Unlocked!</div>
|
||||
<div className="font-semibold text-white">{t("badgeToastUnlocked")}</div>
|
||||
<div className="text-sm text-text-muted">{toast.badgeName}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -339,11 +339,8 @@ export default function QuotaSharePageClient() {
|
||||
<div className="rounded-lg border border-amber-500/30 bg-amber-500/10 px-4 py-3 text-xs text-amber-700 dark:text-amber-300 flex items-start gap-2">
|
||||
<span className="material-symbols-outlined text-[18px] shrink-0">science</span>
|
||||
<div>
|
||||
<strong>Beta — UI preview.</strong> A configuração é salva em <code>localStorage</code>{" "}
|
||||
(não persiste no servidor ainda). A aplicação dos caps por request ainda não está
|
||||
conectada ao pipeline da proxy. Esta tela permite desenhar e visualizar a divisão de cota;
|
||||
a aplicação real virá em uma próxima iteração com persistência no banco e interceptação na
|
||||
chamada upstream.
|
||||
<strong>{t("betaPreviewLabel")}</strong> {t("betaConfigSavedPrefix")}{" "}
|
||||
<code>localStorage</code> {t("betaConfigSavedSuffix")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -598,7 +595,9 @@ function PoolCard({
|
||||
|
||||
<div className="mt-3 flex items-center justify-between gap-2 flex-wrap text-[11px]">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-text-muted font-semibold uppercase tracking-wide">Policy:</span>
|
||||
<span className="text-text-muted font-semibold uppercase tracking-wide">
|
||||
{t("policyLabel")}
|
||||
</span>
|
||||
{(["hard", "soft", "burst"] as PoolPolicy[]).map((p) => (
|
||||
<button
|
||||
key={p}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Card } from "@/shared/components";
|
||||
import { useDisplayBaseUrl } from "@/shared/hooks";
|
||||
|
||||
@@ -44,6 +45,7 @@ const METHOD_COLORS: Record<string, string> = {
|
||||
|
||||
/* ─── Main Component ─────────────────────────────────── */
|
||||
export default function ApiEndpointsTab() {
|
||||
const t = useTranslations("endpoint");
|
||||
const baseUrl = useDisplayBaseUrl();
|
||||
const [catalog, setCatalog] = useState<CatalogData | null>(null);
|
||||
const [catalogError, setCatalogError] = useState<string | null>(null);
|
||||
@@ -224,7 +226,9 @@ export default function ApiEndpointsTab() {
|
||||
<span className="material-symbols-outlined text-[20px] text-red-500">error</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-text-main">API catalog unavailable</h3>
|
||||
<h3 className="text-sm font-semibold text-text-main">
|
||||
{t("apiEndpointsCatalogUnavailable")}
|
||||
</h3>
|
||||
<p className="text-xs text-text-muted mt-1">
|
||||
{catalogError || "The OpenAPI specification could not be loaded."}
|
||||
</p>
|
||||
@@ -254,7 +258,7 @@ export default function ApiEndpointsTab() {
|
||||
<input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Search endpoints..."
|
||||
placeholder={t("apiEndpointsSearchPlaceholder")}
|
||||
className="w-full pl-9 pr-3 py-2 text-xs rounded-lg border border-black/10 dark:border-white/10
|
||||
bg-white dark:bg-black/20 focus:outline-none focus:ring-1 focus:ring-primary"
|
||||
/>
|
||||
@@ -334,7 +338,7 @@ export default function ApiEndpointsTab() {
|
||||
{ep.security && (
|
||||
<span
|
||||
className="material-symbols-outlined text-[12px] text-amber-500"
|
||||
title="Requires auth"
|
||||
title={t("apiEndpointsRequiresAuth")}
|
||||
>
|
||||
lock
|
||||
</span>
|
||||
@@ -482,7 +486,7 @@ export default function ApiEndpointsTab() {
|
||||
<span className="material-symbols-outlined text-[32px] text-text-muted">
|
||||
search_off
|
||||
</span>
|
||||
<p className="text-sm text-text-muted mt-2">No endpoints match your filter</p>
|
||||
<p className="text-sm text-text-muted mt-2">{t("apiEndpointsNoMatch")}</p>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1281,7 +1281,7 @@ export default function APIPageClient({ machineId }: Readonly<APIPageClientProps
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-baseline gap-1 flex-wrap">
|
||||
<span className="text-sm font-medium">Local Server</span>
|
||||
<span className="text-sm font-medium">{t("localServer")}</span>
|
||||
{resolvedMachineId && (
|
||||
<span className="text-xs text-text-muted">· {resolvedMachineId.slice(0, 8)}</span>
|
||||
)}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Card, Toggle } from "@/shared/components";
|
||||
import { useNotificationStore } from "@/store/notificationStore";
|
||||
|
||||
@@ -111,6 +112,7 @@ function EngineRow({
|
||||
}
|
||||
|
||||
export default function TokenSaverCard() {
|
||||
const t = useTranslations("endpoint");
|
||||
const notify = useNotificationStore();
|
||||
const [config, setConfig] = useState<CompressionConfig | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -182,14 +184,14 @@ export default function TokenSaverCard() {
|
||||
</span>
|
||||
)}
|
||||
</h2>
|
||||
<p className="text-sm text-text-muted mt-1">Spend less tokens on every request.</p>
|
||||
<p className="text-sm text-text-muted mt-1">{t("tokenSaverSubtitle")}</p>
|
||||
</div>
|
||||
<Toggle size="md" checked={masterEnabled} onChange={(v) => save({ enabled: v })} />
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-border mt-4">
|
||||
<EngineRow
|
||||
title="Tool output"
|
||||
title={t("tokenSaverToolOutput")}
|
||||
badge="RTK"
|
||||
href="/dashboard/context/rtk"
|
||||
description="git/grep/ls/tree/logs cleaner → 60-90% fewer input tokens"
|
||||
@@ -206,7 +208,7 @@ export default function TokenSaverCard() {
|
||||
}
|
||||
/>
|
||||
<EngineRow
|
||||
title="LLM output"
|
||||
title={t("tokenSaverLlmOutput")}
|
||||
badge="Caveman"
|
||||
href="/dashboard/context/caveman"
|
||||
description="Terse-style system prompt → ~65% fewer output tokens (up to 87%)"
|
||||
@@ -223,7 +225,7 @@ export default function TokenSaverCard() {
|
||||
}
|
||||
/>
|
||||
<EngineRow
|
||||
title="Input compression"
|
||||
title={t("tokenSaverInputCompression")}
|
||||
badge="Caveman"
|
||||
href="/dashboard/context/caveman"
|
||||
description="Rewrite chat history → ~50% fewer input tokens"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Card } from "@/shared/components";
|
||||
|
||||
type LeaderboardScope = "global" | "weekly" | "monthly" | "tokens_shared";
|
||||
@@ -26,6 +27,7 @@ const MEDAL_COLORS = [
|
||||
const MEDAL_EMOJI = ["🥇", "🥈", "🥉"];
|
||||
|
||||
export default function LeaderboardPage() {
|
||||
const t = useTranslations("common");
|
||||
const [scope, setScope] = useState<LeaderboardScope>("global");
|
||||
const [entries, setEntries] = useState<LeaderboardEntry[]>([]);
|
||||
const [myRank, setMyRank] = useState<number | null>(null);
|
||||
@@ -110,7 +112,7 @@ export default function LeaderboardPage() {
|
||||
<Card>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-text-muted">Your Rank</p>
|
||||
<p className="text-sm text-text-muted">{t("leaderboardYourRank")}</p>
|
||||
<p className="text-3xl font-bold mt-1">#{myRank}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
@@ -125,7 +127,7 @@ export default function LeaderboardPage() {
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center min-h-[200px]">
|
||||
<div className="text-text-muted">Loading leaderboard...</div>
|
||||
<div className="text-text-muted">{t("leaderboardLoading")}</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Card } from "@/shared/components";
|
||||
import { copyToClipboard } from "@/shared/utils/clipboard";
|
||||
import McpDashboardPage from "../endpoint/components/MCPDashboard";
|
||||
@@ -98,6 +99,7 @@ function TransportSelector({
|
||||
disabled: boolean;
|
||||
baseUrl: string;
|
||||
}) {
|
||||
const t = useTranslations("mcpDashboard");
|
||||
const options: { value: McpTransport; label: string; desc: string }[] = [
|
||||
{ value: "stdio", label: "stdio", desc: "Local — IDE spawns process via omniroute --mcp" },
|
||||
{ value: "sse", label: "SSE", desc: "Remote — Server-Sent Events over HTTP" },
|
||||
@@ -181,7 +183,7 @@ function TransportSelector({
|
||||
className="ml-auto text-xs px-2 py-0.5 rounded border hover:opacity-80 transition-opacity"
|
||||
style={{ borderColor: "var(--color-border)", color: "var(--color-text-muted)" }}
|
||||
onClick={() => void copyToClipboard(urlMap[value])}
|
||||
title="Copy URL"
|
||||
title={t("mcpDashboardCopyUrl")}
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Image from "next/image";
|
||||
|
||||
export function TierFlowDiagram() {
|
||||
const t = useTranslations("onboarding");
|
||||
const { resolvedTheme } = useTheme();
|
||||
const src =
|
||||
resolvedTheme === "dark" ? "/images/tier-flow-dark.svg" : "/images/tier-flow-light.svg";
|
||||
@@ -12,7 +14,7 @@ export function TierFlowDiagram() {
|
||||
<div className="flex flex-col items-center gap-3 my-4">
|
||||
<Image
|
||||
src={src}
|
||||
alt="OmniRoute 3-tier fallback diagram"
|
||||
alt={t("tierFlowDiagramAlt")}
|
||||
width={800}
|
||||
height={420}
|
||||
priority
|
||||
|
||||
@@ -222,7 +222,7 @@ export default function ChatPlayground({
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-[18px] text-text-muted">chat</span>
|
||||
<h3 className="text-sm font-semibold text-text-main">Conversational Chat</h3>
|
||||
<h3 className="text-sm font-semibold text-text-main">{t("conversationalChat")}</h3>
|
||||
{responseStatus !== null && (
|
||||
<Badge variant={responseStatus < 400 ? "success" : "error"} size="sm">
|
||||
{responseStatus}
|
||||
@@ -235,7 +235,7 @@ export default function ChatPlayground({
|
||||
<button
|
||||
onClick={handleClear}
|
||||
className="p-1.5 rounded hover:bg-red-500/10 text-text-muted hover:text-red-500 transition-colors"
|
||||
title="Clear chat"
|
||||
title={t("clearChat")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">delete</span>
|
||||
</button>
|
||||
@@ -292,7 +292,7 @@ export default function ChatPlayground({
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Type a message... (Shift+Enter for new line)"
|
||||
placeholder={t("typeMessagePlaceholder")}
|
||||
className="flex-1 min-h-[44px] max-h-[120px] bg-surface border border-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary resize-y"
|
||||
rows={1}
|
||||
disabled={loading || noModels}
|
||||
|
||||
@@ -175,7 +175,7 @@ export default function SearchPlayground() {
|
||||
<button
|
||||
onClick={() => navigator.clipboard.writeText(requestBody)}
|
||||
className="p-1.5 rounded hover:bg-black/5 dark:hover:bg-white/5 text-text-muted hover:text-text-main transition-colors"
|
||||
title="Copy"
|
||||
title={t("copy")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">content_copy</span>
|
||||
</button>
|
||||
@@ -194,7 +194,7 @@ export default function SearchPlayground() {
|
||||
)
|
||||
}
|
||||
className="p-1.5 rounded hover:bg-black/5 dark:hover:bg-white/5 text-text-muted hover:text-text-main transition-colors"
|
||||
title="Reset to default"
|
||||
title={t("resetToDefault")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">restart_alt</span>
|
||||
</button>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Card, Badge } from "@/shared/components";
|
||||
import {
|
||||
xpForLevel,
|
||||
@@ -57,6 +58,7 @@ const RARITY_COLORS: Record<string, string> = {
|
||||
};
|
||||
|
||||
export default function ProfilePage() {
|
||||
const t = useTranslations("common");
|
||||
const [userLevel, setUserLevel] = useState<UserLevel | null>(null);
|
||||
const [allBadges, setAllBadges] = useState<BadgeDef[]>([]);
|
||||
const [earnedBadges, setEarnedBadges] = useState<UserBadge[]>([]);
|
||||
@@ -99,7 +101,7 @@ export default function ProfilePage() {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-text-muted">Loading profile...</div>
|
||||
<div className="text-text-muted">{t("profileLoading")}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -269,7 +271,7 @@ export default function ProfilePage() {
|
||||
|
||||
{selectedBadge.criteria && (
|
||||
<div className="p-3 rounded-lg bg-surface/50 border border-border/50">
|
||||
<p className="text-xs font-medium text-text-muted mb-1">How to earn</p>
|
||||
<p className="text-xs font-medium text-text-muted mb-1">{t("profileHowToEarn")}</p>
|
||||
<p className="text-sm">{selectedBadge.criteria}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Card, Button, Input, Toggle } from "@/shared/components";
|
||||
|
||||
interface Settings {
|
||||
@@ -28,6 +29,7 @@ function isValidUrl(value: string): boolean {
|
||||
}
|
||||
|
||||
export default function CliproxyapiSettingsTab() {
|
||||
const t = useTranslations("settings");
|
||||
const [settings, setSettings] = useState<Settings>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
@@ -139,7 +141,7 @@ export default function CliproxyapiSettingsTab() {
|
||||
<span className="material-symbols-outlined text-indigo-500 text-xl">swap_horiz</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-sm">CLIProxyAPI Fallback</h3>
|
||||
<h3 className="font-medium text-sm">{t("cliproxyapiFallback")}</h3>
|
||||
<p className="text-xs text-text-muted">
|
||||
When enabled, failed requests are retried through CLIProxyAPI (localhost:8317)
|
||||
</p>
|
||||
@@ -148,7 +150,7 @@ export default function CliproxyapiSettingsTab() {
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm text-text-main">Enable CLIProxyAPI Fallback</label>
|
||||
<label className="text-sm text-text-main">{t("cliproxyapiEnableFallback")}</label>
|
||||
<Toggle
|
||||
checked={cpaEnabled}
|
||||
onChange={(checked) => updateSetting("cliproxyapi_fallback_enabled", checked)}
|
||||
@@ -158,7 +160,9 @@ export default function CliproxyapiSettingsTab() {
|
||||
{cpaEnabled && (
|
||||
<>
|
||||
<div>
|
||||
<label className="text-xs text-text-muted mb-1.5 block">CLIProxyAPI URL</label>
|
||||
<label className="text-xs text-text-muted mb-1.5 block">
|
||||
{t("cliproxyapiUrl")}
|
||||
</label>
|
||||
<Input
|
||||
value={cpaUrl}
|
||||
onChange={(e) => updateSetting("cliproxyapi_url", e.target.value)}
|
||||
@@ -184,7 +188,7 @@ export default function CliproxyapiSettingsTab() {
|
||||
</Card>
|
||||
|
||||
<Card padding="md">
|
||||
<h3 className="font-medium text-sm mb-4">CLIProxyAPI Status</h3>
|
||||
<h3 className="font-medium text-sm mb-4">{t("cliproxyapiStatus")}</h3>
|
||||
{loading ? (
|
||||
<div className="flex items-center gap-2 text-text-muted text-sm">
|
||||
<span className="material-symbols-outlined animate-spin text-base">
|
||||
@@ -237,7 +241,7 @@ export default function CliproxyapiSettingsTab() {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-text-muted">CLIProxyAPI not detected</p>
|
||||
<p className="text-sm text-text-muted">{t("cliproxyapiNotDetected")}</p>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Button, Card } from "@/shared/components";
|
||||
import { useNotificationStore } from "@/store/notificationStore";
|
||||
|
||||
@@ -20,6 +21,7 @@ function formatRemaining(ms: number): string {
|
||||
}
|
||||
|
||||
export default function ModelCooldownsCard() {
|
||||
const t = useTranslations("settings");
|
||||
const notify = useNotificationStore();
|
||||
const [items, setItems] = useState<CooldownItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -95,7 +97,7 @@ export default function ModelCooldownsCard() {
|
||||
<Card className="p-6">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-text-main">Models in cooldown</h2>
|
||||
<h2 className="text-lg font-bold text-text-main">{t("modelCooldownsTitle")}</h2>
|
||||
<p className="mt-1 text-sm text-text-muted">
|
||||
Models temporarily isolated after a failure. When the cooldown expires they come back
|
||||
automatically.
|
||||
@@ -120,7 +122,7 @@ export default function ModelCooldownsCard() {
|
||||
{loading ? (
|
||||
<p className="text-sm text-text-muted">Loading...</p>
|
||||
) : !hasItems ? (
|
||||
<p className="text-sm text-text-muted">No models in cooldown right now.</p>
|
||||
<p className="text-sm text-text-muted">{t("modelCooldownsEmpty")}</p>
|
||||
) : (
|
||||
sorted.map((item) => {
|
||||
const rowKey = `${item.provider}::${item.model}`;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Card, Button } from "@/shared/components";
|
||||
|
||||
const EMPTY_PAYLOAD_RULES_TEMPLATE = {
|
||||
@@ -55,6 +56,7 @@ function getErrorMessage(payload: unknown): string {
|
||||
}
|
||||
|
||||
export default function PayloadRulesTab() {
|
||||
const t = useTranslations("settings");
|
||||
const [editorValue, setEditorValue] = useState(EMPTY_PAYLOAD_RULES_TEXT);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
@@ -162,7 +164,7 @@ export default function PayloadRulesTab() {
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold">Payload Rules</h3>
|
||||
<h3 className="text-lg font-semibold">{t("payloadRulesTitle")}</h3>
|
||||
<p className="text-sm text-text-muted mt-1">
|
||||
Configure request payload mutations by model and protocol. Changes are persisted in
|
||||
settings and hot reloaded into the runtime immediately after save.
|
||||
|
||||
@@ -874,7 +874,7 @@ export default function ProxyRegistryManager() {
|
||||
value={bulkProxyId}
|
||||
onChange={(e) => setBulkProxyId(e.target.value)}
|
||||
>
|
||||
<option value="">(clear assignment)</option>
|
||||
<option value="">{t("clearAssignment")}</option>
|
||||
{items.map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.name} ({item.type}://{item.host}:{item.port})
|
||||
|
||||
@@ -372,7 +372,7 @@ export default function SkillsPage() {
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
placeholder="Filter skills by name, description, or tag"
|
||||
placeholder={t("filterSkillsPlaceholder")}
|
||||
className="px-3 py-2 rounded-lg bg-background border border-border text-sm focus:outline-none focus:ring-1 focus:ring-violet-500"
|
||||
/>
|
||||
<select
|
||||
@@ -380,7 +380,7 @@ export default function SkillsPage() {
|
||||
onChange={(e) => setModeFilter(e.target.value as "all" | "on" | "off" | "auto")}
|
||||
className="px-3 py-2 rounded-lg bg-background border border-border text-sm focus:outline-none focus:ring-1 focus:ring-violet-500"
|
||||
>
|
||||
<option value="all">All modes</option>
|
||||
<option value="all">{t("allModes")}</option>
|
||||
<option value="on">On</option>
|
||||
<option value="auto">Auto</option>
|
||||
<option value="off">Off</option>
|
||||
@@ -659,7 +659,7 @@ export default function SkillsPage() {
|
||||
{activeTab === "marketplace" && (
|
||||
<div className="grid gap-4">
|
||||
<Card>
|
||||
<h3 className="font-semibold mb-2">Skills Marketplace</h3>
|
||||
<h3 className="font-semibold mb-2">{t("skillsMarketplace")}</h3>
|
||||
<p className="text-sm text-text-muted mb-4">
|
||||
Active provider:{" "}
|
||||
<span className="font-medium">
|
||||
@@ -775,7 +775,7 @@ export default function SkillsPage() {
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||
<div className="bg-surface border border-border rounded-xl p-6 w-full max-w-lg mx-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold">Install Skill</h2>
|
||||
<h2 className="text-lg font-semibold">{t("installSkill")}</h2>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowInstallModal(false);
|
||||
|
||||
@@ -573,7 +573,7 @@ export default function PlaygroundMode() {
|
||||
</div>
|
||||
{compressionResult.techniquesUsed.length > 0 && (
|
||||
<div className="text-xs text-text-muted">
|
||||
<span className="font-semibold">Techniques:</span>{" "}
|
||||
<span className="font-semibold">{t("techniques")}</span>{" "}
|
||||
{compressionResult.techniquesUsed.join(", ")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -892,7 +892,7 @@ function BudgetRowExpanded({
|
||||
{t("budgetLoading")}
|
||||
</div>
|
||||
) : breakdown.length === 0 ? (
|
||||
<div className="text-[11px] text-text-muted py-2">No spend in last 30 days</div>
|
||||
<div className="text-[11px] text-text-muted py-2">{t("noSpendLast30Days")}</div>
|
||||
) : (
|
||||
<div className="space-y-1.5">
|
||||
{breakdown.slice(0, 5).map((b) => (
|
||||
|
||||
@@ -52,11 +52,11 @@ export default function BudgetTelemetryCards() {
|
||||
<span className="font-mono">{telemetry.totalRequests ?? 0}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-muted">Active sessions</span>
|
||||
<span className="text-text-muted">{t("activeSessions")}</span>
|
||||
<span className="font-mono">{telemetry.sessions?.activeCount ?? 0}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-muted">Quota alerts</span>
|
||||
<span className="text-text-muted">{t("quotaAlerts")}</span>
|
||||
<span className="font-mono">{telemetry.quotaMonitor?.alerting ?? 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -142,7 +142,7 @@ export default function QuotaTable({ quotas = [] }) {
|
||||
{/* Reset Time */}
|
||||
<td className="py-2 px-3">
|
||||
{staleAfterReset ? (
|
||||
<div className="text-xs text-text-muted">⟳ Refreshing...</div>
|
||||
<div className="text-xs text-text-muted">{t("quotaTableRefreshing")}</div>
|
||||
) : countdown !== t("notAvailableSymbol") || resetDisplay ? (
|
||||
<div className="space-y-0.5">
|
||||
{countdown !== t("notAvailableSymbol") && (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import {
|
||||
ReactFlow,
|
||||
Handle,
|
||||
@@ -280,6 +281,7 @@ export default function ProviderTopology({
|
||||
lastProvider = "",
|
||||
errorProvider = "",
|
||||
}: Props) {
|
||||
const t = useTranslations("common");
|
||||
const activeKey = useMemo(
|
||||
() =>
|
||||
activeRequests
|
||||
@@ -377,7 +379,7 @@ export default function ProviderTopology({
|
||||
{providers.length === 0 ? (
|
||||
<div className="h-full flex flex-col items-center justify-center gap-2 text-text-muted">
|
||||
<span className="material-symbols-outlined text-[32px]">device_hub</span>
|
||||
<p className="text-sm">No providers connected yet</p>
|
||||
<p className="text-sm">{t("providerTopologyEmpty")}</p>
|
||||
</div>
|
||||
) : (
|
||||
<ReactFlow
|
||||
|
||||
@@ -679,7 +679,30 @@
|
||||
"tokensMaxUses": "Max Uses",
|
||||
"tokensRedeemCode": "Redeem Code",
|
||||
"tokensRedeemCodePlaceholder": "Enter invite code",
|
||||
"tokensYourActiveInvites": "Your Active Invites"
|
||||
"tokensYourActiveInvites": "Your Active Invites",
|
||||
"tierCoverageTitle": "Tier coverage",
|
||||
"tierCoverageSubtitle": "Providers configured per fallback tier",
|
||||
"batchDetailCopyId": "Copy ID",
|
||||
"batchDetailClose": "Close",
|
||||
"batchDetailEndpoint": "Endpoint",
|
||||
"batchDetailModel": "Model",
|
||||
"batchDetailWindow": "Window",
|
||||
"batchDetailCreated": "Created",
|
||||
"providerTopologyEmpty": "No providers connected yet",
|
||||
"badgeToastUnlocked": "Badge Unlocked!",
|
||||
"batchListSearchPlaceholder": "Search by ID, endpoint, model…",
|
||||
"batchListDeleteAllCompletedTitle": "Delete all completed batches",
|
||||
"batchListBatchesTable": "Batches",
|
||||
"changelogViewerLoading": "Loading changelog from GitHub...",
|
||||
"profileLoading": "Loading profile...",
|
||||
"profileHowToEarn": "How to earn",
|
||||
"bootstrapBannerDismiss": "Dismiss",
|
||||
"batchListDeleteBatchTitle": "Delete batch and its files",
|
||||
"leaderboardYourRank": "Your Rank",
|
||||
"leaderboardLoading": "Loading leaderboard...",
|
||||
"batchFileDetailCopyId": "Copy ID",
|
||||
"batchFileDetailClose": "Close",
|
||||
"batchFileDetailFailedToLoad": "Failed to load file contents"
|
||||
},
|
||||
"sidebar": {
|
||||
"home": "Home",
|
||||
@@ -1177,7 +1200,19 @@
|
||||
"compressionAnalyticsCompletionTokens": "Completion tokens",
|
||||
"compressionAnalyticsTotalTokens": "Total tokens",
|
||||
"compressionAnalyticsCacheTokens": "Cache tokens",
|
||||
"compressionAnalyticsNoDataYet": "No compression data yet"
|
||||
"compressionAnalyticsNoDataYet": "No compression data yet",
|
||||
"searchAnalyticsTotalSearches": "Total Searches",
|
||||
"searchAnalyticsCacheHitRate": "Cache Hit Rate",
|
||||
"searchAnalyticsTotalCost": "Total Cost",
|
||||
"searchAnalyticsAvgResponse": "Avg Response",
|
||||
"searchAnalyticsNoSearchesYet": "No searches yet",
|
||||
"providerUtilizationTitle": "Provider utilization",
|
||||
"providerUtilizationFailedToLoad": "Failed to load utilization data",
|
||||
"providerUtilizationNoData": "No utilization data available",
|
||||
"providerUtilizationGettingStarted": "Getting started",
|
||||
"providerUtilizationLatestSnapshot": "Latest quota snapshot",
|
||||
"providerUtilizationRemainingCapacity": "Remaining capacity",
|
||||
"diversityScoreTitle": "Provider Diversity"
|
||||
},
|
||||
"apiManager": {
|
||||
"title": "API Keys",
|
||||
@@ -1390,7 +1425,9 @@
|
||||
"rerank": "Rerank",
|
||||
"rerankModel": "Rerank Model",
|
||||
"positionDelta": "Position Change",
|
||||
"emptyState": "Send a search query to see results"
|
||||
"emptyState": "Send a search query to see results",
|
||||
"copy": "Copy",
|
||||
"resetToDefault": "Reset to default"
|
||||
},
|
||||
"cliTools": {
|
||||
"title": "CLI Tools",
|
||||
@@ -1736,7 +1773,9 @@
|
||||
"copilotMaxInputTokens": "Max Input Tokens",
|
||||
"copilotMaxOutputTokens": "Max Output Tokens",
|
||||
"copilotToolCalling": "Tool Calling",
|
||||
"copilotPasteInto": "Paste into: "
|
||||
"copilotPasteInto": "Paste into: ",
|
||||
"wireApiChatCompletions": "Chat Completions (/chat/completions)",
|
||||
"wireApiResponses": "Responses API (/responses)"
|
||||
},
|
||||
"combos": {
|
||||
"title": "Combos",
|
||||
@@ -2201,7 +2240,8 @@
|
||||
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
|
||||
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
|
||||
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000",
|
||||
"compressionOverride": "Compression Override"
|
||||
"compressionOverride": "Compression Override",
|
||||
"modePack": "Mode Pack"
|
||||
},
|
||||
"costs": {
|
||||
"title": "Costs",
|
||||
@@ -2457,7 +2497,15 @@
|
||||
"ngrokLastError": "Last error: {error}",
|
||||
"ngrokStarted": "ngrok tunnel started",
|
||||
"ngrokStopped": "ngrok tunnel stopped",
|
||||
"ngrokRequestFailed": "Failed to update ngrok tunnel"
|
||||
"ngrokRequestFailed": "Failed to update ngrok tunnel",
|
||||
"tokenSaverSubtitle": "Spend less tokens on every request.",
|
||||
"tokenSaverToolOutput": "Tool output",
|
||||
"tokenSaverLlmOutput": "LLM output",
|
||||
"tokenSaverInputCompression": "Input compression",
|
||||
"apiEndpointsCatalogUnavailable": "API catalog unavailable",
|
||||
"apiEndpointsSearchPlaceholder": "Search endpoints...",
|
||||
"apiEndpointsRequiresAuth": "Requires auth",
|
||||
"apiEndpointsNoMatch": "No endpoints match your filter"
|
||||
},
|
||||
"endpoints": {
|
||||
"tabProxy": "Endpoint Proxy",
|
||||
@@ -2541,7 +2589,8 @@
|
||||
"apiKeyId": "Api Key Id",
|
||||
"offset": "Offset",
|
||||
"limit": "Limit",
|
||||
"tool": "Tool"
|
||||
"tool": "Tool",
|
||||
"mcpDashboardCopyUrl": "Copy URL"
|
||||
},
|
||||
"a2aDashboard": {
|
||||
"loading": "Loading A2A dashboard...",
|
||||
@@ -2672,7 +2721,10 @@
|
||||
"networkAccess": "Network Access",
|
||||
"networkAccessDesc": "Allow outbound network requests",
|
||||
"mode": "Mode",
|
||||
"q": "Q"
|
||||
"q": "Q",
|
||||
"filterSkillsPlaceholder": "Filter skills by name, description, or tag",
|
||||
"allModes": "All modes",
|
||||
"skillsMarketplace": "Skills Marketplace"
|
||||
},
|
||||
"health": {
|
||||
"title": "System Health",
|
||||
@@ -2962,7 +3014,8 @@
|
||||
"description": "Locally-hosted or specialty endpoints used as fallbacks."
|
||||
},
|
||||
"configure": "Configure providers"
|
||||
}
|
||||
},
|
||||
"tierFlowDiagramAlt": "OmniRoute 3-tier fallback diagram"
|
||||
},
|
||||
"providers": {
|
||||
"title": "Providers",
|
||||
@@ -4448,7 +4501,15 @@
|
||||
"memorySkillsSkillsmpMarketplace": "SkillsMP Marketplace",
|
||||
"memorySkillsFailedToSave": "Failed to save",
|
||||
"memorySkillsApiKey": "API Key",
|
||||
"memorySkillsActiveSkillsProvider": "Active Skills Provider"
|
||||
"memorySkillsActiveSkillsProvider": "Active Skills Provider",
|
||||
"cliproxyapiFallback": "CLIProxyAPI Fallback",
|
||||
"cliproxyapiEnableFallback": "Enable CLIProxyAPI Fallback",
|
||||
"cliproxyapiUrl": "CLIProxyAPI URL",
|
||||
"cliproxyapiStatus": "CLIProxyAPI Status",
|
||||
"cliproxyapiNotDetected": "Not detected",
|
||||
"payloadRulesTitle": "Payload Rules",
|
||||
"modelCooldownsTitle": "Models in cooldown",
|
||||
"modelCooldownsEmpty": "No models in cooldown right now."
|
||||
},
|
||||
"contextRtk": {
|
||||
"title": "RTK Engine",
|
||||
@@ -4785,7 +4846,8 @@
|
||||
"routeEndpointLabel": "Endpoint",
|
||||
"routeConnectionLabel": "Connection",
|
||||
"scenarioVision": "Vision (image understanding)",
|
||||
"scenarioSchemaCoercion": "Schema coercion (structured output)"
|
||||
"scenarioSchemaCoercion": "Schema coercion (structured output)",
|
||||
"techniques": "Techniques:"
|
||||
},
|
||||
"usage": {
|
||||
"title": "Usage",
|
||||
@@ -5086,7 +5148,9 @@
|
||||
"budgetDailyDollar": "Daily $",
|
||||
"budgetWeeklyDollar": "Weekly $",
|
||||
"budgetMonthlyDollar": "Monthly $",
|
||||
"budgetWarnAtPct": "Warn at %"
|
||||
"budgetWarnAtPct": "Warn at %",
|
||||
"quotaAlerts": "Quota alerts",
|
||||
"quotaTableRefreshing": "⟳ Refreshing..."
|
||||
},
|
||||
"modals": {
|
||||
"waitingAuth": "Waiting for Authorization",
|
||||
@@ -5847,7 +5911,10 @@
|
||||
"reasoningClearAll": "Clear Reasoning Cache",
|
||||
"reasoningClearSuccess": "Cleared {count} reasoning cache entries",
|
||||
"reasoningClearError": "Failed to clear reasoning cache",
|
||||
"reasoningNoData": "No reasoning entries cached yet. Entries appear when thinking models use tool calling."
|
||||
"reasoningNoData": "No reasoning entries cached yet. Entries appear when thinking models use tool calling.",
|
||||
"cachePerformanceRetry": "Retry",
|
||||
"cachePerformanceHitRate": "Hit Rate",
|
||||
"cachePerformanceAvgLatency": "Avg Latency (ms)"
|
||||
},
|
||||
"proxyConfigModal": {
|
||||
"levelGlobal": "Global",
|
||||
@@ -6027,7 +6094,8 @@
|
||||
"bulkImportErrorMissingHost": "Missing HOST",
|
||||
"bulkImportErrorInvalidPort": "Invalid PORT (must be 1-65535)",
|
||||
"bulkImportErrorInvalidType": "Invalid TYPE (use http, https, or socks5)",
|
||||
"bulkImportErrorInvalidStatus": "Invalid STATUS (use active or inactive)"
|
||||
"bulkImportErrorInvalidStatus": "Invalid STATUS (use active or inactive)",
|
||||
"clearAssignment": "(clear assignment)"
|
||||
},
|
||||
"playground": {
|
||||
"title": "Model Playground",
|
||||
@@ -6068,7 +6136,10 @@
|
||||
"music": "Music generation",
|
||||
"rerank": "Rerank",
|
||||
"search": "Web search"
|
||||
}
|
||||
},
|
||||
"conversationalChat": "Conversational Chat",
|
||||
"clearChat": "Clear chat",
|
||||
"typeMessagePlaceholder": "Type a message... (Shift+Enter for new line)"
|
||||
},
|
||||
"requestLogger": {
|
||||
"recording": "Recording",
|
||||
@@ -6306,6 +6377,10 @@
|
||||
"totalExceeded": "⚠ exceeds 100%",
|
||||
"addKey": "+ Add key…",
|
||||
"equalSplit": "Equal split",
|
||||
"save": "Save allocations"
|
||||
"save": "Save allocations",
|
||||
"betaPreviewLabel": "Beta — UI preview.",
|
||||
"betaConfigSavedPrefix": "A configuração é salva em",
|
||||
"betaConfigSavedSuffix": "(não persiste no servidor ainda). A aplicação dos caps por request ainda não está conectada ao pipeline da proxy. Esta tela permite desenhar e visualizar a divisão de cota;",
|
||||
"policyLabel": "Policy:"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user