mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
Merge pull request #2316 from oyi77/feat/ui-rework
feat(ui): simple/advanced mode for Caveman & RTK + newbie UX improvements Adapts oyi77's UX rework on top of our refactor/pages overhaul. Layout priority: our 9-section sidebar restructure stays; PR's additions (subtitles, intros, simple/advanced toggles, empty states, error labels) are integrated into our structure. Conflict resolution: - index.tsx: union — exports InfoTooltip, PresetSlider (new shared components from PR) alongside our NoAuthProviderCard. - en.json: union — kept our "OmniSkills"/"AgentSkills" labels and "API Key Manager" naming; added PR's subtitleKey strings, settings intro keys, and empty state keys. - sidebarVisibility.ts: kept our 9-section structure; added subtitleKey?: string to SidebarItemDefinition and mapped subtitle keys onto the 7 matching items (endpoints, api-manager, combos, batch, context-caveman, context-rtk, webhooks). - Sidebar.tsx: kept our collapsible-section rendering; integrated PR's subtitle support into resolveItem() and renderNavLink() label area. - CavemanContextPageClient.tsx: took PR's version — adds SegmentedControl for simple/advanced mode (gates full settings tab in advanced). - RtkContextPageClient.tsx: took PR's version — adds SegmentedControl + Collapsible filter catalog. - settings/page.tsx: kept our redirect (we converted tabs→pages). Ported PR's intro text paragraphs to /settings/ai, /settings/routing, and /settings/resilience subpages using the auto-merged i18n keys. - HomePageClient.tsx: kept ours — we removed Providers Overview card in the refactor, and PR's empty state for that card is now redundant. PR's equivalent empty state at /dashboard/providers (in providers/page.tsx) auto-merged cleanly and serves the same purpose. Closes #2316
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { SegmentedControl } from "@/shared/components";
|
||||
import CompressionSettingsTab from "@/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab";
|
||||
|
||||
type AnalyticsSummary = {
|
||||
@@ -44,6 +45,7 @@ export default function CavemanContextPageClient() {
|
||||
const [settings, setSettings] = useState<CompressionSettings | null>(null);
|
||||
const [languagePacks, setLanguagePacks] = useState<LanguagePack[]>([]);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [viewMode, setViewMode] = useState<"simple" | "advanced">("simple");
|
||||
|
||||
const refreshSettings = () => {
|
||||
fetch("/api/context/caveman/config")
|
||||
@@ -116,7 +118,27 @@ export default function CavemanContextPageClient() {
|
||||
const previewPrompt = `[OmniRoute Caveman Output Mode]\n${t(`preview.${outputMode.intensity}`)}`;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="mx-auto flex max-w-6xl flex-col gap-6">
|
||||
<header className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="material-symbols-outlined text-[30px] text-primary">compress</span>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-text-main">{t("title")}</h1>
|
||||
<p className="text-sm text-text-muted">{t("description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<SegmentedControl
|
||||
value={viewMode}
|
||||
onChange={(v) => setViewMode(v as "simple" | "advanced")}
|
||||
options={[
|
||||
{ value: "simple", label: t("simpleMode") || "Simple" },
|
||||
{ value: "advanced", label: t("advancedMode") || "Advanced" },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section className="grid grid-cols-1 gap-3 sm:grid-cols-4">
|
||||
{statCards.map(([label, value]) => (
|
||||
<div key={label} className="rounded-lg border border-border bg-surface p-4">
|
||||
@@ -246,7 +268,7 @@ export default function CavemanContextPageClient() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<CompressionSettingsTab />
|
||||
{viewMode === "advanced" && <CompressionSettingsTab />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { SegmentedControl, Collapsible } from "@/shared/components";
|
||||
|
||||
type RtkFilter = {
|
||||
id: string;
|
||||
@@ -66,6 +67,7 @@ export default function RtkContextPageClient() {
|
||||
const [sample, setSample] = useState(SAMPLE_OUTPUT);
|
||||
const [preview, setPreview] = useState<PreviewResult | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [viewMode, setViewMode] = useState<"simple" | "advanced">("simple");
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/context/rtk/filters")
|
||||
@@ -138,7 +140,27 @@ export default function RtkContextPageClient() {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="mx-auto flex max-w-6xl flex-col gap-6">
|
||||
<header className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="material-symbols-outlined text-[30px] text-primary">filter_alt</span>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-text-main">{t("title")}</h1>
|
||||
<p className="text-sm text-text-muted">{t("description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<SegmentedControl
|
||||
value={viewMode}
|
||||
onChange={(v) => setViewMode(v as "simple" | "advanced")}
|
||||
options={[
|
||||
{ value: "simple", label: t("simpleMode") || "Simple" },
|
||||
{ value: "advanced", label: t("advancedMode") || "Advanced" },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
||||
{statCards.map(([label, value]) => (
|
||||
<div key={label} className="rounded-lg border border-border bg-surface p-4">
|
||||
@@ -268,72 +290,84 @@ export default function RtkContextPageClient() {
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="grid grid-cols-1 gap-4 xl:grid-cols-[1fr_1fr]">
|
||||
<div className="rounded-lg border border-border bg-surface p-4">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h2 className="text-sm font-semibold text-text-main">{t("filterTesting")}</h2>
|
||||
<button
|
||||
onClick={runPreview}
|
||||
className="rounded-lg bg-primary px-3 py-2 text-sm font-medium text-white"
|
||||
>
|
||||
{t("run")}
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
value={sample}
|
||||
onChange={(event) => setSample(event.target.value)}
|
||||
placeholder={t("pasteOutput")}
|
||||
className="h-72 w-full rounded-lg border border-border bg-bg p-3 font-mono text-xs text-text-main"
|
||||
/>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border bg-surface p-4">
|
||||
<h2 className="mb-3 text-sm font-semibold text-text-main">{t("result")}</h2>
|
||||
{preview?.detection && (
|
||||
<p className="mb-2 text-xs text-text-muted">
|
||||
{t("detected")}: {preview.detection.type} (
|
||||
{Math.round(preview.detection.confidence * 100)}%)
|
||||
</p>
|
||||
)}
|
||||
<pre className="h-72 overflow-auto rounded-lg border border-border bg-bg p-3 text-xs text-text-main">
|
||||
{preview ? JSON.stringify(preview, null, 2) : t("previewEmpty")}
|
||||
</pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||
{Object.entries(groupedFilters).map(([category, items]) => (
|
||||
<div key={category} className="rounded-lg border border-border bg-surface p-4">
|
||||
<h2 className="text-sm font-semibold capitalize text-text-main">{category}</h2>
|
||||
<div className="mt-3 space-y-3">
|
||||
{items.map((filter) => {
|
||||
const enabled = config ? !config.disabledFilters.includes(filter.id) : true;
|
||||
return (
|
||||
<div
|
||||
key={filter.id}
|
||||
className="border-t border-border pt-3 first:border-t-0 first:pt-0"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium text-text-main">{filter.name}</p>
|
||||
<p className="mt-1 text-xs text-text-muted">{filter.description}</p>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={enabled}
|
||||
disabled={!config || saving}
|
||||
onChange={(event) => toggleFilter(filter.id, event.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-2 font-mono text-[11px] text-text-muted">
|
||||
{filter.commandTypes.join(", ")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{viewMode === "advanced" && (
|
||||
<section className="grid grid-cols-1 gap-4 xl:grid-cols-[1fr_1fr]">
|
||||
<div className="rounded-lg border border-border bg-surface p-4">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h2 className="text-sm font-semibold text-text-main">{t("filterTesting")}</h2>
|
||||
<button
|
||||
onClick={runPreview}
|
||||
className="rounded-lg bg-primary px-3 py-2 text-sm font-medium text-white"
|
||||
>
|
||||
{t("run")}
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
value={sample}
|
||||
onChange={(event) => setSample(event.target.value)}
|
||||
placeholder={t("pasteOutput")}
|
||||
className="h-72 w-full rounded-lg border border-border bg-bg p-3 font-mono text-xs text-text-main"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
<div className="rounded-lg border border-border bg-surface p-4">
|
||||
<h2 className="mb-3 text-sm font-semibold text-text-main">{t("result")}</h2>
|
||||
{preview?.detection && (
|
||||
<p className="mb-2 text-xs text-text-muted">
|
||||
{t("detected")}: {preview.detection.type} (
|
||||
{Math.round(preview.detection.confidence * 100)}%)
|
||||
</p>
|
||||
)}
|
||||
<pre className="h-72 overflow-auto rounded-lg border border-border bg-bg p-3 text-xs text-text-main">
|
||||
{preview ? JSON.stringify(preview, null, 2) : t("previewEmpty")}
|
||||
</pre>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<Collapsible
|
||||
title={t("filterCatalog") || "Filter Catalog"}
|
||||
subtitle={t("filterCatalogDesc") || "Available output filters by category"}
|
||||
icon="filter_list"
|
||||
trailing={
|
||||
<span className="text-xs text-text-muted">
|
||||
{Object.values(groupedFilters).flat().length} {t("filtersActive") || "filters"}
|
||||
</span>
|
||||
}
|
||||
defaultOpen={viewMode === "advanced"}
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||
{Object.entries(groupedFilters).map(([category, items]) => (
|
||||
<div key={category} className="rounded-lg border border-border bg-bg p-3">
|
||||
<h3 className="text-xs font-semibold capitalize text-text-main">{category}</h3>
|
||||
<div className="mt-2 space-y-2">
|
||||
{items.map((filter) => {
|
||||
const enabled = config ? !config.disabledFilters.includes(filter.id) : true;
|
||||
return (
|
||||
<div
|
||||
key={filter.id}
|
||||
className="border-t border-border pt-2 first:border-t-0 first:pt-0"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-medium text-text-main">{filter.name}</p>
|
||||
<p className="mt-0.5 text-[11px] text-text-muted">{filter.description}</p>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={enabled}
|
||||
disabled={!config || saving}
|
||||
onChange={(event) => toggleFilter(filter.id, event.target.checked)}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Collapsible>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -65,18 +65,18 @@ function getConnectionErrorTag(connection) {
|
||||
if (!connection) return null;
|
||||
|
||||
const explicitType = connection.lastErrorType;
|
||||
if (explicitType === "runtime_error") return "RUNTIME";
|
||||
if (explicitType === "runtime_error") return "Runtime";
|
||||
if (
|
||||
explicitType === "upstream_auth_error" ||
|
||||
explicitType === "auth_missing" ||
|
||||
explicitType === "token_refresh_failed" ||
|
||||
explicitType === "token_expired"
|
||||
) {
|
||||
return "AUTH";
|
||||
return "Auth";
|
||||
}
|
||||
if (explicitType === "upstream_rate_limited") return "429";
|
||||
if (explicitType === "upstream_unavailable") return "5XX";
|
||||
if (explicitType === "network_error") return "NET";
|
||||
if (explicitType === "upstream_rate_limited") return "Rate limited";
|
||||
if (explicitType === "upstream_unavailable") return "Server error";
|
||||
if (explicitType === "network_error") return "Network";
|
||||
|
||||
const numericCode = Number(connection.errorCode);
|
||||
if (Number.isFinite(numericCode) && numericCode >= 400) {
|
||||
@@ -84,19 +84,19 @@ function getConnectionErrorTag(connection) {
|
||||
}
|
||||
|
||||
const fromMessage = getErrorCode(connection.lastError);
|
||||
if (fromMessage === "401" || fromMessage === "403") return "AUTH";
|
||||
if (fromMessage === "401" || fromMessage === "403") return "Auth";
|
||||
if (fromMessage && fromMessage !== "ERR") return fromMessage;
|
||||
|
||||
const msg = (connection.lastError || "").toLowerCase();
|
||||
if (msg.includes("runtime") || msg.includes("not runnable") || msg.includes("not installed"))
|
||||
return "RUNTIME";
|
||||
return "Runtime";
|
||||
if (
|
||||
msg.includes("invalid api key") ||
|
||||
msg.includes("token invalid") ||
|
||||
msg.includes("revoked") ||
|
||||
msg.includes("unauthorized")
|
||||
)
|
||||
return "AUTH";
|
||||
return "Auth";
|
||||
|
||||
return "ERR";
|
||||
}
|
||||
@@ -592,6 +592,40 @@ export default function ProvidersPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const totalConfigured =
|
||||
oauthProviderEntriesAll.filter((e) => Number(e.stats?.total || 0) > 0).length +
|
||||
apiKeyProviderEntriesAll.filter((e) => Number(e.stats?.total || 0) > 0).length +
|
||||
webCookieProviderEntriesAll.filter((e) => Number(e.stats?.total || 0) > 0).length +
|
||||
localProviderEntriesAll.filter((e) => Number(e.stats?.total || 0) > 0).length;
|
||||
|
||||
if (totalConfigured === 0 && !searchQuery) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-center">
|
||||
<div className="flex items-center justify-center size-16 rounded-full bg-primary/10 mb-4">
|
||||
<span className="material-symbols-outlined text-[32px] text-primary">dns</span>
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold text-text-main">
|
||||
{t("addFirstProvider") || "Add your first provider"}
|
||||
</h2>
|
||||
<p className="text-sm text-text-muted mt-2 max-w-md">
|
||||
{t("addFirstProviderDesc") ||
|
||||
"Connect an AI provider to start routing requests through OmniRoute. You can use free providers, API keys, or OAuth accounts."}
|
||||
</p>
|
||||
<div className="flex items-center gap-3 mt-4">
|
||||
<a
|
||||
href="https://docs.omniroute.io/providers"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium rounded-lg border border-border text-text-muted hover:text-text-main hover:bg-bg-subtle transition-colors"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">help</span>
|
||||
{t("learnMore") || "Learn more"}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Provider Summary Card */}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import ThinkingBudgetTab from "../components/ThinkingBudgetTab";
|
||||
import VisionBridgeSettingsTab from "../components/VisionBridgeSettingsTab";
|
||||
import SystemPromptTab from "../components/SystemPromptTab";
|
||||
@@ -7,8 +8,10 @@ import MemorySkillsTab from "../components/MemorySkillsTab";
|
||||
import ModelsDevSyncTab from "../components/ModelsDevSyncTab";
|
||||
|
||||
export default function SettingsAiPage() {
|
||||
const t = useTranslations("settings");
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<p className="text-sm text-text-muted">{t("aiSettingsIntro")}</p>
|
||||
<ThinkingBudgetTab />
|
||||
<VisionBridgeSettingsTab />
|
||||
<SystemPromptTab />
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import ResilienceTab from "../components/ResilienceTab";
|
||||
|
||||
export default function SettingsResiliencePage() {
|
||||
return <ResilienceTab />;
|
||||
const t = useTranslations("settings");
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<p className="text-sm text-text-muted">{t("resilienceSettingsIntro")}</p>
|
||||
<ResilienceTab />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import RoutingTab from "../components/RoutingTab";
|
||||
import ModelRoutingSection from "@/shared/components/ModelRoutingSection";
|
||||
import ComboDefaultsTab from "../components/ComboDefaultsTab";
|
||||
@@ -7,8 +8,10 @@ import ModelAliasesUnified from "../components/ModelAliasesUnified";
|
||||
import BackgroundDegradationTab from "../components/BackgroundDegradationTab";
|
||||
|
||||
export default function SettingsRoutingPage() {
|
||||
const t = useTranslations("settings");
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<p className="text-sm text-text-muted">{t("routingSettingsIntro")}</p>
|
||||
<RoutingTab />
|
||||
<ModelRoutingSection />
|
||||
<ComboDefaultsTab />
|
||||
|
||||
@@ -365,6 +365,10 @@
|
||||
"apiKeyForCheck": "Api Key For Check",
|
||||
"cloudRequestTimeout": "Cloud Request Timeout",
|
||||
"showConfiguredOnly": "Show Configured Only",
|
||||
"showFreeOnly": "Free only",
|
||||
"addFirstProvider": "Add your first provider",
|
||||
"addFirstProviderDesc": "Connect an AI provider to start routing requests through OmniRoute. You can use free providers, API keys, or OAuth accounts.",
|
||||
"learnMore": "Learn more",
|
||||
"includeDomains": "Include Domains",
|
||||
"promptCache": "Prompt Cache",
|
||||
"cloudConnected": "Cloud Connected",
|
||||
@@ -405,7 +409,7 @@
|
||||
"expirationBannerExpiringSoonDesc": "Expiration Banner Expiring Soon Desc",
|
||||
"source": "Source",
|
||||
"failed": "Failed",
|
||||
"apiKeyMgmt": "Api Key Mgmt",
|
||||
"apiKeyMgmt": "API Key Management",
|
||||
"mcpQuickStartStep1": "Mcp Quick Start Step1",
|
||||
"runTest": "Run Test",
|
||||
"loadingFallbackChains": "Loading Fallback Chains",
|
||||
@@ -680,9 +684,16 @@
|
||||
"docs": "Docs",
|
||||
"issues": "Issues",
|
||||
"endpoints": "Endpoints",
|
||||
"endpointsSubtitle": "Your AI connection URLs",
|
||||
"apiManager": "API Key Manager",
|
||||
"apiManagerSubtitle": "Manage API keys and access",
|
||||
"logs": "Logs",
|
||||
"webhooks": "Webhooks",
|
||||
"webhooksSubtitle": "Get notified of events",
|
||||
"combosSubtitle": "Group providers for failover",
|
||||
"batchSubtitle": "Process multiple requests",
|
||||
"contextCavemanSubtitle": "Prompt compression",
|
||||
"contextRtkSubtitle": "Output filtering",
|
||||
"auditLog": "Audit Log",
|
||||
"shutdown": "Shutdown",
|
||||
"restart": "Restart",
|
||||
@@ -1013,6 +1024,8 @@
|
||||
"providersOverview": "Providers Overview",
|
||||
"configuredOf": "{configured} configured of {total} available providers",
|
||||
"noModelsAvailable": "No models available for this provider.",
|
||||
"noProvidersConfigured": "No providers configured yet",
|
||||
"addProvider": "Add a provider",
|
||||
"configureFirst": "Configure a connection first in {providers}",
|
||||
"configureProvider": "Configure Provider",
|
||||
"modelAvailable": "{count} model available",
|
||||
@@ -2734,7 +2747,6 @@
|
||||
"welcomeDesc": "OmniRoute is your local AI API proxy. It routes requests to multiple AI providers with load balancing, failover, and usage tracking.",
|
||||
"multiProvider": "Multi-Provider",
|
||||
"usageTracking": "Usage Tracking",
|
||||
"apiKeyMgmt": "API Key Mgmt",
|
||||
"securityDesc": "Set a password to protect your dashboard, or skip for now.",
|
||||
"providerDesc": "Connect your first AI provider. You can add more later.",
|
||||
"apiKeyRequired": "API Key (required)",
|
||||
@@ -2753,7 +2765,8 @@
|
||||
"failedSetPassword": "Failed to set password. Try again.",
|
||||
"failedAddProvider": "Failed to add provider. Try again.",
|
||||
"connectionError": "Connection error. Please try again.",
|
||||
"provider": "Provider"
|
||||
"provider": "Provider",
|
||||
"apiKeyHelp": "An API key is a password for AI services. Get one from your provider's website (e.g., platform.openai.com, console.anthropic.com)."
|
||||
},
|
||||
"providers": {
|
||||
"title": "Providers",
|
||||
@@ -3268,6 +3281,9 @@
|
||||
"routing": "Routing",
|
||||
"cache": "Cache",
|
||||
"resilience": "Resilience",
|
||||
"routingSettingsIntro": "Controls how your requests are routed, transformed, and sent to AI providers.",
|
||||
"resilienceSettingsIntro": "Automatic retry, cooldown, and fallback when providers fail.",
|
||||
"aiSettingsIntro": "AI-specific settings for thinking budget, model behavior, and compression.",
|
||||
"systemPrompt": "System Prompt",
|
||||
"thinkingBudget": "Thinking Budget",
|
||||
"proxy": "Proxy",
|
||||
@@ -3981,7 +3997,11 @@
|
||||
"tokensFiltered": "Tokens filtered",
|
||||
"filtersActive": "Filters active",
|
||||
"requests": "Requests",
|
||||
"avgSavings": "Avg savings"
|
||||
"avgSavings": "Avg savings",
|
||||
"simpleMode": "Simple",
|
||||
"advancedMode": "Advanced",
|
||||
"filterCatalog": "Filter Catalog",
|
||||
"filterCatalogDesc": "Available output filters by category"
|
||||
},
|
||||
"contextCombos": {
|
||||
"title": "Compression Combos",
|
||||
@@ -4024,6 +4044,8 @@
|
||||
"autoClarity": "Auto-Clarity Bypass",
|
||||
"bypassConditions": "Bypass conditions",
|
||||
"bypassConditionsList": "security, irreversible, clarification, order-sensitive",
|
||||
"simpleMode": "Simple",
|
||||
"advancedMode": "Advanced",
|
||||
"preview": {
|
||||
"lite": "Respond concise. Preserve technical terms, code, errors, URLs, and identifiers.",
|
||||
"full": "Respond terse and compact. Preserve all technical substance.",
|
||||
|
||||
34
src/shared/components/InfoTooltip.tsx
Normal file
34
src/shared/components/InfoTooltip.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/shared/utils/cn";
|
||||
|
||||
interface InfoTooltipProps {
|
||||
text: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function InfoTooltip({ text, className }: InfoTooltipProps) {
|
||||
return (
|
||||
<span className={cn("relative inline-flex group", className)}>
|
||||
<span
|
||||
className="material-symbols-outlined text-[16px] text-text-muted cursor-help"
|
||||
aria-label={text}
|
||||
>
|
||||
info
|
||||
</span>
|
||||
<span
|
||||
role="tooltip"
|
||||
className={cn(
|
||||
"absolute bottom-full left-1/2 -translate-x-1/2 mb-2",
|
||||
"px-2.5 py-1.5 text-xs font-medium text-white bg-gray-900/95 rounded-md shadow-lg",
|
||||
"whitespace-nowrap pointer-events-none",
|
||||
"border border-white/10",
|
||||
"opacity-0 group-hover:opacity-100 transition-opacity duration-150",
|
||||
"z-50"
|
||||
)}
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
64
src/shared/components/PresetSlider.tsx
Normal file
64
src/shared/components/PresetSlider.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/shared/utils/cn";
|
||||
|
||||
interface PresetSliderProps {
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
presets: { label: string; value: number }[];
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function PresetSlider({
|
||||
value,
|
||||
onChange,
|
||||
presets,
|
||||
min = 0,
|
||||
max = 100,
|
||||
step = 1,
|
||||
className,
|
||||
}: PresetSliderProps) {
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-3", className)}>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{presets.map((preset) => (
|
||||
<button
|
||||
key={preset.value}
|
||||
type="button"
|
||||
onClick={() => onChange(preset.value)}
|
||||
className={cn(
|
||||
"px-3 py-1.5 text-xs font-medium rounded-full transition-colors",
|
||||
"border",
|
||||
value === preset.value
|
||||
? "bg-primary text-white border-primary"
|
||||
: "bg-transparent text-text-muted border-black/10 dark:border-white/10 hover:border-primary/30 hover:text-text-main"
|
||||
)}
|
||||
>
|
||||
{preset.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
value={value}
|
||||
onChange={(e) => onChange(Number(e.target.value))}
|
||||
className={cn(
|
||||
"w-full h-1.5 rounded-full appearance-none cursor-pointer",
|
||||
"bg-black/10 dark:bg-white/10",
|
||||
"accent-primary"
|
||||
)}
|
||||
/>
|
||||
<div className="flex justify-between text-[10px] text-text-muted">
|
||||
<span>{min}</span>
|
||||
<span className="font-medium text-text-main">{value}</span>
|
||||
<span>{max}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -148,7 +148,12 @@ export default function Sidebar({
|
||||
|
||||
const resolveItem = (item: SidebarItemDefinition, hidden: Set<string>) => {
|
||||
if (hidden.has(item.id)) return null;
|
||||
return { ...item, label: getSidebarLabel(item.i18nKey, item.id) };
|
||||
const subtitle = item.subtitleKey ? getSidebarLabel(item.subtitleKey, "") : undefined;
|
||||
return {
|
||||
...item,
|
||||
label: getSidebarLabel(item.i18nKey, item.id),
|
||||
subtitle: subtitle || undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const hiddenSidebarSet = new Set(hiddenSidebarItems);
|
||||
@@ -321,7 +326,14 @@ export default function Sidebar({
|
||||
const content = (
|
||||
<>
|
||||
<span className={iconClassName}>{item.icon}</span>
|
||||
{!collapsed && <span className="text-sm font-medium truncate">{item.label}</span>}
|
||||
{!collapsed && (
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<span className="truncate text-sm font-medium">{item.label}</span>
|
||||
{item.subtitle && (
|
||||
<span className="truncate text-[10px] text-text-muted/60">{item.subtitle}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
const sharedProps = {
|
||||
|
||||
@@ -34,6 +34,8 @@ export { default as FilterBar } from "./FilterBar";
|
||||
export { default as ColumnToggle } from "./ColumnToggle";
|
||||
export { default as DataTable } from "./DataTable";
|
||||
export { default as NoAuthProviderCard } from "./NoAuthProviderCard";
|
||||
export { default as InfoTooltip } from "./InfoTooltip";
|
||||
export { default as PresetSlider } from "./PresetSlider";
|
||||
|
||||
// Layouts
|
||||
export * from "./layouts";
|
||||
|
||||
@@ -91,6 +91,7 @@ export interface SidebarItemDefinition {
|
||||
id: HideableSidebarItemId;
|
||||
href: string;
|
||||
i18nKey: string;
|
||||
subtitleKey?: string;
|
||||
icon: string;
|
||||
exact?: boolean;
|
||||
external?: boolean;
|
||||
@@ -131,10 +132,28 @@ const HOME_ITEMS: readonly SidebarItemDefinition[] = [
|
||||
];
|
||||
|
||||
const OMNI_PROXY_ITEMS: readonly SidebarItemDefinition[] = [
|
||||
{ id: "endpoints", href: "/dashboard/endpoint", i18nKey: "endpoints", icon: "api" },
|
||||
{ id: "api-manager", href: "/dashboard/api-manager", i18nKey: "apiManager", icon: "vpn_key" },
|
||||
{
|
||||
id: "endpoints",
|
||||
href: "/dashboard/endpoint",
|
||||
i18nKey: "endpoints",
|
||||
subtitleKey: "endpointsSubtitle",
|
||||
icon: "api",
|
||||
},
|
||||
{
|
||||
id: "api-manager",
|
||||
href: "/dashboard/api-manager",
|
||||
i18nKey: "apiManager",
|
||||
subtitleKey: "apiManagerSubtitle",
|
||||
icon: "vpn_key",
|
||||
},
|
||||
{ id: "providers", href: "/dashboard/providers", i18nKey: "providers", icon: "dns" },
|
||||
{ id: "combos", href: "/dashboard/combos", i18nKey: "combos", icon: "layers" },
|
||||
{
|
||||
id: "combos",
|
||||
href: "/dashboard/combos",
|
||||
i18nKey: "combos",
|
||||
subtitleKey: "combosSubtitle",
|
||||
icon: "layers",
|
||||
},
|
||||
{ id: "limits", href: "/dashboard/limits", i18nKey: "quotaTracker", icon: "tune" },
|
||||
];
|
||||
|
||||
@@ -148,12 +167,14 @@ const COMPRESSION_CONTEXT_GROUP: SidebarItemGroup = {
|
||||
id: "context-caveman",
|
||||
href: "/dashboard/context/caveman",
|
||||
i18nKey: "contextCaveman",
|
||||
subtitleKey: "contextCavemanSubtitle",
|
||||
icon: "compress",
|
||||
},
|
||||
{
|
||||
id: "context-rtk",
|
||||
href: "/dashboard/context/rtk",
|
||||
i18nKey: "contextRtk",
|
||||
subtitleKey: "contextRtkSubtitle",
|
||||
icon: "filter_alt",
|
||||
},
|
||||
{
|
||||
@@ -184,7 +205,13 @@ const INTEGRATIONS_GROUP: SidebarItemGroup = {
|
||||
titleFallback: "Integrations",
|
||||
items: [
|
||||
{ id: "api-endpoints", href: "/dashboard/api-endpoints", i18nKey: "apiEndpoints", icon: "api" },
|
||||
{ id: "webhooks", href: "/dashboard/webhooks", i18nKey: "webhooks", icon: "webhook" },
|
||||
{
|
||||
id: "webhooks",
|
||||
href: "/dashboard/webhooks",
|
||||
i18nKey: "webhooks",
|
||||
subtitleKey: "webhooksSubtitle",
|
||||
icon: "webhook",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -319,7 +346,13 @@ const BATCH_GROUP: SidebarItemGroup = {
|
||||
titleKey: "batchGroup",
|
||||
titleFallback: "Batch",
|
||||
items: [
|
||||
{ id: "batch", href: "/dashboard/batch", i18nKey: "batch", icon: "view_list" },
|
||||
{
|
||||
id: "batch",
|
||||
href: "/dashboard/batch",
|
||||
i18nKey: "batch",
|
||||
subtitleKey: "batchSubtitle",
|
||||
icon: "view_list",
|
||||
},
|
||||
{ id: "batch-files", href: "/dashboard/batch/files", i18nKey: "batchFiles", icon: "folder" },
|
||||
],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user