Merge pull request #1324 from clousky2020/feat/i18n-clean
fix(i18n): resolve code review issues for PR #1318
This commit is contained in:
@@ -121,19 +121,19 @@ const STRATEGY_GUIDANCE_FALLBACK = {
|
||||
example: "Example: Multiple accounts of the same model to distribute usage evenly.",
|
||||
},
|
||||
auto: {
|
||||
when: "Use when you have one preferred model and only want fallback on failure.",
|
||||
avoid: "Avoid when you need balanced load between models.",
|
||||
example: "Example: Primary coding model with cheaper backup for outages.",
|
||||
when: "Use when you want multi-factor scoring based on cost, latency, and quality.",
|
||||
avoid: "Avoid when you need strict priority ordering or historical persistence.",
|
||||
example: "Example: Balance requests between models with different strengths.",
|
||||
},
|
||||
lkgp: {
|
||||
when: "Use when you have one preferred model and only want fallback on failure.",
|
||||
avoid: "Avoid when you need balanced load between models.",
|
||||
example: "Example: Primary coding model with cheaper backup for outages.",
|
||||
when: "Use when you want routing based on historical success rates and performance.",
|
||||
avoid: "Avoid when historical data is limited or unreliable.",
|
||||
example: "Example: Route to models with proven track records for specific tasks.",
|
||||
},
|
||||
"context-optimized": {
|
||||
when: "Use when you have one preferred model and only want fallback on failure.",
|
||||
avoid: "Avoid when you need balanced load between models.",
|
||||
example: "Example: Primary coding model with cheaper backup for outages.",
|
||||
when: "Use when you need to optimize for context window usage across models.",
|
||||
avoid: "Avoid when models have similar context lengths or simple tasks.",
|
||||
example: "Example: Distribute long conversations across models with large context windows.",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -240,30 +240,30 @@ const STRATEGY_RECOMMENDATIONS_FALLBACK = {
|
||||
],
|
||||
},
|
||||
auto: {
|
||||
title: "Fail-safe baseline",
|
||||
description: "Use one primary model and keep fallback chain short and reliable.",
|
||||
title: "Multi-factor optimization",
|
||||
description: "Routes based on real-time scoring of cost, latency, quality, and health.",
|
||||
tips: [
|
||||
"Put your most reliable model first.",
|
||||
"Keep 1-2 backup models with similar quality.",
|
||||
"Use safe retries to absorb transient provider failures.",
|
||||
"Let the engine balance across multiple factors automatically.",
|
||||
"Monitor which factors drive routing decisions in the logs.",
|
||||
"Use for complex workloads where no single factor dominates.",
|
||||
],
|
||||
},
|
||||
lkgp: {
|
||||
title: "Fail-safe baseline",
|
||||
description: "Use one primary model and keep fallback chain short and reliable.",
|
||||
title: "History-based routing",
|
||||
description: "Routes based on historical success rates and persistent performance data.",
|
||||
tips: [
|
||||
"Put your most reliable model first.",
|
||||
"Keep 1-2 backup models with similar quality.",
|
||||
"Use safe retries to absorb transient provider failures.",
|
||||
"Let success history accumulate before relying on this strategy.",
|
||||
"Models with better track records get preference over time.",
|
||||
"Ideal for stable workloads with consistent model availability.",
|
||||
],
|
||||
},
|
||||
"context-optimized": {
|
||||
title: "Fail-safe baseline",
|
||||
description: "Use one primary model and keep fallback chain short and reliable.",
|
||||
title: "Context-aware distribution",
|
||||
description: "Routes to optimize context window usage and conversation continuity.",
|
||||
tips: [
|
||||
"Put your most reliable model first.",
|
||||
"Keep 1-2 backup models with similar quality.",
|
||||
"Use safe retries to absorb transient provider failures.",
|
||||
"Best for long conversations that span multiple requests.",
|
||||
"Selects models with appropriate context capacity automatically.",
|
||||
"Use when context limits are a bottleneck for your workload.",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Card, Button, Select, Badge } from "@/shared/components";
|
||||
import { ALIAS_TO_ID } from "@/shared/constants/providers";
|
||||
@@ -185,18 +185,21 @@ export default function PlaygroundPage() {
|
||||
const t = useTranslations("playground");
|
||||
|
||||
// Get translated endpoint options
|
||||
const getEndpointOptions = () => [
|
||||
{ value: "chat", label: t("endpointOptions.chat") },
|
||||
{ value: "responses", label: t("endpointOptions.responses") },
|
||||
{ value: "images", label: t("endpointOptions.images") },
|
||||
{ value: "embeddings", label: t("endpointOptions.embeddings") },
|
||||
{ value: "speech", label: t("endpointOptions.speech") },
|
||||
{ value: "transcription", label: t("endpointOptions.transcription") },
|
||||
{ value: "video", label: t("endpointOptions.video") },
|
||||
{ value: "music", label: t("endpointOptions.music") },
|
||||
{ value: "rerank", label: t("endpointOptions.rerank") },
|
||||
{ value: "search", label: t("endpointOptions.search") },
|
||||
];
|
||||
const endpointOptions = useMemo(
|
||||
() => [
|
||||
{ value: "chat", label: t("endpointOptions.chat") },
|
||||
{ value: "responses", label: t("endpointOptions.responses") },
|
||||
{ value: "images", label: t("endpointOptions.images") },
|
||||
{ value: "embeddings", label: t("endpointOptions.embeddings") },
|
||||
{ value: "speech", label: t("endpointOptions.speech") },
|
||||
{ value: "transcription", label: t("endpointOptions.transcription") },
|
||||
{ value: "video", label: t("endpointOptions.video") },
|
||||
{ value: "music", label: t("endpointOptions.music") },
|
||||
{ value: "rerank", label: t("endpointOptions.rerank") },
|
||||
{ value: "search", label: t("endpointOptions.search") },
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
||||
const [models, setModels] = useState<ModelInfo[]>([]);
|
||||
const [providers, setProviders] = useState<ProviderOption[]>([]);
|
||||
@@ -495,7 +498,7 @@ export default function PlaygroundPage() {
|
||||
<Select
|
||||
value={selectedEndpoint}
|
||||
onChange={(e: any) => handleEndpointChange(e.target.value)}
|
||||
options={getEndpointOptions()}
|
||||
options={endpointOptions}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -3318,5 +3318,260 @@
|
||||
"deduplicatedRequests": "Deduplicated Requests",
|
||||
"savedCalls": "Saved API Calls",
|
||||
"totalProcessed": "Total Requests Processed"
|
||||
},
|
||||
"proxyConfigModal": {
|
||||
"levelGlobal": "Global",
|
||||
"levelProvider": "Provider",
|
||||
"levelCombo": "Combo",
|
||||
"levelKey": "Key",
|
||||
"levelDirect": "Direct (no proxy)",
|
||||
"titleGlobal": "Global Proxy Configuration",
|
||||
"titleLevel": "{level} Proxy — {label}",
|
||||
"loading": "Loading proxy configuration...",
|
||||
"inheritingFrom": "Inheriting from",
|
||||
"source": "Source",
|
||||
"savedProxy": "Saved Proxy",
|
||||
"custom": "Custom",
|
||||
"selectSavedProxyPlaceholder": "Select saved proxy...",
|
||||
"proxyType": "Proxy Type",
|
||||
"host": "Host",
|
||||
"hostPlaceholder": "1.2.3.4 or proxy.example.com",
|
||||
"port": "Port",
|
||||
"authOptional": "Authentication (optional)",
|
||||
"username": "Username",
|
||||
"usernamePlaceholder": "Username",
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "Password",
|
||||
"connected": "Connected",
|
||||
"ip": "IP:",
|
||||
"connectionFailed": "Connection failed",
|
||||
"testConnection": "Test connection",
|
||||
"clear": "Clear",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"errorSelectSavedProxy": "Please select a saved proxy first.",
|
||||
"errorSelectProxyFirst": "Please select a proxy first.",
|
||||
"errorProxyNotFound": "Selected proxy not found.",
|
||||
"errorClearSavedProxy": "Failed to clear saved proxy",
|
||||
"errorSaveProxy": "Failed to save proxy configuration",
|
||||
"errorClearProxy": "Failed to clear proxy configuration",
|
||||
"errorSocks5Hidden": "SOCKS5 is configured but hidden because NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=false."
|
||||
},
|
||||
"oauthModal": {
|
||||
"title": "Connect {providerName}",
|
||||
"waiting": "Waiting for authorization",
|
||||
"completeAuthInPopup": "Complete authorization in popup.",
|
||||
"popupClosedHint": "If the popup closed without redirecting back (e.g. Qoder), this dialog will auto-switch to manual URL entry mode.",
|
||||
"popupBlocked": "Popup blocked? Enter URL manually",
|
||||
"deviceCodeVisitUrl": "Visit the URL below and enter code:",
|
||||
"deviceCodeVerificationUrl": "Verification URL",
|
||||
"deviceCodeYourCode": "Your code",
|
||||
"deviceCodeWaiting": "Waiting for authorization...",
|
||||
"googleOAuthWarning": "Remote access + Google OAuth: Default credentials only accept redirects to <code>localhost</code>. After authorizing, your browser will try to open <code>localhost</code> — copy that full URL and paste it below. For fully remote use without this manual step, <a>configure your own OAuth credentials</a>.",
|
||||
"remoteAccessInfo": "Remote access: Since you're accessing OmniRoute remotely, after authorization you'll see an error page (localhost not found). This is normal — just copy the full URL from your browser address bar and paste it below.",
|
||||
"step1OpenUrl": "Step 1: Open this URL in your browser",
|
||||
"copy": "Copy",
|
||||
"step2PasteCallback": "Step 2: Paste callback URL or authorization code here",
|
||||
"step2Hint": "After authorization, paste the full callback URL. For Claude Code and Cline, you can also paste the authentication code directly, e.g. <code>code#state</code>.",
|
||||
"connect": "Connect",
|
||||
"cancel": "Cancel",
|
||||
"success": "Connection successful!",
|
||||
"successMessage": "Your {providerName} account has been connected.",
|
||||
"done": "Done",
|
||||
"error": "Connection failed",
|
||||
"tryAgain": "Try again"
|
||||
},
|
||||
"cursorAuthModal": {
|
||||
"title": "Connect Cursor IDE",
|
||||
"autoDetecting": "Auto-detecting tokens...",
|
||||
"readingFromCursor": "Reading from Cursor IDE or cursor-agent",
|
||||
"tokensAutoDetected": "Tokens successfully auto-detected from Cursor IDE!",
|
||||
"cursorNotDetected": "Cursor IDE not detected. Please manually paste your token.",
|
||||
"accessToken": "Access Token",
|
||||
"required": "*",
|
||||
"accessTokenPlaceholder": "Access token will auto-populate...",
|
||||
"machineId": "Machine ID",
|
||||
"optional": "(optional)",
|
||||
"machineIdPlaceholder": "Machine ID will auto-populate...",
|
||||
"importing": "Importing...",
|
||||
"importToken": "Import Token",
|
||||
"cancel": "Cancel",
|
||||
"errorAutoDetect": "Unable to auto-detect tokens",
|
||||
"errorAutoDetectFailed": "Auto-detect tokens failed",
|
||||
"errorEnterToken": "Please enter access token",
|
||||
"errorImportFailed": "Import failed"
|
||||
},
|
||||
"pricingModal": {
|
||||
"title": "Pricing Configuration",
|
||||
"loading": "Loading pricing data...",
|
||||
"pricingRatesFormat": "Pricing Rates Format",
|
||||
"ratesDescription": "All rates are in <strong>dollars per million tokens</strong> ($/1M tokens). Example: Input rate of 2.50 means $2.50 per 1,000,000 input tokens.",
|
||||
"model": "Model",
|
||||
"input": "Input",
|
||||
"output": "Output",
|
||||
"cached": "Cached",
|
||||
"reasoning": "Reasoning",
|
||||
"cacheCreation": "Cache Creation",
|
||||
"noPricingData": "No pricing data available",
|
||||
"resetToDefaults": "Reset to defaults",
|
||||
"cancel": "Cancel",
|
||||
"saving": "Saving...",
|
||||
"saveChanges": "Save changes",
|
||||
"resetConfirm": "Reset all pricing to defaults? This cannot be undone.",
|
||||
"errorSaveFailed": "Failed to save pricing",
|
||||
"errorResetFailed": "Failed to reset pricing"
|
||||
},
|
||||
"proxyRegistry": {
|
||||
"title": "Proxy Registry",
|
||||
"description": "Store reusable proxies and track assignments.",
|
||||
"importLegacy": "Import Legacy",
|
||||
"bulkAssign": "Bulk Assign",
|
||||
"addProxy": "Add Proxy",
|
||||
"loading": "Loading proxies...",
|
||||
"noProxies": "No saved proxies yet.",
|
||||
"tableName": "Name",
|
||||
"tableEndpoint": "Endpoint",
|
||||
"tableStatus": "Status",
|
||||
"tableHealth": "Health (24h)",
|
||||
"tableUsage": "Usage",
|
||||
"tableActions": "Actions",
|
||||
"test": "Test",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"modalCreateTitle": "Create Proxy",
|
||||
"modalEditTitle": "Edit Proxy",
|
||||
"labelName": "Name",
|
||||
"labelType": "Type",
|
||||
"labelHost": "Host",
|
||||
"labelPort": "Port",
|
||||
"labelUsername": "Username",
|
||||
"labelPassword": "Password",
|
||||
"labelRegion": "Region",
|
||||
"labelStatus": "Status",
|
||||
"labelNotes": "Notes",
|
||||
"usernamePlaceholderEdit": "Leave blank to keep current username",
|
||||
"passwordPlaceholderEdit": "Leave blank to keep current password",
|
||||
"statusActive": "Active",
|
||||
"statusInactive": "Inactive",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"bulkModalTitle": "Bulk Proxy Assignment",
|
||||
"bulkLabelScope": "Scope",
|
||||
"bulkLabelProxy": "Proxy",
|
||||
"bulkClearAssignment": "(Clear assignment)",
|
||||
"bulkLabelScopeIds": "Scope IDs (comma or newline separated)",
|
||||
"bulkScopeIdsPlaceholder": "provider-openai,provider-anthropic",
|
||||
"bulkApply": "Apply",
|
||||
"errorLoadFailed": "Failed to load proxy registry",
|
||||
"errorNameHostRequired": "Name and host are required",
|
||||
"errorSaveFailed": "Failed to save proxy",
|
||||
"errorDeleteFailed": "Failed to delete proxy",
|
||||
"errorForceDeleteConfirm": "This proxy is still assigned. Force delete and remove all assignments?",
|
||||
"errorMigrateFailed": "Failed to migrate legacy proxy config",
|
||||
"errorBulkFailed": "Failed to execute bulk assignment",
|
||||
"success": "✓",
|
||||
"failure": "✗",
|
||||
"failed": "Failed",
|
||||
"successRate": "{rate}% success",
|
||||
"avgLatency": "{latency}ms average",
|
||||
"assignmentsCount": "{count} assignments",
|
||||
"noData": "—",
|
||||
"testSuccess": "✓ {ip}",
|
||||
"testLatency": "{latency}ms",
|
||||
"testFailure": "✗ {error}"
|
||||
},
|
||||
"playground": {
|
||||
"title": "Model Playground",
|
||||
"description": "Test any model directly from the dashboard. Select provider, model, and endpoint type, then send a request to see the raw response.",
|
||||
"endpoint": "Endpoint",
|
||||
"provider": "Provider",
|
||||
"model": "Model",
|
||||
"accountKey": "Account / Key",
|
||||
"autoAccounts": "Auto ({count} accounts)",
|
||||
"noAccounts": "No accounts",
|
||||
"send": "Send",
|
||||
"cancel": "Cancel",
|
||||
"audioFile": "Audio File",
|
||||
"attachImages": "Attach Images (Vision)",
|
||||
"multipartFormData": "multipart/form-data",
|
||||
"upToImages": "Up to 4 images",
|
||||
"selectAudioFile": "Select audio file for transcription (mp3, wav, m4a, ogg, flac…)",
|
||||
"clearAll": "Clear All",
|
||||
"request": "Request",
|
||||
"response": "Response",
|
||||
"transcription": "Transcription",
|
||||
"copy": "Copy",
|
||||
"resetToDefault": "Reset to default",
|
||||
"downloadAudio": "Download audio",
|
||||
"copyText": "Copy text",
|
||||
"transcriptionHint": "Transcription uses multipart/form-data. Upload the audio file above — the JSON below controls extra parameters (model, language).",
|
||||
"imagesGenerated": "Generated {count} images",
|
||||
"generatedImage": "Generated image {index}",
|
||||
"save": "Save",
|
||||
"endpointOptions": {
|
||||
"chat": "Chat completions",
|
||||
"responses": "Responses",
|
||||
"images": "Image generation",
|
||||
"embeddings": "Embeddings",
|
||||
"speech": "Text-to-speech",
|
||||
"transcription": "Audio transcription",
|
||||
"video": "Video generation",
|
||||
"music": "Music generation",
|
||||
"rerank": "Rerank",
|
||||
"search": "Web search"
|
||||
}
|
||||
},
|
||||
"requestLogger": {
|
||||
"recording": "Recording",
|
||||
"paused": "Paused",
|
||||
"pipelineLogsOn": "Pipeline logs on",
|
||||
"pipelineLogsOff": "Pipeline logs off",
|
||||
"updatingPipelineLogs": "Updating pipeline logs...",
|
||||
"searchPlaceholder": "Search models, providers, accounts, API keys, combos...",
|
||||
"allProviders": "All providers",
|
||||
"allModels": "All models",
|
||||
"allAccounts": "All accounts",
|
||||
"allApiKeys": "All API keys",
|
||||
"total": "Total",
|
||||
"ok": "Success",
|
||||
"err": "Error",
|
||||
"combo": "Combo",
|
||||
"keys": "Keys",
|
||||
"shown": "Shown",
|
||||
"sortNewest": "Newest",
|
||||
"sortOldest": "Oldest",
|
||||
"sortTokensDesc": "Tokens ↓",
|
||||
"sortTokensAsc": "Tokens ↑",
|
||||
"sortDurationDesc": "Duration ↓",
|
||||
"sortDurationAsc": "Duration ↑",
|
||||
"sortStatusDesc": "Status ↓",
|
||||
"sortStatusAsc": "Status ↑",
|
||||
"sortModelAsc": "Model A-Z",
|
||||
"sortModelDesc": "Model Z-A",
|
||||
"statusFilters": {
|
||||
"all": "All",
|
||||
"error": "Error",
|
||||
"success": "Success",
|
||||
"combo": "Combo"
|
||||
},
|
||||
"columns": {
|
||||
"status": "Status",
|
||||
"cacheSource": "Cache Source",
|
||||
"model": "Model",
|
||||
"requested": "Requested",
|
||||
"provider": "Provider",
|
||||
"protocol": "Request Protocol",
|
||||
"account": "Account",
|
||||
"apiKey": "API Key",
|
||||
"combo": "Combo",
|
||||
"tokens": "Tokens",
|
||||
"tps": "TPS",
|
||||
"duration": "Duration",
|
||||
"time": "Time"
|
||||
},
|
||||
"loadingLogs": "Loading logs...",
|
||||
"noLogs": "No logs yet. Make some API calls to see them here.",
|
||||
"noMatchingLogs": "No logs matching current filters.",
|
||||
"callLogsInfo": "Call logs are also saved as JSON files to {dataDir} and rotated based on {retentionDays} and {maxEntries}."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -921,19 +921,19 @@
|
||||
"example": "示例:在使用完所有 200 美元的 Deepgram 额度后再回退到 Groq。"
|
||||
},
|
||||
"auto": {
|
||||
"when": "当您有一个首选模型且只希望在失败时才回退到备用模型时使用。",
|
||||
"avoid": "当您需要在多个模型之间平衡负载时避免使用。",
|
||||
"example": "示例:主力编码模型搭配更便宜的备用模型以应对故障。"
|
||||
"when": "当您需要基于成本、延迟和质量的多因素评分路由时使用。",
|
||||
"avoid": "当您需要严格的优先级排序或历史持久性时避免使用。",
|
||||
"example": "示例:在具有不同优势的模型之间平衡请求。"
|
||||
},
|
||||
"lkgp": {
|
||||
"when": "当您有一个首选模型且只希望在失败时才回退到备用模型时使用。",
|
||||
"avoid": "当您需要在多个模型之间平衡负载时避免使用。",
|
||||
"example": "示例:主力编码模型搭配更便宜的备用模型以应对故障。"
|
||||
"when": "当您希望基于历史成功率和性能进行路由时使用。",
|
||||
"avoid": "当历史数据有限或不可靠时避免使用。",
|
||||
"example": "示例:路由到在特定任务上有良好记录的模型。"
|
||||
},
|
||||
"context-optimized": {
|
||||
"when": "当您有一个首选模型且只希望在失败时才回退到备用模型时使用。",
|
||||
"avoid": "当您需要在多个模型之间平衡负载时避免使用。",
|
||||
"example": "示例:主力编码模型搭配更便宜的备用模型以应对故障。"
|
||||
"when": "当您需要优化模型间的上下文窗口使用时使用。",
|
||||
"avoid": "当模型具有相似的上下文长度或任务简单时避免使用。",
|
||||
"example": "示例:在具有大上下文窗口的模型之间分配长对话。"
|
||||
}
|
||||
},
|
||||
"advancedHelp": {
|
||||
@@ -1120,25 +1120,25 @@
|
||||
"tip3": "非常适合在多个 API 账户之间做负载均衡。"
|
||||
},
|
||||
"auto": {
|
||||
"title": "稳妥基线",
|
||||
"description": "使用一个主模型,并保持回退链路简短且可靠。",
|
||||
"tip1": "把最可靠的模型放在第一位。",
|
||||
"tip2": "保留 1 到 2 个质量相近的备用模型。",
|
||||
"tip3": "开启安全重试,吸收临时性的提供商故障。"
|
||||
"title": "多因素优化",
|
||||
"description": "基于成本、延迟、质量和健康的实时评分进行路由。",
|
||||
"tip1": "让引擎自动平衡多个因素。",
|
||||
"tip2": "在日志中监控哪些因素驱动路由决策。",
|
||||
"tip3": "用于复杂工作负载,其中没有单一因素占主导。"
|
||||
},
|
||||
"lkgp": {
|
||||
"title": "稳妥基线",
|
||||
"description": "使用一个主模型,并保持回退链路简短且可靠。",
|
||||
"tip1": "把最可靠的模型放在第一位。",
|
||||
"tip2": "保留 1 到 2 个质量相近的备用模型。",
|
||||
"tip3": "开启安全重试,吸收临时性的提供商故障。"
|
||||
"title": "历史路由",
|
||||
"description": "基于历史成功率和持久性能数据进行路由。",
|
||||
"tip1": "在依赖此策略之前让成功历史积累足够数据。",
|
||||
"tip2": "最适合具有稳定性能特征的工作负载。",
|
||||
"tip3": "定期审查历史数据,确保路由决策保持准确。"
|
||||
},
|
||||
"context-optimized": {
|
||||
"title": "稳妥基线",
|
||||
"description": "使用一个主模型,并保持回退链路简短且可靠。",
|
||||
"tip1": "把最可靠的模型放在第一位。",
|
||||
"tip2": "保留 1 到 2 个质量相近的备用模型。",
|
||||
"tip3": "开启安全重试,吸收临时性的提供商故障。"
|
||||
"title": "上下文优化",
|
||||
"description": "基于上下文窗口使用情况和令牌效率优化路由。",
|
||||
"tip1": "将长对话路由到具有更大上下文窗口的模型。",
|
||||
"tip2": "监控上下文利用率以避免令牌浪费。",
|
||||
"tip3": "最适合需要大量上下文保留的对话式 AI。"
|
||||
}
|
||||
},
|
||||
"templateFreeStack": "免费栈($0)",
|
||||
@@ -3658,6 +3658,7 @@
|
||||
},
|
||||
"columns": {
|
||||
"status": "状态",
|
||||
"cacheSource": "缓存来源",
|
||||
"model": "模型",
|
||||
"requested": "请求",
|
||||
"provider": "提供商",
|
||||
@@ -3666,6 +3667,7 @@
|
||||
"apiKey": "API密钥",
|
||||
"combo": "组合",
|
||||
"tokens": "Tokens",
|
||||
"tps": "TPS",
|
||||
"duration": "时长",
|
||||
"time": "时间"
|
||||
},
|
||||
|
||||
@@ -646,16 +646,21 @@ export default function OAuthModal({
|
||||
<span className="material-symbols-outlined text-sm align-middle mr-1">
|
||||
warning
|
||||
</span>
|
||||
<strong
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t("googleOAuthWarning")
|
||||
.replace(
|
||||
"<a>",
|
||||
'<a href="https://github.com/diegosouzapw/OmniRoute#oauth-on-a-remote-server" target="_blank" rel="noreferrer" class="underline">'
|
||||
)
|
||||
.replace("</a>", "</a>"),
|
||||
}}
|
||||
/>
|
||||
<strong>
|
||||
{t.rich("googleOAuthWarning", {
|
||||
code: (chunks) => <code>{chunks}</code>,
|
||||
a: (chunks) => (
|
||||
<a
|
||||
href="https://github.com/diegosouzapw/OmniRoute#oauth-on-a-remote-server"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
{chunks}
|
||||
</a>
|
||||
),
|
||||
})}
|
||||
</strong>
|
||||
</div>
|
||||
)}
|
||||
{/* Generic remote info for other providers */}
|
||||
@@ -686,13 +691,9 @@ export default function OAuthModal({
|
||||
<div>
|
||||
<p className="text-sm font-medium mb-2">{t("step2PasteCallback")}</p>
|
||||
<p className="text-xs text-text-muted mb-2">
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t("step2Hint")
|
||||
.replace("<code>", "<code>")
|
||||
.replace("</code>", "</code>"),
|
||||
}}
|
||||
/>
|
||||
{t.rich("step2Hint", {
|
||||
code: (chunks) => <code>{chunks}</code>,
|
||||
})}
|
||||
</p>
|
||||
<Input
|
||||
value={callbackUrl}
|
||||
|
||||
@@ -119,13 +119,9 @@ export default function PricingModal({ isOpen, onClose, onSave }) {
|
||||
<div className="bg-bg-subtle border border-border rounded-lg p-3 text-sm">
|
||||
<p className="font-medium mb-1">{t("pricingRatesFormat")}</p>
|
||||
<p className="text-text-muted">
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t("ratesDescription")
|
||||
.replace("<strong>", "<strong>")
|
||||
.replace("</strong>", "</strong>"),
|
||||
}}
|
||||
/>
|
||||
{t.rich("ratesDescription", {
|
||||
strong: (chunks) => <strong>{chunks}</strong>,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -354,7 +354,10 @@ export default function ProxyConfigModal({
|
||||
const title =
|
||||
level === "global"
|
||||
? t("titleGlobal")
|
||||
: `${t(`level${level.charAt(0).toUpperCase() + level.slice(1)}` as any)} Proxy — ${levelLabel || levelId || ""}`;
|
||||
: t("titleLevel", {
|
||||
level: t(`level${level.charAt(0).toUpperCase() + level.slice(1)}` as any),
|
||||
label: levelLabel || levelId || "",
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} title={title} maxWidth="lg">
|
||||
|
||||
@@ -116,27 +116,35 @@ export default function RequestLoggerV2() {
|
||||
const t = useTranslations("requestLogger");
|
||||
|
||||
// Get translated status filters
|
||||
const getStatusFilters = () => [
|
||||
{ key: "all", label: t("statusFilters.all"), icon: "" },
|
||||
{ key: "error", label: t("statusFilters.error"), icon: "error" },
|
||||
{ key: "ok", label: t("statusFilters.success"), icon: "check_circle" },
|
||||
{ key: "combo", label: t("statusFilters.combo"), icon: "hub" },
|
||||
];
|
||||
const statusFilters = useMemo(
|
||||
() => [
|
||||
{ key: "all", label: t("statusFilters.all"), icon: "" },
|
||||
{ key: "error", label: t("statusFilters.error"), icon: "error" },
|
||||
{ key: "ok", label: t("statusFilters.success"), icon: "check_circle" },
|
||||
{ key: "combo", label: t("statusFilters.combo"), icon: "hub" },
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
||||
// Get translated columns
|
||||
const getColumns = () => [
|
||||
{ key: "status", label: t("columns.status") },
|
||||
{ key: "model", label: t("columns.model") },
|
||||
{ key: "requestedModel", label: t("columns.requested") },
|
||||
{ key: "provider", label: t("columns.provider") },
|
||||
{ key: "protocol", label: t("columns.protocol") },
|
||||
{ key: "account", label: t("columns.account") },
|
||||
{ key: "apiKey", label: t("columns.apiKey") },
|
||||
{ key: "combo", label: t("columns.combo") },
|
||||
{ key: "tokens", label: t("columns.tokens") },
|
||||
{ key: "duration", label: t("columns.duration") },
|
||||
{ key: "time", label: t("columns.time") },
|
||||
];
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{ key: "status", label: t("columns.status") },
|
||||
{ key: "cacheSource", label: t("columns.cacheSource") },
|
||||
{ key: "model", label: t("columns.model") },
|
||||
{ key: "requestedModel", label: t("columns.requested") },
|
||||
{ key: "provider", label: t("columns.provider") },
|
||||
{ key: "protocol", label: t("columns.protocol") },
|
||||
{ key: "account", label: t("columns.account") },
|
||||
{ key: "apiKey", label: t("columns.apiKey") },
|
||||
{ key: "combo", label: t("columns.combo") },
|
||||
{ key: "tokens", label: t("columns.tokens") },
|
||||
{ key: "tps", label: t("columns.tps") },
|
||||
{ key: "duration", label: t("columns.duration") },
|
||||
{ key: "time", label: t("columns.time") },
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
||||
const [logs, setLogs] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -159,7 +167,7 @@ export default function RequestLoggerV2() {
|
||||
|
||||
// Column visibility with localStorage persistence
|
||||
const [visibleColumns, setVisibleColumns] = useState(() => {
|
||||
const defaultVisible = Object.fromEntries(getColumns().map((c) => [c.key, true]));
|
||||
const defaultVisible = Object.fromEntries(columns.map((c) => [c.key, true]));
|
||||
if (typeof window === "undefined") return defaultVisible;
|
||||
try {
|
||||
const saved = localStorage.getItem("loggerVisibleColumns");
|
||||
@@ -525,7 +533,7 @@ export default function RequestLoggerV2() {
|
||||
{/* Quick Filters */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{/* Status Filters */}
|
||||
{getStatusFilters().map((f) => (
|
||||
{statusFilters.map((f) => (
|
||||
<button
|
||||
key={f.key}
|
||||
onClick={() => setActiveFilter(activeFilter === f.key ? "all" : f.key)}
|
||||
@@ -582,7 +590,7 @@ export default function RequestLoggerV2() {
|
||||
{/* Column Visibility Toggles */}
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
<span className="text-[10px] text-text-muted uppercase tracking-wider mr-1">Columns</span>
|
||||
{getColumns().map((col) => (
|
||||
{columns.map((col) => (
|
||||
<button
|
||||
key={col.key}
|
||||
onClick={() => toggleColumn(col.key)}
|
||||
@@ -628,7 +636,7 @@ export default function RequestLoggerV2() {
|
||||
)}
|
||||
{visibleColumns.cacheSource && (
|
||||
<th className="px-3 py-2.5 font-semibold text-text-muted uppercase tracking-wider text-[10px]">
|
||||
Cache Source
|
||||
{t("columns.cacheSource")}
|
||||
</th>
|
||||
)}
|
||||
{visibleColumns.model && (
|
||||
@@ -673,7 +681,7 @@ export default function RequestLoggerV2() {
|
||||
)}
|
||||
{visibleColumns.tps && (
|
||||
<th className="px-3 py-2.5 font-semibold text-text-muted uppercase tracking-wider text-[10px] text-right">
|
||||
TPS
|
||||
{t("columns.tps")}
|
||||
</th>
|
||||
)}
|
||||
{visibleColumns.duration && (
|
||||
|
||||
Reference in New Issue
Block a user