mirror of
https://github.com/decolua/9router.git
synced 2026-05-08 12:01:28 +00:00
Merge branch 'master' of https://github.com/decolua/9router
This commit is contained in:
@@ -191,11 +191,19 @@ export default function CLIToolsPageClient({ machineId }) {
|
||||
const mitmTools = Object.entries(MITM_TOOLS);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="mx-auto flex w-full max-w-5xl flex-col gap-6 px-1 sm:px-0">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h1 className="text-xl font-semibold text-text-main sm:text-2xl">CLI Tools</h1>
|
||||
<p className="text-sm text-text-muted">Configure local coding tools to use your 9Router providers.</p>
|
||||
</div>
|
||||
<div className="grid gap-3 sm:gap-4">
|
||||
{regularTools.map(([toolId, tool]) => renderToolCard(toolId, tool))}
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="grid gap-3 sm:gap-4">
|
||||
<div className="flex items-center gap-2 px-1">
|
||||
<span className="material-symbols-outlined text-[18px] text-primary">security</span>
|
||||
<h2 className="text-sm font-semibold text-text-main">MITM Tools</h2>
|
||||
</div>
|
||||
{mitmTools.map(([toolId, tool]) => (
|
||||
<MitmLinkCard key={toolId} tool={tool} />
|
||||
))}
|
||||
|
||||
@@ -231,8 +231,8 @@ export default function AntigravityToolCard({
|
||||
|
||||
return (
|
||||
<Card padding="xs" className="overflow-hidden">
|
||||
<div className="flex items-center justify-between hover:cursor-pointer" onClick={onToggle}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-start justify-between gap-3 hover:cursor-pointer sm:items-center" onClick={onToggle}>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div className="size-8 flex items-center justify-center shrink-0">
|
||||
<Image
|
||||
src="/providers/antigravity.png"
|
||||
@@ -245,7 +245,7 @@ export default function AntigravityToolCard({
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<h3 className="font-medium text-sm">{tool.name}</h3>
|
||||
{isRunning ? (
|
||||
<Badge variant="success" size="sm">Active</Badge>
|
||||
@@ -290,7 +290,7 @@ export default function AntigravityToolCard({
|
||||
</div>
|
||||
|
||||
{/* Start/Stop Button */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
|
||||
{isRunning ? (
|
||||
<button
|
||||
onClick={handleStop}
|
||||
@@ -322,39 +322,39 @@ export default function AntigravityToolCard({
|
||||
{/* When running: API Key + Model Mappings */}
|
||||
{isRunning && (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">API Key</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
|
||||
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">API Key</span>
|
||||
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||
{apiKeys.length > 0 ? (
|
||||
<select
|
||||
value={selectedApiKey}
|
||||
onChange={(e) => setSelectedApiKey(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"
|
||||
className="min-w-0 px-2 py-2 bg-surface rounded text-xs border border-border focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
|
||||
>
|
||||
{apiKeys.map((key) => <option key={key.id} value={key.key}>{key.key}</option>)}
|
||||
</select>
|
||||
) : (
|
||||
<span className="flex-1 text-xs text-text-muted px-2 py-1.5">
|
||||
<span className="min-w-0 rounded bg-surface/40 px-2 py-2 text-xs text-text-muted sm:py-1.5">
|
||||
{cloudEnabled ? "No API keys - Create one in Keys page" : "sk_9router (default)"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{tool.defaultModels.map((model) => (
|
||||
<div key={model.alias} className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">{model.name}</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<div key={model.alias} className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
|
||||
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">{model.name}</span>
|
||||
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||
<input
|
||||
type="text"
|
||||
value={modelMappings[model.alias] || ""}
|
||||
onChange={(e) => handleModelMappingChange(model.alias, e.target.value)}
|
||||
placeholder="provider/model-id"
|
||||
className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
className="min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
|
||||
/>
|
||||
<button
|
||||
onClick={() => openModelSelector(model.alias)}
|
||||
disabled={!hasActiveProviders}
|
||||
className={`px-2 py-1.5 rounded border text-xs transition-colors shrink-0 whitespace-nowrap ${hasActiveProviders ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}
|
||||
className={`rounded border px-2 py-2 text-xs transition-colors sm:py-1.5 whitespace-nowrap sm:shrink-0 ${hasActiveProviders ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}
|
||||
>
|
||||
Select
|
||||
</button>
|
||||
@@ -370,7 +370,7 @@ export default function AntigravityToolCard({
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="grid grid-cols-1 gap-2 sm:flex sm:items-center">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
|
||||
@@ -224,13 +224,13 @@ export default function ClaudeToolCard({
|
||||
|
||||
return (
|
||||
<Card padding="xs" className="overflow-hidden">
|
||||
<div className="flex items-center justify-between hover:cursor-pointer" onClick={onToggle}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-start justify-between gap-3 hover:cursor-pointer sm:items-center" onClick={onToggle}>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div className="size-8 flex items-center justify-center shrink-0">
|
||||
<Image src="/providers/claude.png" alt={tool.name} width={32} height={32} className="size-8 object-contain rounded-lg" sizes="32px" onError={(e) => { e.target.style.display = "none"; }} />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<h3 className="font-medium text-sm">{tool.name}</h3>
|
||||
{configStatus === "configured" && <span className="px-1.5 py-0.5 text-[10px] font-medium bg-green-500/10 text-green-600 dark:text-green-400 rounded-full">Connected</span>}
|
||||
{configStatus === "not_configured" && <span className="px-1.5 py-0.5 text-[10px] font-medium bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 rounded-full">Not configured</span>}
|
||||
@@ -292,25 +292,25 @@ export default function ClaudeToolCard({
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Current Base URL */}
|
||||
{claudeStatus?.settings?.env?.ANTHROPIC_BASE_URL && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">Current</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<span className="flex-1 px-2 py-1.5 text-xs text-text-muted truncate">
|
||||
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
|
||||
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Current</span>
|
||||
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||
<span className="min-w-0 truncate rounded bg-surface/40 px-2 py-2 text-xs text-text-muted sm:py-1.5">
|
||||
{claudeStatus.settings.env.ANTHROPIC_BASE_URL}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Base URL */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">Base URL</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
|
||||
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Base URL</span>
|
||||
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||
<input
|
||||
type="text"
|
||||
value={getDisplayUrl()}
|
||||
onChange={(e) => setCustomBaseUrl(e.target.value)}
|
||||
placeholder="https://.../v1"
|
||||
className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
className="min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
|
||||
/>
|
||||
{customBaseUrl && customBaseUrl !== baseUrl && (
|
||||
<button onClick={() => setCustomBaseUrl("")} className="p-1 text-text-muted hover:text-primary rounded transition-colors" title="Reset to default">
|
||||
@@ -320,15 +320,15 @@ export default function ClaudeToolCard({
|
||||
</div>
|
||||
|
||||
{/* API Key */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">API Key</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
|
||||
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">API Key</span>
|
||||
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||
{apiKeys.length > 0 ? (
|
||||
<select value={selectedApiKey} onChange={(e) => setSelectedApiKey(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">
|
||||
<select value={selectedApiKey} onChange={(e) => setSelectedApiKey(e.target.value)} className="min-w-0 px-2 py-2 bg-surface rounded text-xs border border-border focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5">
|
||||
{apiKeys.map((key) => <option key={key.id} value={key.key}>{key.key}</option>)}
|
||||
</select>
|
||||
) : (
|
||||
<span className="flex-1 text-xs text-text-muted px-2 py-1.5">
|
||||
<span className="min-w-0 rounded bg-surface/40 px-2 py-2 text-xs text-text-muted sm:py-1.5">
|
||||
{cloudEnabled ? "No API keys - Create one in Keys page" : "sk_9router (default)"}
|
||||
</span>
|
||||
)}
|
||||
@@ -336,19 +336,19 @@ export default function ClaudeToolCard({
|
||||
|
||||
{/* Model Mappings */}
|
||||
{tool.defaultModels.map((model) => (
|
||||
<div key={model.alias} className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">{model.name}</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<input type="text" value={modelMappings[model.alias] || ""} onChange={(e) => onModelMappingChange(model.alias, e.target.value)} placeholder="provider/model-id" className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" />
|
||||
<button onClick={() => openModelSelector(model.alias)} disabled={!hasActiveProviders} className={`px-2 py-1.5 rounded border text-xs transition-colors shrink-0 whitespace-nowrap ${hasActiveProviders ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}>Select Model</button>
|
||||
<div key={model.alias} className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
|
||||
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">{model.name}</span>
|
||||
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||
<input type="text" value={modelMappings[model.alias] || ""} onChange={(e) => onModelMappingChange(model.alias, e.target.value)} placeholder="provider/model-id" className="min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5" />
|
||||
<button onClick={() => openModelSelector(model.alias)} disabled={!hasActiveProviders} className={`rounded border px-2 py-2 text-xs transition-colors sm:py-1.5 whitespace-nowrap sm:shrink-0 ${hasActiveProviders ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}>Select Model</button>
|
||||
{modelMappings[model.alias] && <button onClick={() => onModelMappingChange(model.alias, "")} className="p-1 text-text-muted hover:text-red-500 rounded transition-colors" title="Clear"><span className="material-symbols-outlined text-[14px]">close</span></button>}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* CC Filter Naming */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">Filter naming</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
|
||||
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Filter naming</span>
|
||||
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||
<label className="flex items-center gap-1.5 cursor-pointer select-none">
|
||||
<input type="checkbox" checked={ccFilterNaming} onChange={handleCcFilterNamingToggle} className="w-3.5 h-3.5 accent-primary cursor-pointer" />
|
||||
<span className="text-xs text-text-muted">Filter naming requests</span>
|
||||
@@ -366,7 +366,7 @@ export default function ClaudeToolCard({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="grid grid-cols-1 gap-2 sm:flex sm:items-center">
|
||||
<Button variant="primary" size="sm" onClick={handleApplySettings} disabled={!hasActiveProviders} loading={applying}>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">save</span>Apply
|
||||
</Button>
|
||||
|
||||
@@ -192,13 +192,13 @@ model = "${effectiveSubagentModel}"
|
||||
|
||||
return (
|
||||
<Card padding="xs" className="overflow-hidden">
|
||||
<div className="flex items-center justify-between hover:cursor-pointer" onClick={onToggle}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-start justify-between gap-3 hover:cursor-pointer sm:items-center" onClick={onToggle}>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div className="size-8 flex items-center justify-center shrink-0">
|
||||
<Image src="/providers/codex.png" alt={tool.name} width={32} height={32} className="size-8 object-contain rounded-lg" sizes="32px" onError={(e) => { e.target.style.display = "none"; }} />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<h3 className="font-medium text-sm">{tool.name}</h3>
|
||||
{configStatus === "configured" && <span className="px-1.5 py-0.5 text-[10px] font-medium bg-green-500/10 text-green-600 dark:text-green-400 rounded-full">Connected</span>}
|
||||
{configStatus === "not_configured" && <span className="px-1.5 py-0.5 text-[10px] font-medium bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 rounded-full">Not configured</span>}
|
||||
@@ -269,10 +269,10 @@ model = "${effectiveSubagentModel}"
|
||||
const parsed = codexStatus.config.match(/base_url\s*=\s*"([^"]+)"/);
|
||||
const currentBaseUrl = parsed ? parsed[1] : null;
|
||||
return currentBaseUrl ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">Current</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<span className="flex-1 px-2 py-1.5 text-xs text-text-muted truncate">
|
||||
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
|
||||
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Current</span>
|
||||
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||
<span className="min-w-0 truncate rounded bg-surface/40 px-2 py-2 text-xs text-text-muted sm:py-1.5">
|
||||
{currentBaseUrl}
|
||||
</span>
|
||||
</div>
|
||||
@@ -280,15 +280,15 @@ model = "${effectiveSubagentModel}"
|
||||
})()}
|
||||
|
||||
{/* Base URL */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">Base URL</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
|
||||
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Base URL</span>
|
||||
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||
<input
|
||||
type="text"
|
||||
value={getDisplayUrl()}
|
||||
onChange={(e) => setCustomBaseUrl(e.target.value)}
|
||||
placeholder="https://.../v1"
|
||||
className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
className="min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
|
||||
/>
|
||||
{customBaseUrl && customBaseUrl !== `${baseUrl}/v1` && (
|
||||
<button onClick={() => setCustomBaseUrl("")} className="p-1 text-text-muted hover:text-primary rounded transition-colors" title="Reset to default">
|
||||
@@ -298,44 +298,44 @@ model = "${effectiveSubagentModel}"
|
||||
</div>
|
||||
|
||||
{/* API Key */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">API Key</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
|
||||
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">API Key</span>
|
||||
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||
{apiKeys.length > 0 ? (
|
||||
<select value={selectedApiKey} onChange={(e) => setSelectedApiKey(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">
|
||||
<select value={selectedApiKey} onChange={(e) => setSelectedApiKey(e.target.value)} className="min-w-0 px-2 py-2 bg-surface rounded text-xs border border-border focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5">
|
||||
{apiKeys.map((key) => <option key={key.id} value={key.key}>{key.key}</option>)}
|
||||
</select>
|
||||
) : (
|
||||
<span className="flex-1 text-xs text-text-muted px-2 py-1.5">
|
||||
<span className="min-w-0 rounded bg-surface/40 px-2 py-2 text-xs text-text-muted sm:py-1.5">
|
||||
{cloudEnabled ? "No API keys - Create one in Keys page" : "sk_9router (default)"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Model */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">Model</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<input type="text" value={selectedModel} onChange={(e) => setSelectedModel(e.target.value)} placeholder="provider/model-id" className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" />
|
||||
<button onClick={() => setModalOpen(true)} disabled={!activeProviders?.length} className={`px-2 py-1.5 rounded border text-xs transition-colors shrink-0 whitespace-nowrap ${activeProviders?.length ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}>Select Model</button>
|
||||
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
|
||||
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Model</span>
|
||||
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||
<input type="text" value={selectedModel} onChange={(e) => setSelectedModel(e.target.value)} placeholder="provider/model-id" className="min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5" />
|
||||
<button onClick={() => setModalOpen(true)} disabled={!activeProviders?.length} className={`rounded border px-2 py-2 text-xs transition-colors sm:py-1.5 whitespace-nowrap sm:shrink-0 ${activeProviders?.length ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}>Select Model</button>
|
||||
{selectedModel && <button onClick={() => setSelectedModel("")} className="p-1 text-text-muted hover:text-red-500 rounded transition-colors" title="Clear"><span className="material-symbols-outlined text-[14px]">close</span></button>}
|
||||
</div>
|
||||
|
||||
{/* Subagent Model */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">Subagent Model</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
|
||||
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Subagent Model</span>
|
||||
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||
<input
|
||||
type="text"
|
||||
value={subagentModel}
|
||||
onChange={(e) => setSubagentModel(e.target.value)}
|
||||
placeholder={selectedModel || "provider/model-id (defaults to main model)"}
|
||||
className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
className="min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setSubagentModalOpen(true)}
|
||||
disabled={!activeProviders?.length}
|
||||
className={`px-2 py-1.5 rounded border text-xs transition-colors shrink-0 whitespace-nowrap ${activeProviders?.length ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}
|
||||
className={`rounded border px-2 py-2 text-xs transition-colors sm:py-1.5 whitespace-nowrap sm:shrink-0 ${activeProviders?.length ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}
|
||||
>
|
||||
Select Model
|
||||
</button>
|
||||
@@ -358,7 +358,7 @@ model = "${effectiveSubagentModel}"
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="grid grid-cols-1 gap-2 sm:flex sm:items-center">
|
||||
<Button variant="primary" size="sm" onClick={handleApplySettings} disabled={(!selectedApiKey && (cloudEnabled && apiKeys.length > 0)) || !selectedModel} loading={applying}>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">save</span>Apply
|
||||
</Button>
|
||||
|
||||
@@ -234,17 +234,17 @@ export default function CopilotToolCard({ tool, isExpanded, onToggle, baseUrl, a
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-[1fr_auto_auto] sm:items-center">
|
||||
<input
|
||||
type="text"
|
||||
value={modelInput}
|
||||
onChange={(e) => setModelInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && addModel()}
|
||||
placeholder="provider/model-id"
|
||||
className="flex-1 px-3 py-2 bg-bg-secondary rounded-lg text-sm border border-border focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
className="min-w-0 px-3 py-2 bg-bg-secondary rounded-lg text-sm border border-border focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
/>
|
||||
<button onClick={() => setModalOpen(true)} disabled={!activeProviders?.length} className={`px-3 py-2 rounded-lg border text-sm transition-colors shrink-0 ${activeProviders?.length ? "bg-bg-secondary border-border hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}>Select</button>
|
||||
<button onClick={addModel} disabled={!modelInput.trim()} className="px-3 py-2 rounded-lg border text-sm bg-bg-secondary border-border hover:border-primary transition-colors shrink-0 disabled:opacity-50" title="Add model">
|
||||
<button onClick={() => setModalOpen(true)} disabled={!activeProviders?.length} className={`rounded-lg border px-3 py-2 text-sm transition-colors sm:shrink-0 ${activeProviders?.length ? "bg-bg-secondary border-border hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}>Select</button>
|
||||
<button onClick={addModel} disabled={!modelInput.trim()} className="rounded-lg border border-border bg-bg-secondary px-3 py-2 text-sm transition-colors hover:border-primary disabled:opacity-50 sm:shrink-0" title="Add model">
|
||||
<span className="material-symbols-outlined text-[16px]">add</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -258,7 +258,7 @@ export default function CopilotToolCard({ tool, isExpanded, onToggle, baseUrl, a
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="grid grid-cols-1 gap-2 sm:flex sm:items-center">
|
||||
<Button variant="primary" size="sm" onClick={handleApply} disabled={modelList.length === 0} loading={applying}>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">save</span>Apply
|
||||
</Button>
|
||||
|
||||
@@ -219,13 +219,13 @@ export default function DroidToolCard({
|
||||
|
||||
return (
|
||||
<Card padding="xs" className="overflow-hidden">
|
||||
<div className="flex items-center justify-between hover:cursor-pointer" onClick={onToggle}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-start justify-between gap-3 hover:cursor-pointer sm:items-center" onClick={onToggle}>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div className="size-8 flex items-center justify-center shrink-0">
|
||||
<Image src="/providers/droid.png" alt={tool.name} width={32} height={32} className="size-8 object-contain rounded-lg" sizes="32px" onError={(e) => { e.target.style.display = "none"; }} />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<h3 className="font-medium text-sm">{tool.name}</h3>
|
||||
{configStatus === "configured" && <span className="px-1.5 py-0.5 text-[10px] font-medium bg-green-500/10 text-green-600 dark:text-green-400 rounded-full">Connected</span>}
|
||||
{configStatus === "not_configured" && <span className="px-1.5 py-0.5 text-[10px] font-medium bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 rounded-full">Not configured</span>}
|
||||
@@ -287,25 +287,25 @@ export default function DroidToolCard({
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Current Base URL */}
|
||||
{droidStatus?.settings?.customModels?.find(m => m.id?.startsWith("custom:9Router"))?.baseUrl && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">Current</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<span className="flex-1 px-2 py-1.5 text-xs text-text-muted truncate">
|
||||
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
|
||||
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Current</span>
|
||||
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||
<span className="min-w-0 truncate rounded bg-surface/40 px-2 py-2 text-xs text-text-muted sm:py-1.5">
|
||||
{droidStatus.settings.customModels.find(m => m.id?.startsWith("custom:9Router")).baseUrl}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Base URL */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">Base URL</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
|
||||
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Base URL</span>
|
||||
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||
<input
|
||||
type="text"
|
||||
value={getDisplayUrl()}
|
||||
onChange={(e) => setCustomBaseUrl(e.target.value)}
|
||||
placeholder="https://.../v1"
|
||||
className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
className="min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
|
||||
/>
|
||||
{customBaseUrl && customBaseUrl !== baseUrl && (
|
||||
<button onClick={() => setCustomBaseUrl("")} className="p-1 text-text-muted hover:text-primary rounded transition-colors" title="Reset to default">
|
||||
@@ -315,26 +315,26 @@ export default function DroidToolCard({
|
||||
</div>
|
||||
|
||||
{/* API Key */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">API Key</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
|
||||
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">API Key</span>
|
||||
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||
{apiKeys.length > 0 ? (
|
||||
<select value={selectedApiKey} onChange={(e) => setSelectedApiKey(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">
|
||||
<select value={selectedApiKey} onChange={(e) => setSelectedApiKey(e.target.value)} className="min-w-0 px-2 py-2 bg-surface rounded text-xs border border-border focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5">
|
||||
{apiKeys.map((key) => <option key={key.id} value={key.key}>{key.key}</option>)}
|
||||
</select>
|
||||
) : (
|
||||
<span className="flex-1 text-xs text-text-muted px-2 py-1.5">
|
||||
<span className="min-w-0 rounded bg-surface/40 px-2 py-2 text-xs text-text-muted sm:py-1.5">
|
||||
{cloudEnabled ? "No API keys - Create one in Keys page" : "sk_9router (default)"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Models */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">
|
||||
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
|
||||
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">
|
||||
Models {modelList.length > 0 && <span className="text-primary">({modelList.length})</span>}
|
||||
</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||
<div className="flex-1 flex flex-col gap-1">
|
||||
{/* Model list */}
|
||||
{modelList.length > 0 && (
|
||||
@@ -357,7 +357,7 @@ export default function DroidToolCard({
|
||||
onChange={(e) => setModelInput(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); addModel(); } }}
|
||||
placeholder="provider/model-id"
|
||||
className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
className="min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setModalOpen(true)}
|
||||
@@ -381,7 +381,7 @@ export default function DroidToolCard({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="grid grid-cols-1 gap-2 sm:flex sm:items-center">
|
||||
<Button variant="primary" size="sm" onClick={handleApplySettings} disabled={modelList.length === 0} loading={applying}>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">save</span>Apply
|
||||
</Button>
|
||||
|
||||
@@ -177,13 +177,13 @@ export default function HermesToolCard({
|
||||
|
||||
return (
|
||||
<Card padding="xs" className="overflow-hidden">
|
||||
<div className="flex items-center justify-between hover:cursor-pointer" onClick={onToggle}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-start justify-between gap-3 hover:cursor-pointer sm:items-center" onClick={onToggle}>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div className="size-8 flex items-center justify-center shrink-0">
|
||||
<Image src="/providers/hermes.png" alt={tool.name} width={32} height={32} className="size-8 object-contain rounded-lg" sizes="32px" onError={(e) => { e.target.style.display = "none"; }} />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<h3 className="font-medium text-sm">{tool.name}</h3>
|
||||
{configStatus === "configured" && <span className="px-1.5 py-0.5 text-[10px] font-medium bg-green-500/10 text-green-600 dark:text-green-400 rounded-full">Connected</span>}
|
||||
{configStatus === "not_configured" && <span className="px-1.5 py-0.5 text-[10px] font-medium bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 rounded-full">Not configured</span>}
|
||||
@@ -228,24 +228,24 @@ export default function HermesToolCard({
|
||||
<>
|
||||
<div className="flex flex-col gap-2">
|
||||
{hermesStatus?.settings?.model?.base_url && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">Current</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<span className="flex-1 px-2 py-1.5 text-xs text-text-muted truncate">
|
||||
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
|
||||
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Current</span>
|
||||
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||
<span className="min-w-0 truncate rounded bg-surface/40 px-2 py-2 text-xs text-text-muted sm:py-1.5">
|
||||
{hermesStatus.settings.model.base_url}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">Base URL</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
|
||||
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Base URL</span>
|
||||
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||
<input
|
||||
type="text"
|
||||
value={getEffectiveBaseUrl()}
|
||||
onChange={(e) => setCustomBaseUrl(e.target.value)}
|
||||
placeholder="https://.../v1"
|
||||
className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
className="min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
|
||||
/>
|
||||
{customBaseUrl && customBaseUrl !== baseUrl && (
|
||||
<button onClick={() => setCustomBaseUrl("")} className="p-1 text-text-muted hover:text-primary rounded transition-colors" title="Reset to default">
|
||||
@@ -254,25 +254,25 @@ export default function HermesToolCard({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">API Key</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
|
||||
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">API Key</span>
|
||||
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||
{apiKeys.length > 0 ? (
|
||||
<select value={selectedApiKey} onChange={(e) => setSelectedApiKey(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">
|
||||
<select value={selectedApiKey} onChange={(e) => setSelectedApiKey(e.target.value)} className="min-w-0 px-2 py-2 bg-surface rounded text-xs border border-border focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5">
|
||||
{apiKeys.map((key) => <option key={key.id} value={key.key}>{key.key}</option>)}
|
||||
</select>
|
||||
) : (
|
||||
<span className="flex-1 text-xs text-text-muted px-2 py-1.5">
|
||||
<span className="min-w-0 rounded bg-surface/40 px-2 py-2 text-xs text-text-muted sm:py-1.5">
|
||||
{cloudEnabled ? "No API keys - Create one in Keys page" : "sk_9router (default)"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">Default Model</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<input type="text" value={selectedModel} onChange={(e) => setSelectedModel(e.target.value)} placeholder="provider/model-id" className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" />
|
||||
<button onClick={() => setModalOpen(true)} disabled={!hasActiveProviders} className={`px-2 py-1.5 rounded border text-xs transition-colors shrink-0 whitespace-nowrap ${hasActiveProviders ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}>Select</button>
|
||||
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
|
||||
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Default Model</span>
|
||||
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||
<input type="text" value={selectedModel} onChange={(e) => setSelectedModel(e.target.value)} placeholder="provider/model-id" className="min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5" />
|
||||
<button onClick={() => setModalOpen(true)} disabled={!hasActiveProviders} className={`rounded border px-2 py-2 text-xs transition-colors sm:py-1.5 whitespace-nowrap sm:shrink-0 ${hasActiveProviders ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}>Select</button>
|
||||
{selectedModel && <button onClick={() => setSelectedModel("")} className="p-1 text-text-muted hover:text-red-500 rounded transition-colors" title="Clear"><span className="material-symbols-outlined text-[14px]">close</span></button>}
|
||||
</div>
|
||||
</div>
|
||||
@@ -284,7 +284,7 @@ export default function HermesToolCard({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="grid grid-cols-1 gap-2 sm:flex sm:items-center">
|
||||
<Button variant="primary" size="sm" onClick={handleApply} disabled={!selectedModel} loading={applying}>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">save</span>Apply
|
||||
</Button>
|
||||
|
||||
@@ -122,8 +122,8 @@ export default function MitmServerCard({ apiKeys, cloudEnabled, onStatusChange }
|
||||
<Card padding="sm" className="border-primary/20 bg-primary/5">
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<span className="material-symbols-outlined text-primary text-[20px]">security</span>
|
||||
<span className="font-semibold text-sm text-text-main">MITM Server</span>
|
||||
{isRunning ? (
|
||||
@@ -132,7 +132,7 @@ export default function MitmServerCard({ apiKeys, cloudEnabled, onStatusChange }
|
||||
<Badge variant="default" size="sm">Stopped</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-xs text-text-muted" data-i18n-skip="true">
|
||||
<div className="flex flex-wrap items-center gap-1 text-xs text-text-muted" data-i18n-skip="true">
|
||||
{[
|
||||
{ label: "Cert", ok: status?.certExists },
|
||||
{ label: "Trusted", ok: status?.certTrusted },
|
||||
@@ -160,9 +160,9 @@ export default function MitmServerCard({ apiKeys, cloudEnabled, onStatusChange }
|
||||
|
||||
{/* Base URL + API Key — same row pattern as Claude Code / cli-tools */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">9Router Base URL</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<div className="grid gap-1 sm:grid-cols-[8rem_auto_1fr] sm:items-center sm:gap-2">
|
||||
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">9Router Base URL</span>
|
||||
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||
<input
|
||||
type="text"
|
||||
value={mitmRouterBaseUrl}
|
||||
@@ -173,9 +173,9 @@ export default function MitmServerCard({ apiKeys, cloudEnabled, onStatusChange }
|
||||
/>
|
||||
</div>
|
||||
{!isRunning && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">API Key</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<div className="grid gap-1 sm:grid-cols-[8rem_auto_1fr] sm:items-center sm:gap-2">
|
||||
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">API Key</span>
|
||||
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||
<input
|
||||
type="text"
|
||||
list="mitm-api-keys"
|
||||
@@ -196,12 +196,12 @@ export default function MitmServerCard({ apiKeys, cloudEnabled, onStatusChange }
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex items-center gap-2 flex-wrap" data-i18n-skip="true">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center" data-i18n-skip="true">
|
||||
{status?.certExists && !status?.certTrusted && (
|
||||
<button
|
||||
onClick={() => handleAction("trust-cert")}
|
||||
disabled={loading}
|
||||
className="px-4 py-1.5 rounded-lg bg-yellow-500/10 border border-yellow-500/30 text-yellow-600 font-medium text-xs flex items-center gap-1.5 hover:bg-yellow-500/20 transition-colors disabled:opacity-50"
|
||||
className="flex w-full items-center justify-center gap-1.5 rounded-lg border border-yellow-500/30 bg-yellow-500/10 px-4 py-2 text-xs font-medium text-yellow-600 transition-colors hover:bg-yellow-500/20 disabled:opacity-50 sm:w-auto sm:py-1.5"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">verified_user</span>
|
||||
Trust Cert
|
||||
@@ -211,7 +211,7 @@ export default function MitmServerCard({ apiKeys, cloudEnabled, onStatusChange }
|
||||
<button
|
||||
onClick={() => handleAction("stop")}
|
||||
disabled={loading}
|
||||
className="px-4 py-1.5 rounded-lg bg-red-500/10 border border-red-500/30 text-red-500 font-medium text-xs flex items-center gap-1.5 hover:bg-red-500/20 transition-colors disabled:opacity-50"
|
||||
className="flex w-full items-center justify-center gap-1.5 rounded-lg border border-red-500/30 bg-red-500/10 px-4 py-2 text-xs font-medium text-red-500 transition-colors hover:bg-red-500/20 disabled:opacity-50 sm:w-auto sm:py-1.5"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">stop_circle</span>
|
||||
Stop Server
|
||||
@@ -220,7 +220,7 @@ export default function MitmServerCard({ apiKeys, cloudEnabled, onStatusChange }
|
||||
<button
|
||||
onClick={() => handleAction("start")}
|
||||
disabled={loading || (isWindows && !isAdmin)}
|
||||
className="px-4 py-1.5 rounded-lg bg-primary/10 border border-primary/30 text-primary font-medium text-xs flex items-center gap-1.5 hover:bg-primary/20 transition-colors disabled:opacity-50"
|
||||
className="flex w-full items-center justify-center gap-1.5 rounded-lg border border-primary/30 bg-primary/10 px-4 py-2 text-xs font-medium text-primary transition-colors hover:bg-primary/20 disabled:opacity-50 sm:w-auto sm:py-1.5"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">play_circle</span>
|
||||
Start Server
|
||||
@@ -252,7 +252,7 @@ export default function MitmServerCard({ apiKeys, cloudEnabled, onStatusChange }
|
||||
{/* Password Modal */}
|
||||
{showPasswordModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
||||
<div className="bg-surface border border-border rounded-xl p-6 w-full max-w-sm flex flex-col gap-4 shadow-xl">
|
||||
<div className="mx-4 flex w-full max-w-sm flex-col gap-4 rounded-xl border border-border bg-surface p-5 shadow-xl sm:p-6">
|
||||
<h3 className="font-semibold text-text-main">Sudo Password Required</h3>
|
||||
<div className="flex items-start gap-3 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
|
||||
<span className="material-symbols-outlined text-yellow-500 text-[20px]">warning</span>
|
||||
|
||||
@@ -130,8 +130,8 @@ export default function MitmToolCard({
|
||||
return (
|
||||
<>
|
||||
<Card padding="xs" className="overflow-hidden">
|
||||
<div className="flex items-center justify-between hover:cursor-pointer" onClick={onToggle}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-start justify-between gap-3 hover:cursor-pointer sm:items-center" onClick={onToggle}>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div className="size-8 flex items-center justify-center shrink-0">
|
||||
<Image
|
||||
src={tool.image}
|
||||
@@ -144,7 +144,7 @@ export default function MitmToolCard({
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h3 className="font-medium text-sm">{tool.name}</h3>
|
||||
{!serverRunning ? (
|
||||
<Badge variant="default" size="sm">Server off</Badge>
|
||||
@@ -154,7 +154,7 @@ export default function MitmToolCard({
|
||||
<Badge variant="warning" size="sm">DNS off</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-text-muted">Intercept {tool.name} requests via MITM proxy</p>
|
||||
<p className="text-xs text-text-muted sm:truncate">Intercept {tool.name} requests via MITM proxy</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className={`material-symbols-outlined text-text-muted text-[20px] transition-transform ${isExpanded ? "rotate-180" : ""}`}>
|
||||
@@ -191,9 +191,9 @@ export default function MitmToolCard({
|
||||
{tool.defaultModels?.length > 0 && (
|
||||
<div className="flex flex-col gap-2">
|
||||
{tool.defaultModels.map((model) => (
|
||||
<div key={model.alias} className="flex items-center gap-2">
|
||||
<span className="w-36 shrink-0 text-xs font-semibold text-text-main text-right">{model.name}</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<div key={model.alias} className="grid gap-1.5 sm:grid-cols-[9rem_auto_1fr_auto_auto] sm:items-center sm:gap-2">
|
||||
<span className="text-xs font-semibold text-text-main sm:text-right">{model.name}</span>
|
||||
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||
<input
|
||||
type="text"
|
||||
value={modelMappings[model.alias] || ""}
|
||||
@@ -201,12 +201,12 @@ export default function MitmToolCard({
|
||||
onBlur={(e) => handleMappingBlur(model.alias, e.target.value)}
|
||||
placeholder="provider/model-id"
|
||||
disabled={!dnsActive}
|
||||
className={`flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 ${!dnsActive ? "opacity-50 cursor-not-allowed" : ""}`}
|
||||
className={`min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5 ${!dnsActive ? "opacity-50 cursor-not-allowed" : ""}`}
|
||||
/>
|
||||
<button
|
||||
onClick={() => openModelSelector(model.alias)}
|
||||
disabled={!hasActiveProviders || !dnsActive}
|
||||
className={`px-2 py-1.5 rounded border text-xs transition-colors shrink-0 ${hasActiveProviders && dnsActive ? "bg-surface border-border hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}
|
||||
className={`rounded border px-2 py-2 text-xs transition-colors sm:py-1.5 ${hasActiveProviders && dnsActive ? "bg-surface border-border hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}
|
||||
>
|
||||
Select
|
||||
</button>
|
||||
@@ -232,12 +232,12 @@ export default function MitmToolCard({
|
||||
)}
|
||||
|
||||
{/* Start / Stop DNS button */}
|
||||
<div className="flex flex-col gap-2 items-start">
|
||||
<div className="flex flex-col gap-2 sm:items-start">
|
||||
{dnsActive ? (
|
||||
<button
|
||||
onClick={handleDnsToggle}
|
||||
disabled={!serverRunning || loading}
|
||||
className="px-4 py-1.5 rounded-lg bg-red-500/10 border border-red-500/30 text-red-500 font-medium text-xs flex items-center gap-1.5 hover:bg-red-500/20 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="flex w-full items-center justify-center gap-1.5 rounded-lg border border-red-500/30 bg-red-500/10 px-4 py-2 text-xs font-medium text-red-500 transition-colors hover:bg-red-500/20 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto sm:py-1.5"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">stop_circle</span>
|
||||
Stop DNS
|
||||
@@ -246,7 +246,7 @@ export default function MitmToolCard({
|
||||
<button
|
||||
onClick={handleDnsToggle}
|
||||
disabled={!serverRunning || loading}
|
||||
className="px-4 py-1.5 rounded-lg bg-primary/10 border border-primary/30 text-primary font-medium text-xs flex items-center gap-1.5 hover:bg-primary/20 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="flex w-full items-center justify-center gap-1.5 rounded-lg border border-primary/30 bg-primary/10 px-4 py-2 text-xs font-medium text-primary transition-colors hover:bg-primary/20 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto sm:py-1.5"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">play_circle</span>
|
||||
Start DNS
|
||||
@@ -268,7 +268,7 @@ export default function MitmToolCard({
|
||||
{/* Password Modal */}
|
||||
{showPasswordModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
||||
<div className="bg-surface border border-border rounded-xl p-6 w-full max-w-sm flex flex-col gap-4 shadow-xl">
|
||||
<div className="mx-4 flex w-full max-w-sm flex-col gap-4 rounded-xl border border-border bg-surface p-5 shadow-xl sm:p-6">
|
||||
<h3 className="font-semibold text-text-main">Sudo Password Required</h3>
|
||||
<div className="flex items-start gap-3 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
|
||||
<span className="material-symbols-outlined text-yellow-500 text-[20px]">warning</span>
|
||||
|
||||
@@ -226,13 +226,13 @@ export default function OpenClawToolCard({
|
||||
|
||||
return (
|
||||
<Card padding="xs" className="overflow-hidden">
|
||||
<div className="flex items-center justify-between hover:cursor-pointer" onClick={onToggle}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-start justify-between gap-3 hover:cursor-pointer sm:items-center" onClick={onToggle}>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div className="size-8 flex items-center justify-center shrink-0">
|
||||
<Image src="/providers/openclaw.png" alt={tool.name} width={32} height={32} className="size-8 object-contain rounded-lg" sizes="32px" onError={(e) => { e.target.style.display = "none"; }} />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<h3 className="font-medium text-sm">{tool.name}</h3>
|
||||
{configStatus === "configured" && <span className="px-1.5 py-0.5 text-[10px] font-medium bg-green-500/10 text-green-600 dark:text-green-400 rounded-full">Connected</span>}
|
||||
{configStatus === "not_configured" && <span className="px-1.5 py-0.5 text-[10px] font-medium bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 rounded-full">Not configured</span>}
|
||||
@@ -278,25 +278,25 @@ export default function OpenClawToolCard({
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Current Base URL */}
|
||||
{openclawStatus?.settings?.models?.providers?.["9router"]?.baseUrl && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">Current</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<span className="flex-1 px-2 py-1.5 text-xs text-text-muted truncate">
|
||||
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
|
||||
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Current</span>
|
||||
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||
<span className="min-w-0 truncate rounded bg-surface/40 px-2 py-2 text-xs text-text-muted sm:py-1.5">
|
||||
{openclawStatus.settings.models.providers["9router"].baseUrl}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Base URL */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">Base URL</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
|
||||
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Base URL</span>
|
||||
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||
<input
|
||||
type="text"
|
||||
value={getDisplayUrl()}
|
||||
onChange={(e) => setCustomBaseUrl(e.target.value)}
|
||||
placeholder="https://.../v1"
|
||||
className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
className="min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
|
||||
/>
|
||||
{customBaseUrl && customBaseUrl !== baseUrl && (
|
||||
<button onClick={() => setCustomBaseUrl("")} className="p-1 text-text-muted hover:text-primary rounded transition-colors" title="Reset to default">
|
||||
@@ -306,26 +306,26 @@ export default function OpenClawToolCard({
|
||||
</div>
|
||||
|
||||
{/* API Key */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">API Key</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
|
||||
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">API Key</span>
|
||||
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||
{apiKeys.length > 0 ? (
|
||||
<select value={selectedApiKey} onChange={(e) => setSelectedApiKey(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">
|
||||
<select value={selectedApiKey} onChange={(e) => setSelectedApiKey(e.target.value)} className="min-w-0 px-2 py-2 bg-surface rounded text-xs border border-border focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5">
|
||||
{apiKeys.map((key) => <option key={key.id} value={key.key}>{key.key}</option>)}
|
||||
</select>
|
||||
) : (
|
||||
<span className="flex-1 text-xs text-text-muted px-2 py-1.5">
|
||||
<span className="min-w-0 rounded bg-surface/40 px-2 py-2 text-xs text-text-muted sm:py-1.5">
|
||||
{cloudEnabled ? "No API keys - Create one in Keys page" : "sk_9router (default)"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Default Model */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">Default Model</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<input type="text" value={selectedModel} onChange={(e) => setSelectedModel(e.target.value)} placeholder="provider/model-id" className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" />
|
||||
<button onClick={() => { setAgentModalFor(null); setModalOpen(true); }} disabled={!hasActiveProviders} className={`px-2 py-1.5 rounded border text-xs transition-colors shrink-0 whitespace-nowrap ${hasActiveProviders ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}>Select</button>
|
||||
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
|
||||
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Default Model</span>
|
||||
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||
<input type="text" value={selectedModel} onChange={(e) => setSelectedModel(e.target.value)} placeholder="provider/model-id" className="min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5" />
|
||||
<button onClick={() => { setAgentModalFor(null); setModalOpen(true); }} disabled={!hasActiveProviders} className={`rounded border px-2 py-2 text-xs transition-colors sm:py-1.5 whitespace-nowrap sm:shrink-0 ${hasActiveProviders ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}>Select</button>
|
||||
{selectedModel && <button onClick={() => setSelectedModel("")} className="p-1 text-text-muted hover:text-red-500 rounded transition-colors" title="Clear"><span className="material-symbols-outlined text-[14px]">close</span></button>}
|
||||
</div>
|
||||
|
||||
@@ -333,15 +333,15 @@ export default function OpenClawToolCard({
|
||||
{(openclawStatus.agents || []).filter(a => a.agentDir).map((agent) => (
|
||||
<div key={agent.id} className="flex items-center gap-2 pl-4">
|
||||
<span className="w-32 shrink-0 text-xs text-primary text-right truncate" title={agent.name || agent.id}>Agent {agent.name || agent.id}</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||
<input
|
||||
type="text"
|
||||
value={agentModels[agent.id] || ""}
|
||||
onChange={(e) => setAgentModels(prev => ({ ...prev, [agent.id]: e.target.value }))}
|
||||
placeholder={`default (${selectedModel || "provider/model-id"})`}
|
||||
className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
className="min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
|
||||
/>
|
||||
<button onClick={() => { setAgentModalFor(agent.id); setModalOpen(true); }} disabled={!hasActiveProviders} className={`px-2 py-1.5 rounded border text-xs transition-colors shrink-0 whitespace-nowrap ${hasActiveProviders ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}>Select</button>
|
||||
<button onClick={() => { setAgentModalFor(agent.id); setModalOpen(true); }} disabled={!hasActiveProviders} className={`rounded border px-2 py-2 text-xs transition-colors sm:py-1.5 whitespace-nowrap sm:shrink-0 ${hasActiveProviders ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}>Select</button>
|
||||
{agentModels[agent.id] && <button onClick={() => setAgentModels(prev => ({ ...prev, [agent.id]: "" }))} className="p-1 text-text-muted hover:text-red-500 rounded transition-colors" title="Clear"><span className="material-symbols-outlined text-[14px]">close</span></button>}
|
||||
</div>
|
||||
))}
|
||||
@@ -354,7 +354,7 @@ export default function OpenClawToolCard({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="grid grid-cols-1 gap-2 sm:flex sm:items-center">
|
||||
<Button variant="primary" size="sm" onClick={handleApplySettings} disabled={!selectedModel} loading={applying}>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">save</span>Apply
|
||||
</Button>
|
||||
|
||||
@@ -189,13 +189,13 @@ export default function OpenCodeToolCard({ tool, isExpanded, onToggle, baseUrl,
|
||||
|
||||
return (
|
||||
<Card padding="xs" className="overflow-hidden">
|
||||
<div className="flex items-center justify-between hover:cursor-pointer" onClick={onToggle}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-start justify-between gap-3 hover:cursor-pointer sm:items-center" onClick={onToggle}>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div className="size-8 flex items-center justify-center shrink-0">
|
||||
<Image src="/providers/opencode.png" alt={tool.name} width={32} height={32} className="size-8 object-contain rounded-lg" sizes="32px" onError={(e) => { e.target.style.display = "none"; }} />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<h3 className="font-medium text-sm">{tool.name}</h3>
|
||||
{configStatus === "configured" && <span className="px-1.5 py-0.5 text-[10px] font-medium bg-green-500/10 text-green-600 dark:text-green-400 rounded-full">Connected</span>}
|
||||
{configStatus === "not_configured" && <span className="px-1.5 py-0.5 text-[10px] font-medium bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 rounded-full">Not configured</span>}
|
||||
@@ -257,25 +257,25 @@ export default function OpenCodeToolCard({ tool, isExpanded, onToggle, baseUrl,
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Current base URL */}
|
||||
{status?.config?.provider?.["9router"]?.options?.baseURL && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">Current</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<span className="flex-1 px-2 py-1.5 text-xs text-text-muted truncate">
|
||||
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
|
||||
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Current</span>
|
||||
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||
<span className="min-w-0 truncate rounded bg-surface/40 px-2 py-2 text-xs text-text-muted sm:py-1.5">
|
||||
{status.config.provider["9router"].options.baseURL}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Base URL */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">Base URL</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
|
||||
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Base URL</span>
|
||||
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||
<input
|
||||
type="text"
|
||||
value={getDisplayUrl()}
|
||||
onChange={(e) => setCustomBaseUrl(e.target.value)}
|
||||
placeholder="https://.../v1"
|
||||
className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
className="min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
|
||||
/>
|
||||
{customBaseUrl && customBaseUrl !== `${baseUrl}/v1` && (
|
||||
<button onClick={() => setCustomBaseUrl("")} className="p-1 text-text-muted hover:text-primary rounded transition-colors" title="Reset to default">
|
||||
@@ -285,22 +285,22 @@ export default function OpenCodeToolCard({ tool, isExpanded, onToggle, baseUrl,
|
||||
</div>
|
||||
|
||||
{/* API Key */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">API Key</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
|
||||
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">API Key</span>
|
||||
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||
{apiKeys.length > 0 ? (
|
||||
<select value={selectedApiKey} onChange={(e) => setSelectedApiKey(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">
|
||||
<select value={selectedApiKey} onChange={(e) => setSelectedApiKey(e.target.value)} className="min-w-0 px-2 py-2 bg-surface rounded text-xs border border-border focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5">
|
||||
{apiKeys.map((key) => <option key={key.id} value={key.key}>{key.key}</option>)}
|
||||
</select>
|
||||
) : (
|
||||
<span className="flex-1 text-xs text-text-muted px-2 py-1.5">
|
||||
<span className="min-w-0 rounded bg-surface/40 px-2 py-2 text-xs text-text-muted sm:py-1.5">
|
||||
{cloudEnabled ? "No API keys - Create one in Keys page" : "sk_9router (default)"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Models */}
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr] sm:items-start sm:gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right pt-1">Models</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px] mt-1.5">arrow_forward</span>
|
||||
<div className="flex-1 flex flex-col gap-2">
|
||||
@@ -364,7 +364,7 @@ export default function OpenCodeToolCard({ tool, isExpanded, onToggle, baseUrl,
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
|
||||
<button onClick={() => setModalOpen(true)} disabled={!activeProviders?.length} className={`px-2 py-1 rounded border text-xs transition-colors ${activeProviders?.length ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}>Add Model</button>
|
||||
<span className="text-xs text-text-muted">
|
||||
{selectedModels.length > 0 && activeModel ? (
|
||||
@@ -380,20 +380,20 @@ export default function OpenCodeToolCard({ tool, isExpanded, onToggle, baseUrl,
|
||||
</div>
|
||||
|
||||
{/* Subagent Model */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">Subagent Model</span>
|
||||
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
|
||||
<div className="grid gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
|
||||
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Subagent Model</span>
|
||||
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||
<input
|
||||
type="text"
|
||||
value={subagentModel}
|
||||
onChange={(e) => setSubagentModel(e.target.value)}
|
||||
placeholder={selectedModel || "provider/model-id (defaults to main model)"}
|
||||
className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50"
|
||||
className="min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setSubagentModalOpen(true)}
|
||||
disabled={!activeProviders?.length}
|
||||
className={`px-2 py-1.5 rounded border text-xs transition-colors shrink-0 whitespace-nowrap ${activeProviders?.length ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}
|
||||
className={`rounded border px-2 py-2 text-xs transition-colors sm:py-1.5 whitespace-nowrap sm:shrink-0 ${activeProviders?.length ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}
|
||||
>
|
||||
Select Model
|
||||
</button>
|
||||
@@ -416,7 +416,7 @@ export default function OpenCodeToolCard({ tool, isExpanded, onToggle, baseUrl,
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="grid grid-cols-1 gap-2 sm:flex sm:items-center">
|
||||
<Button variant="primary" size="sm" onClick={handleApply} disabled={selectedModels.length === 0} loading={applying}>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">save</span>Apply
|
||||
</Button>
|
||||
|
||||
@@ -125,16 +125,16 @@ export default function CombosPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex min-w-0 flex-col gap-6 px-1 sm:px-0">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-2xl font-semibold">Combos</h1>
|
||||
<p className="text-sm text-text-muted mt-1">
|
||||
Create model combos with fallback support
|
||||
</p>
|
||||
</div>
|
||||
<Button icon="add" onClick={() => setShowCreateModal(true)}>
|
||||
<Button icon="add" onClick={() => setShowCreateModal(true)} className="w-full sm:w-auto">
|
||||
Create Combo
|
||||
</Button>
|
||||
</div>
|
||||
@@ -148,7 +148,7 @@ export default function CombosPage() {
|
||||
</div>
|
||||
<p className="text-text-main font-medium mb-1">No combos yet</p>
|
||||
<p className="text-sm text-text-muted mb-4">Create model combos with fallback support</p>
|
||||
<Button icon="add" onClick={() => setShowCreateModal(true)}>
|
||||
<Button icon="add" onClick={() => setShowCreateModal(true)} className="w-full sm:w-auto">
|
||||
Create Combo
|
||||
</Button>
|
||||
</div>
|
||||
@@ -195,19 +195,19 @@ export default function CombosPage() {
|
||||
function ComboCard({ combo, copied, onCopy, onEdit, onDelete, roundRobinEnabled, onToggleRoundRobin }) {
|
||||
return (
|
||||
<Card padding="sm" className="group">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
<div className="flex min-w-0 flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex min-w-0 flex-1 items-start gap-3 sm:items-center">
|
||||
<div className="size-8 rounded-lg bg-primary/10 flex items-center justify-center shrink-0">
|
||||
<span className="material-symbols-outlined text-primary text-[18px]">layers</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<code className="text-sm font-medium font-mono truncate">{combo.name}</code>
|
||||
<div className="flex items-center gap-1 mt-0.5 flex-wrap">
|
||||
<code className="block truncate font-mono text-sm font-medium">{combo.name}</code>
|
||||
<div className="mt-1 flex min-w-0 flex-wrap items-center gap-1">
|
||||
{combo.models.length === 0 ? (
|
||||
<span className="text-xs text-text-muted italic">No models</span>
|
||||
) : (
|
||||
combo.models.slice(0, 3).map((model, index) => (
|
||||
<code key={index} className="text-[10px] font-mono bg-black/5 dark:bg-white/5 px-1.5 py-0.5 rounded text-text-muted">
|
||||
<code key={index} className="max-w-full truncate rounded bg-black/5 px-1.5 py-0.5 font-mono text-[10px] text-text-muted dark:bg-white/5 sm:max-w-[220px]">
|
||||
{model}
|
||||
</code>
|
||||
))
|
||||
@@ -220,9 +220,9 @@ function ComboCard({ combo, copied, onCopy, onEdit, onDelete, roundRobinEnabled,
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-3 shrink-0">
|
||||
<div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:items-center sm:gap-3 sm:shrink-0">
|
||||
{/* Round Robin Toggle — always visible */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="flex items-center justify-between gap-1.5 rounded-lg bg-black/[0.02] px-2 py-1.5 dark:bg-white/[0.02] sm:justify-start sm:bg-transparent sm:px-0 sm:py-0 sm:dark:bg-transparent">
|
||||
<span className="text-xs text-text-muted font-medium">Round Robin</span>
|
||||
<Toggle
|
||||
size="sm"
|
||||
@@ -231,10 +231,10 @@ function ComboCard({ combo, copied, onCopy, onEdit, onDelete, roundRobinEnabled,
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1">
|
||||
<div className="grid grid-cols-3 gap-1 sm:flex">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onCopy(combo.name, `combo-${combo.id}`); }}
|
||||
className="flex flex-col items-center px-2 py-1 rounded hover:bg-black/5 dark:hover:bg-white/5 text-text-muted hover:text-primary transition-colors"
|
||||
className="flex flex-col items-center rounded px-2 py-1 text-text-muted transition-colors hover:bg-black/5 hover:text-primary dark:hover:bg-white/5"
|
||||
title="Copy combo name"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[18px]">
|
||||
@@ -244,7 +244,7 @@ function ComboCard({ combo, copied, onCopy, onEdit, onDelete, roundRobinEnabled,
|
||||
</button>
|
||||
<button
|
||||
onClick={onEdit}
|
||||
className="flex flex-col items-center px-2 py-1 rounded hover:bg-black/5 dark:hover:bg-white/5 text-text-muted hover:text-primary transition-colors"
|
||||
className="flex flex-col items-center rounded px-2 py-1 text-text-muted transition-colors hover:bg-black/5 hover:text-primary dark:hover:bg-white/5"
|
||||
title="Edit"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[18px]">edit</span>
|
||||
@@ -252,7 +252,7 @@ function ComboCard({ combo, copied, onCopy, onEdit, onDelete, roundRobinEnabled,
|
||||
</button>
|
||||
<button
|
||||
onClick={onDelete}
|
||||
className="flex flex-col items-center px-2 py-1 rounded hover:bg-red-500/10 text-red-500 transition-colors"
|
||||
className="flex flex-col items-center rounded px-2 py-1 text-red-500 transition-colors hover:bg-red-500/10"
|
||||
title="Delete"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[18px]">delete</span>
|
||||
@@ -283,7 +283,7 @@ function ModelItem({ index, model, isFirst, isLast, onEdit, onMoveUp, onMoveDown
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="group flex items-center gap-1.5 px-2 py-1 rounded-md bg-black/[0.02] dark:bg-white/[0.02] hover:bg-black/[0.04] dark:hover:bg-white/[0.04] transition-colors">
|
||||
<div className="group flex min-w-0 items-center gap-1.5 rounded-md bg-black/[0.02] px-2 py-1 transition-colors hover:bg-black/[0.04] dark:bg-white/[0.02] dark:hover:bg-white/[0.04]">
|
||||
{/* Index badge */}
|
||||
<span className="text-[10px] font-medium text-text-muted w-3 text-center shrink-0">{index + 1}</span>
|
||||
|
||||
@@ -295,11 +295,11 @@ function ModelItem({ index, model, isFirst, isLast, onEdit, onMoveUp, onMoveDown
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
onBlur={commit}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="flex-1 min-w-0 px-1.5 py-0.5 text-xs font-mono bg-white dark:bg-black/20 border border-primary/40 rounded outline-none text-text-main"
|
||||
className="min-w-0 flex-1 rounded border border-primary/40 bg-white px-1.5 py-0.5 font-mono text-xs text-text-main outline-none dark:bg-black/20"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="flex-1 min-w-0 px-1.5 py-0.5 text-xs font-mono text-text-main truncate cursor-text hover:bg-black/5 dark:hover:bg-white/5 rounded"
|
||||
className="min-w-0 flex-1 cursor-text truncate rounded px-1.5 py-0.5 font-mono text-xs text-text-main hover:bg-black/5 dark:hover:bg-white/5"
|
||||
onClick={() => setEditing(true)}
|
||||
title="Click to edit"
|
||||
>
|
||||
@@ -308,7 +308,7 @@ function ModelItem({ index, model, isFirst, isLast, onEdit, onMoveUp, onMoveDown
|
||||
)}
|
||||
|
||||
{/* Priority arrows */}
|
||||
<div className="flex items-center gap-0.5">
|
||||
<div className="flex shrink-0 items-center gap-0.5">
|
||||
<button
|
||||
onClick={onMoveUp}
|
||||
disabled={isFirst}
|
||||
@@ -448,7 +448,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, kindF
|
||||
<p className="text-xs text-text-muted">No models added yet</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-1 max-h-[350px] overflow-y-auto">
|
||||
<div className="flex max-h-[55vh] min-w-0 flex-col gap-1 overflow-y-auto sm:max-h-[350px]">
|
||||
{models.map((model, index) => (
|
||||
<ModelItem
|
||||
key={index}
|
||||
@@ -480,7 +480,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, kindF
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-2 pt-1">
|
||||
<div className="flex flex-col gap-2 pt-1 sm:flex-row">
|
||||
<Button onClick={onClose} variant="ghost" fullWidth size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
@@ -75,7 +75,11 @@ export default function MitmPageClient() {
|
||||
const mitmTools = Object.entries(MITM_TOOLS);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="mx-auto flex w-full max-w-5xl flex-col gap-6 px-1 sm:px-0">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h1 className="text-xl font-semibold text-text-main sm:text-2xl">MITM</h1>
|
||||
<p className="text-sm text-text-muted">Route supported IDE traffic through 9Router with local DNS interception.</p>
|
||||
</div>
|
||||
{/* MITM Server Card */}
|
||||
<MitmServerCard
|
||||
apiKeys={apiKeys}
|
||||
@@ -84,7 +88,7 @@ export default function MitmPageClient() {
|
||||
/>
|
||||
|
||||
{/* Tool Cards */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="grid gap-3 sm:gap-4">
|
||||
{mitmTools.map(([toolId, tool]) => (
|
||||
<MitmToolCard
|
||||
key={toolId}
|
||||
|
||||
@@ -332,28 +332,28 @@ export default function ProfilePage() {
|
||||
const observabilityEnabled = settings.enableObservability === true;
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<div className="max-w-2xl mx-auto px-4 sm:px-0">
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Local Mode Info */}
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="size-12 rounded-lg bg-green-500/10 text-green-500 flex items-center justify-center">
|
||||
<span className="material-symbols-outlined text-2xl">computer</span>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-4">
|
||||
<div className="flex items-center gap-3 sm:gap-4">
|
||||
<div className="size-10 sm:size-12 rounded-lg bg-green-500/10 text-green-500 flex items-center justify-center shrink-0">
|
||||
<span className="material-symbols-outlined text-xl sm:text-2xl">computer</span>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold">Local Mode</h2>
|
||||
<p className="text-text-muted">Running on your machine</p>
|
||||
<h2 className="text-lg sm:text-xl font-semibold">Local Mode</h2>
|
||||
<p className="text-sm text-text-muted">Running on your machine</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="inline-flex p-1 rounded-lg bg-black/5 dark:bg-white/5">
|
||||
<div className="inline-flex p-1 rounded-lg bg-black/5 dark:bg-white/5 w-full sm:w-auto">
|
||||
{["light", "dark", "system"].map((option) => (
|
||||
<button
|
||||
key={option}
|
||||
type="button"
|
||||
onClick={() => setTheme(option)}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 px-3 py-1.5 rounded-md font-medium transition-all",
|
||||
"flex items-center justify-center gap-1 sm:gap-1.5 px-2 sm:px-3 py-1.5 rounded-md font-medium transition-all flex-1 sm:flex-initial",
|
||||
theme === option
|
||||
? "bg-white dark:bg-white/10 text-text-main shadow-sm"
|
||||
: "text-text-muted hover:text-text-main"
|
||||
@@ -362,24 +362,25 @@ export default function ProfilePage() {
|
||||
<span className="material-symbols-outlined text-[18px]">
|
||||
{option === "light" ? "light_mode" : option === "dark" ? "dark_mode" : "contrast"}
|
||||
</span>
|
||||
<span className="capitalize text-sm">{option}</span>
|
||||
<span className="capitalize text-xs sm:text-sm">{option}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 pt-4 border-t border-border">
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-bg border border-border">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between p-3 rounded-lg bg-bg border border-border gap-2">
|
||||
<div>
|
||||
<p className="font-medium">Database Location</p>
|
||||
<p className="text-sm text-text-muted font-mono">~/.9router/db.json</p>
|
||||
<p className="font-medium text-sm sm:text-base">Database Location</p>
|
||||
<p className="text-xs sm:text-sm text-text-muted font-mono break-all">~/.9router/db.json</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon="download"
|
||||
onClick={handleExportDatabase}
|
||||
loading={dbLoading}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
Download Backup
|
||||
</Button>
|
||||
@@ -388,6 +389,7 @@ export default function ProfilePage() {
|
||||
icon="upload"
|
||||
onClick={() => importFileRef.current?.click()}
|
||||
disabled={dbLoading}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
Import Backup
|
||||
</Button>
|
||||
@@ -410,16 +412,16 @@ export default function ProfilePage() {
|
||||
{/* Security */}
|
||||
<Card>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 rounded-lg bg-primary/10 text-primary">
|
||||
<div className="p-2 rounded-lg bg-primary/10 text-primary shrink-0">
|
||||
<span className="material-symbols-outlined text-[20px]">shield</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold">Security</h3>
|
||||
<h3 className="text-base sm:text-lg font-semibold">Security</h3>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium">Require login</p>
|
||||
<p className="text-sm text-text-muted">
|
||||
<div className="flex items-start sm:items-center justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium text-sm sm:text-base">Require login</p>
|
||||
<p className="text-xs sm:text-sm text-text-muted">
|
||||
When ON, dashboard requires password. When OFF, access without login.
|
||||
</p>
|
||||
</div>
|
||||
@@ -433,7 +435,7 @@ export default function ProfilePage() {
|
||||
<form onSubmit={handlePasswordChange} className="flex flex-col gap-4 pt-4 border-t border-border/50">
|
||||
{settings.hasPassword && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">Current Password</label>
|
||||
<label className="text-xs sm:text-sm font-medium">Current Password</label>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Enter current password"
|
||||
@@ -450,9 +452,9 @@ export default function ProfilePage() {
|
||||
</p>
|
||||
</div>
|
||||
)} */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">New Password</label>
|
||||
<label className="text-xs sm:text-sm font-medium">New Password</label>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Enter new password"
|
||||
@@ -462,7 +464,7 @@ export default function ProfilePage() {
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">Confirm New Password</label>
|
||||
<label className="text-xs sm:text-sm font-medium">Confirm New Password</label>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Confirm new password"
|
||||
@@ -474,13 +476,13 @@ export default function ProfilePage() {
|
||||
</div>
|
||||
|
||||
{passStatus.message && (
|
||||
<p className={`text-sm ${passStatus.type === "error" ? "text-red-500" : "text-green-500"}`}>
|
||||
<p className={`text-xs sm:text-sm ${passStatus.type === "error" ? "text-red-500" : "text-green-500"}`}>
|
||||
{passStatus.message}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="pt-2">
|
||||
<Button type="submit" variant="primary" loading={passLoading}>
|
||||
<Button type="submit" variant="primary" loading={passLoading} className="w-full sm:w-auto">
|
||||
{settings.hasPassword ? "Update Password" : "Set Password"}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -492,16 +494,16 @@ export default function ProfilePage() {
|
||||
{/* Routing Preferences */}
|
||||
<Card>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 rounded-lg bg-blue-500/10 text-blue-500">
|
||||
<div className="p-2 rounded-lg bg-blue-500/10 text-blue-500 shrink-0">
|
||||
<span className="material-symbols-outlined text-[20px]">route</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold">Routing Strategy</h3>
|
||||
<h3 className="text-base sm:text-lg font-semibold">Routing Strategy</h3>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium">Round Robin</p>
|
||||
<p className="text-sm text-text-muted">
|
||||
<div className="flex items-start sm:items-center justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium text-sm sm:text-base">Round Robin</p>
|
||||
<p className="text-xs sm:text-sm text-text-muted">
|
||||
Cycle through accounts to distribute load
|
||||
</p>
|
||||
</div>
|
||||
@@ -514,10 +516,10 @@ export default function ProfilePage() {
|
||||
|
||||
{/* Sticky Round Robin Limit */}
|
||||
{settings.fallbackStrategy === "round-robin" && (
|
||||
<div className="flex items-center justify-between pt-2 border-t border-border/50">
|
||||
<div>
|
||||
<p className="font-medium">Sticky Limit</p>
|
||||
<p className="text-sm text-text-muted">
|
||||
<div className="flex items-start sm:items-center justify-between gap-4 pt-2 border-t border-border/50">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium text-sm sm:text-base">Sticky Limit</p>
|
||||
<p className="text-xs sm:text-sm text-text-muted">
|
||||
Calls per account before switching
|
||||
</p>
|
||||
</div>
|
||||
@@ -528,16 +530,16 @@ export default function ProfilePage() {
|
||||
value={settings.stickyRoundRobinLimit || 3}
|
||||
onChange={(e) => updateStickyLimit(e.target.value)}
|
||||
disabled={loading}
|
||||
className="w-20 text-center"
|
||||
className="w-16 sm:w-20 text-center shrink-0"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Combo Round Robin */}
|
||||
<div className="flex items-center justify-between pt-4 border-t border-border/50">
|
||||
<div>
|
||||
<p className="font-medium">Combo Round Robin</p>
|
||||
<p className="text-sm text-text-muted">
|
||||
<div className="flex items-start sm:items-center justify-between gap-4 pt-4 border-t border-border/50">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium text-sm sm:text-base">Combo Round Robin</p>
|
||||
<p className="text-xs sm:text-sm text-text-muted">
|
||||
Cycle through providers in combos instead of always starting with first
|
||||
</p>
|
||||
</div>
|
||||
@@ -559,17 +561,17 @@ export default function ProfilePage() {
|
||||
{/* Network */}
|
||||
<Card>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 rounded-lg bg-purple-500/10 text-purple-500">
|
||||
<div className="p-2 rounded-lg bg-purple-500/10 text-purple-500 shrink-0">
|
||||
<span className="material-symbols-outlined text-[20px]">wifi</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold">Network</h3>
|
||||
<h3 className="text-base sm:text-lg font-semibold">Network</h3>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium">Outbound Proxy</p>
|
||||
<p className="text-sm text-text-muted">Enable proxy for OAuth + provider outbound requests.</p>
|
||||
<div className="flex items-start sm:items-center justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium text-sm sm:text-base">Outbound Proxy</p>
|
||||
<p className="text-xs sm:text-sm text-text-muted">Enable proxy for OAuth + provider outbound requests.</p>
|
||||
</div>
|
||||
<Toggle
|
||||
checked={settings.outboundProxyEnabled === true}
|
||||
@@ -581,38 +583,39 @@ export default function ProfilePage() {
|
||||
{settings.outboundProxyEnabled === true && (
|
||||
<form onSubmit={updateOutboundProxy} className="flex flex-col gap-4 pt-2 border-t border-border/50">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="font-medium">Proxy URL</label>
|
||||
<label className="font-medium text-sm sm:text-base">Proxy URL</label>
|
||||
<Input
|
||||
placeholder="http://127.0.0.1:7897"
|
||||
value={proxyForm.outboundProxyUrl}
|
||||
onChange={(e) => setProxyForm((prev) => ({ ...prev, outboundProxyUrl: e.target.value }))}
|
||||
disabled={loading || proxyLoading}
|
||||
/>
|
||||
<p className="text-sm text-text-muted">Leave empty to inherit existing env proxy (if any).</p>
|
||||
<p className="text-xs sm:text-sm text-text-muted">Leave empty to inherit existing env proxy (if any).</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 pt-2 border-t border-border/50">
|
||||
<label className="font-medium">No Proxy</label>
|
||||
<label className="font-medium text-sm sm:text-base">No Proxy</label>
|
||||
<Input
|
||||
placeholder="localhost,127.0.0.1"
|
||||
value={proxyForm.outboundNoProxy}
|
||||
onChange={(e) => setProxyForm((prev) => ({ ...prev, outboundNoProxy: e.target.value }))}
|
||||
disabled={loading || proxyLoading}
|
||||
/>
|
||||
<p className="text-sm text-text-muted">Comma-separated hostnames/domains to bypass the proxy.</p>
|
||||
<p className="text-xs sm:text-sm text-text-muted">Comma-separated hostnames/domains to bypass the proxy.</p>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t border-border/50 flex items-center gap-2">
|
||||
<div className="pt-2 border-t border-border/50 flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
loading={proxyTestLoading}
|
||||
disabled={loading || proxyLoading}
|
||||
onClick={testOutboundProxy}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
Test proxy URL
|
||||
</Button>
|
||||
<Button type="submit" variant="primary" loading={proxyLoading}>
|
||||
<Button type="submit" variant="primary" loading={proxyLoading} className="w-full sm:w-auto">
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
@@ -620,7 +623,7 @@ export default function ProfilePage() {
|
||||
)}
|
||||
|
||||
{proxyStatus.message && (
|
||||
<p className={`text-sm ${proxyStatus.type === "error" ? "text-red-500" : "text-green-500"} pt-2 border-t border-border/50`}>
|
||||
<p className={`text-xs sm:text-sm ${proxyStatus.type === "error" ? "text-red-500" : "text-green-500"} pt-2 border-t border-border/50`}>
|
||||
{proxyStatus.message}
|
||||
</p>
|
||||
)}
|
||||
@@ -630,15 +633,15 @@ export default function ProfilePage() {
|
||||
{/* Observability Settings */}
|
||||
<Card>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 rounded-lg bg-orange-500/10 text-orange-500">
|
||||
<div className="p-2 rounded-lg bg-orange-500/10 text-orange-500 shrink-0">
|
||||
<span className="material-symbols-outlined text-[20px]">monitoring</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold">Observability</h3>
|
||||
<h3 className="text-base sm:text-lg font-semibold">Observability</h3>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium">Enable Observability</p>
|
||||
<p className="text-sm text-text-muted">
|
||||
<div className="flex items-start sm:items-center justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium text-sm sm:text-base">Enable Observability</p>
|
||||
<p className="text-xs sm:text-sm text-text-muted">
|
||||
Record request details for inspection in the logs view
|
||||
</p>
|
||||
</div>
|
||||
@@ -651,7 +654,7 @@ export default function ProfilePage() {
|
||||
</Card>
|
||||
|
||||
{/* App Info */}
|
||||
<div className="text-center text-sm text-text-muted py-4">
|
||||
<div className="text-center text-xs sm:text-sm text-text-muted py-4">
|
||||
<p>{APP_CONFIG.name} v{APP_CONFIG.version}</p>
|
||||
<p className="mt-1">Local Mode - All data stored on your machine</p>
|
||||
</div>
|
||||
|
||||
@@ -109,10 +109,10 @@ export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`group flex items-center justify-between p-2 rounded-lg hover:bg-black/[0.02] dark:hover:bg-white/[0.02] transition-colors ${connection.isActive === false ? "opacity-60" : ""}`}>
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
<div className={`group flex min-w-0 flex-col gap-3 rounded-lg p-2 transition-colors hover:bg-black/[0.02] dark:hover:bg-white/[0.02] sm:flex-row sm:items-center sm:justify-between ${connection.isActive === false ? "opacity-60" : ""}`}>
|
||||
<div className="flex min-w-0 flex-1 items-start gap-2 sm:items-center sm:gap-3">
|
||||
{/* Priority arrows */}
|
||||
<div className="flex flex-col">
|
||||
<div className="flex shrink-0 flex-col">
|
||||
<button
|
||||
onClick={onMoveUp}
|
||||
disabled={isFirst}
|
||||
@@ -128,12 +128,12 @@ export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst
|
||||
<span className="material-symbols-outlined text-sm">keyboard_arrow_down</span>
|
||||
</button>
|
||||
</div>
|
||||
<span className="material-symbols-outlined text-base text-text-muted">
|
||||
<span className="material-symbols-outlined shrink-0 text-base text-text-muted">
|
||||
{isOAuth ? "lock" : "key"}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium truncate">{displayName}</p>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<div className="mt-1 flex min-w-0 flex-wrap items-center gap-1.5 sm:gap-2">
|
||||
<Badge variant={getStatusVariant()} size="sm" dot>
|
||||
{connection.isActive === false ? "disabled" : (effectiveStatus || "Unknown")}
|
||||
</Badge>
|
||||
@@ -144,7 +144,7 @@ export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst
|
||||
)}
|
||||
{isCooldown && connection.isActive !== false && <CooldownTimer until={modelLockUntil} />}
|
||||
{connection.lastError && connection.isActive !== false && (
|
||||
<span className="text-xs text-red-500 truncate max-w-[300px]" title={connection.lastError}>
|
||||
<span className="max-w-full truncate text-xs text-red-500 sm:max-w-[300px]" title={connection.lastError}>
|
||||
{connection.lastError}
|
||||
</span>
|
||||
)}
|
||||
@@ -155,16 +155,16 @@ export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst
|
||||
</div>
|
||||
{hasAnyProxy && (
|
||||
<div className="mt-1 flex items-center gap-2 flex-wrap">
|
||||
<span className="text-[11px] text-text-muted truncate max-w-[420px]" title={proxyDisplayText}>
|
||||
<span className="max-w-full truncate text-[11px] text-text-muted sm:max-w-[420px]" title={proxyDisplayText}>
|
||||
{proxyDisplayText}
|
||||
</span>
|
||||
{maskedProxyUrl && (
|
||||
<code className="text-[10px] font-mono bg-black/5 dark:bg-white/5 px-1 py-0.5 rounded text-text-muted">
|
||||
<code className="max-w-full truncate rounded bg-black/5 px-1 py-0.5 font-mono text-[10px] text-text-muted dark:bg-white/5 sm:max-w-[260px]">
|
||||
{maskedProxyUrl}
|
||||
</code>
|
||||
)}
|
||||
{noProxyText && (
|
||||
<span className="text-[11px] text-text-muted truncate max-w-[320px]" title={noProxyText}>
|
||||
<span className="max-w-full truncate text-[11px] text-text-muted sm:max-w-[320px]" title={noProxyText}>
|
||||
no_proxy: {noProxyText}
|
||||
</span>
|
||||
)}
|
||||
@@ -172,14 +172,14 @@ export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex gap-1">
|
||||
<div className="flex w-full items-center justify-between gap-2 sm:w-auto sm:justify-end">
|
||||
<div className="grid flex-1 grid-cols-3 gap-1 sm:flex sm:flex-none">
|
||||
{/* Proxy button with inline dropdown */}
|
||||
{(proxyPools || []).length > 0 && (
|
||||
<div className="relative" ref={proxyDropdownRef}>
|
||||
<button
|
||||
onClick={() => setShowProxyDropdown((v) => !v)}
|
||||
className={`flex flex-col items-center px-2 py-1 rounded hover:bg-black/5 dark:hover:bg-white/5 transition-colors ${hasAnyProxy ? "text-primary" : "text-text-muted hover:text-primary"}`}
|
||||
className={`flex w-full flex-col items-center rounded px-2 py-1 transition-colors hover:bg-black/5 dark:hover:bg-white/5 ${hasAnyProxy ? "text-primary" : "text-text-muted hover:text-primary"}`}
|
||||
disabled={updatingProxy}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[18px]">
|
||||
@@ -188,7 +188,7 @@ export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst
|
||||
<span className="text-[10px] leading-tight">Proxy</span>
|
||||
</button>
|
||||
{showProxyDropdown && (
|
||||
<div className="absolute right-0 top-full mt-1 z-50 bg-bg border border-border rounded-lg shadow-lg py-1 min-w-[160px]">
|
||||
<div className="absolute right-0 top-full z-50 mt-1 max-w-[78vw] min-w-[160px] rounded-lg border border-border bg-bg py-1 shadow-lg">
|
||||
<button
|
||||
onClick={() => handleSelectProxy("__none__")}
|
||||
className={`w-full text-left px-3 py-1.5 text-sm hover:bg-black/5 dark:hover:bg-white/5 ${!boundProxyPoolId ? "text-primary font-medium" : "text-text-main"}`}
|
||||
@@ -208,11 +208,11 @@ export default function ConnectionRow({ connection, proxyPools, isOAuth, isFirst
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<button onClick={onEdit} className="flex flex-col items-center px-2 py-1 rounded hover:bg-black/5 dark:hover:bg-white/5 text-text-muted hover:text-primary">
|
||||
<button onClick={onEdit} className="flex flex-col items-center rounded px-2 py-1 text-text-muted hover:bg-black/5 hover:text-primary dark:hover:bg-white/5">
|
||||
<span className="material-symbols-outlined text-[18px]">edit</span>
|
||||
<span className="text-[10px] leading-tight">Edit</span>
|
||||
</button>
|
||||
<button onClick={onDelete} className="flex flex-col items-center px-2 py-1 rounded hover:bg-red-500/10 text-red-500">
|
||||
<button onClick={onDelete} className="flex flex-col items-center rounded px-2 py-1 text-red-500 hover:bg-red-500/10">
|
||||
<span className="material-symbols-outlined text-[18px]">delete</span>
|
||||
<span className="text-[10px] leading-tight">Delete</span>
|
||||
</button>
|
||||
|
||||
@@ -14,24 +14,24 @@ export default function ModelRow({ model, fullModel, alias, copied, onCopy, test
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className={`group px-3 py-2 rounded-lg border ${borderColor} hover:bg-sidebar/50`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`group min-w-0 max-w-full rounded-lg border px-3 py-2 ${borderColor} hover:bg-sidebar/50`}>
|
||||
<div className="flex min-w-0 items-start gap-2 sm:items-center">
|
||||
<span
|
||||
className="material-symbols-outlined text-base"
|
||||
className="material-symbols-outlined shrink-0 text-base"
|
||||
style={iconColor ? { color: iconColor } : undefined}
|
||||
>
|
||||
{testStatus === "ok" ? "check_circle" : testStatus === "error" ? "cancel" : "smart_toy"}
|
||||
</span>
|
||||
<div className="flex flex-col gap-1">
|
||||
<code className="text-xs text-text-muted font-mono bg-sidebar px-1.5 py-0.5 rounded">{fullModel}</code>
|
||||
{model.name && <span className="text-[9px] text-text-muted/70 italic pl-1">{model.name}</span>}
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
||||
<code className="max-w-[72vw] truncate rounded bg-sidebar px-1.5 py-0.5 font-mono text-xs text-text-muted sm:max-w-[360px]">{fullModel}</code>
|
||||
{model.name && <span className="truncate pl-1 text-[9px] italic text-text-muted/70">{model.name}</span>}
|
||||
</div>
|
||||
{onTest && (
|
||||
<div className="relative group/btn">
|
||||
<div className="relative shrink-0 group/btn">
|
||||
<button
|
||||
onClick={onTest}
|
||||
disabled={isTesting}
|
||||
className={`p-0.5 hover:bg-sidebar rounded text-text-muted hover:text-primary transition-opacity ${isTesting ? "opacity-100" : "opacity-0 group-hover:opacity-100"}`}
|
||||
className={`rounded p-0.5 text-text-muted transition-opacity hover:bg-sidebar hover:text-primary ${isTesting ? "opacity-100" : "opacity-100 sm:opacity-0 sm:group-hover:opacity-100"}`}
|
||||
>
|
||||
<span className="material-symbols-outlined text-sm" style={isTesting ? { animation: "spin 1s linear infinite" } : undefined}>
|
||||
{isTesting ? "progress_activity" : "science"}
|
||||
@@ -42,10 +42,10 @@ export default function ModelRow({ model, fullModel, alias, copied, onCopy, test
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="relative group/btn">
|
||||
<div className="relative shrink-0 group/btn">
|
||||
<button
|
||||
onClick={() => onCopy(fullModel, `model-${model.id}`)}
|
||||
className="p-0.5 hover:bg-sidebar rounded text-text-muted hover:text-primary"
|
||||
className="rounded p-0.5 text-text-muted hover:bg-sidebar hover:text-primary"
|
||||
>
|
||||
<span className="material-symbols-outlined text-sm">
|
||||
{copied === `model-${model.id}` ? "check" : "content_copy"}
|
||||
@@ -58,7 +58,7 @@ export default function ModelRow({ model, fullModel, alias, copied, onCopy, test
|
||||
{isCustom && (
|
||||
<button
|
||||
onClick={onDeleteAlias}
|
||||
className="p-0.5 hover:bg-red-500/10 rounded text-text-muted hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity ml-auto"
|
||||
className="ml-auto rounded p-0.5 text-text-muted opacity-100 transition-opacity hover:bg-red-500/10 hover:text-red-500 sm:opacity-0 sm:group-hover:opacity-100"
|
||||
title="Remove custom model"
|
||||
>
|
||||
<span className="material-symbols-outlined text-sm">close</span>
|
||||
|
||||
@@ -464,10 +464,10 @@ export default function ProviderDetailPage() {
|
||||
const isSelected = (connectionId) => selectedConnectionIds.includes(connectionId);
|
||||
|
||||
const connectionsList = (
|
||||
<div className="flex flex-col divide-y divide-black/[0.03] dark:divide-white/[0.03]">
|
||||
<div className="flex min-w-0 flex-col divide-y divide-black/[0.03] dark:divide-white/[0.03]">
|
||||
{connections
|
||||
.map((conn, index) => (
|
||||
<div key={conn.id} className="flex items-stretch">
|
||||
<div key={conn.id} className="flex min-w-0 items-stretch">
|
||||
<div className="flex-1 min-w-0">
|
||||
<ConnectionRow
|
||||
connection={conn}
|
||||
@@ -537,7 +537,7 @@ export default function ProviderDetailPage() {
|
||||
<p className="text-xs text-text-muted">{bulkHint}</p>
|
||||
<p className="text-xs text-text-muted">Selecting None will unbind selected connections from proxy pool.</p>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<Button onClick={handleBulkApplyProxyPool} fullWidth disabled={!canApplyBulkProxy}>
|
||||
{bulkUpdatingProxy ? "Applying..." : "Apply"}
|
||||
</Button>
|
||||
@@ -656,7 +656,7 @@ export default function ProviderDetailPage() {
|
||||
{/* Add model button — inline, same style as model chips */}
|
||||
<button
|
||||
onClick={() => setShowAddCustomModel(true)}
|
||||
className="flex items-center gap-1.5 px-3 py-2 rounded-lg border border-dashed border-black/15 dark:border-white/15 text-xs text-text-muted hover:text-primary hover:border-primary/40 transition-colors"
|
||||
className="flex w-full items-center justify-center gap-1.5 rounded-lg border border-dashed border-black/15 px-3 py-2 text-xs text-text-muted transition-colors hover:border-primary/40 hover:text-primary sm:w-auto"
|
||||
>
|
||||
<span className="material-symbols-outlined text-sm">add</span>
|
||||
Add Model
|
||||
@@ -728,9 +728,9 @@ export default function ProviderDetailPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex min-w-0 flex-col gap-6 px-1 sm:gap-8 sm:px-0">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<div className="min-w-0">
|
||||
<Link
|
||||
href="/dashboard/providers"
|
||||
className="inline-flex items-center gap-1 text-sm text-text-muted hover:text-primary transition-colors mb-4"
|
||||
@@ -738,9 +738,9 @@ export default function ProviderDetailPage() {
|
||||
<span className="material-symbols-outlined text-lg">arrow_back</span>
|
||||
Back to Providers
|
||||
</Link>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex min-w-0 items-center gap-3 sm:gap-4">
|
||||
<div
|
||||
className="rounded-lg flex items-center justify-center"
|
||||
className="flex size-12 shrink-0 items-center justify-center rounded-lg"
|
||||
style={{ backgroundColor: `${providerInfo.color}15` }}
|
||||
>
|
||||
{headerImgError ? (
|
||||
@@ -753,14 +753,14 @@ export default function ProviderDetailPage() {
|
||||
alt={providerInfo.name}
|
||||
width={48}
|
||||
height={48}
|
||||
className="object-contain rounded-lg max-w-[48px] max-h-[48px]"
|
||||
className="max-h-12 max-w-12 rounded-lg object-contain"
|
||||
sizes="48px"
|
||||
onError={() => setHeaderImgError(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-semibold tracking-tight">{providerInfo.name}</h1>
|
||||
<div className="min-w-0">
|
||||
<h1 className="truncate text-2xl font-semibold tracking-tight sm:text-3xl">{providerInfo.name}</h1>
|
||||
<p className="text-text-muted">
|
||||
{connections.length} connection{connections.length === 1 ? "" : "s"}
|
||||
</p>
|
||||
@@ -776,15 +776,15 @@ export default function ProviderDetailPage() {
|
||||
)}
|
||||
|
||||
{providerInfo.notice && !providerInfo.deprecated && (
|
||||
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-blue-500/10 border border-blue-500/30">
|
||||
<div className="flex flex-col gap-2 rounded-lg border border-blue-500/30 bg-blue-500/10 px-3 py-2 sm:flex-row sm:items-center">
|
||||
<span className="material-symbols-outlined text-[16px] text-blue-500 shrink-0">info</span>
|
||||
<p className="text-xs text-blue-600 dark:text-blue-400 leading-relaxed">{providerInfo.notice.text}</p>
|
||||
<p className="min-w-0 flex-1 text-xs leading-relaxed text-blue-600 dark:text-blue-400">{providerInfo.notice.text}</p>
|
||||
{providerInfo.notice.apiKeyUrl && (
|
||||
<a
|
||||
href={providerInfo.notice.apiKeyUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs font-medium text-white bg-blue-500 hover:bg-blue-600 px-2 py-0.5 rounded shrink-0 transition-colors"
|
||||
className="inline-flex justify-center rounded bg-blue-500 px-2 py-1 text-xs font-medium text-white transition-colors hover:bg-blue-600 sm:py-0.5"
|
||||
>
|
||||
Get API Key →
|
||||
</a>
|
||||
@@ -794,20 +794,21 @@ export default function ProviderDetailPage() {
|
||||
|
||||
{isCompatible && providerNode && (
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<div className="mb-4 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="min-w-0">
|
||||
<h2 className="text-lg font-semibold">{isAnthropicCompatible ? "Anthropic Compatible Details" : "OpenAI Compatible Details"}</h2>
|
||||
<p className="text-sm text-text-muted">
|
||||
<p className="break-all text-sm text-text-muted">
|
||||
{isAnthropicCompatible ? "Messages API" : (providerNode.apiType === "responses" ? "Responses API" : "Chat Completions")} · {(providerNode.baseUrl || "").replace(/\/$/, "")}/
|
||||
{isAnthropicCompatible ? "messages" : (providerNode.apiType === "responses" ? "responses" : "chat/completions")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="grid grid-cols-1 gap-2 sm:flex sm:items-center">
|
||||
<Button
|
||||
size="sm"
|
||||
icon="add"
|
||||
onClick={() => setShowAddApiKeyModal(true)}
|
||||
disabled={connections.length > 0}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
@@ -816,6 +817,7 @@ export default function ProviderDetailPage() {
|
||||
variant="secondary"
|
||||
icon="edit"
|
||||
onClick={() => setShowEditNodeModal(true)}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
@@ -834,6 +836,7 @@ export default function ProviderDetailPage() {
|
||||
console.log("Error deleting provider node:", error);
|
||||
}
|
||||
}}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
@@ -852,9 +855,9 @@ export default function ProviderDetailPage() {
|
||||
<NoAuthProxyCard providerId={providerId} />
|
||||
) : (
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<h2 className="text-lg font-semibold">Connections</h2>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-4">
|
||||
{/* Thinking config */}
|
||||
{/* {thinkingConfig && (
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -871,7 +874,7 @@ export default function ProviderDetailPage() {
|
||||
</div>
|
||||
)} */}
|
||||
{/* Round Robin toggle */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-xs text-text-muted font-medium">Round Robin</span>
|
||||
<Toggle
|
||||
checked={providerStrategy === "round-robin"}
|
||||
@@ -902,7 +905,7 @@ export default function ProviderDetailPage() {
|
||||
<p className="text-text-main font-medium mb-1">No connections yet</p>
|
||||
<p className="text-sm text-text-muted mb-4">Add your first connection to get started</p>
|
||||
{!isCompatible && (
|
||||
<div className="flex gap-2 justify-center">
|
||||
<div className="flex flex-col gap-2 justify-center sm:flex-row">
|
||||
{providerId === "iflow" && (
|
||||
<Button icon="cookie" variant="secondary" onClick={() => setShowIFlowCookieModal(true)}>
|
||||
Cookie Auth
|
||||
@@ -918,7 +921,7 @@ export default function ProviderDetailPage() {
|
||||
<>
|
||||
{connectionsList}
|
||||
{!isCompatible && (
|
||||
<div className="flex gap-2 mt-4">
|
||||
<div className="mt-4 grid grid-cols-1 gap-2 sm:flex">
|
||||
{providerId === "iflow" && (
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -926,6 +929,7 @@ export default function ProviderDetailPage() {
|
||||
variant="secondary"
|
||||
onClick={() => setShowIFlowCookieModal(true)}
|
||||
title="Add connection using browser cookie"
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
Cookie
|
||||
</Button>
|
||||
@@ -934,6 +938,7 @@ export default function ProviderDetailPage() {
|
||||
size="sm"
|
||||
icon="add"
|
||||
onClick={() => isOAuth ? setShowOAuthModal(true) : setShowAddApiKeyModal(true)}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
@@ -946,7 +951,7 @@ export default function ProviderDetailPage() {
|
||||
|
||||
{/* Models */}
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="mb-4 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<h2 className="text-lg font-semibold">
|
||||
{"Available Models"}
|
||||
</h2>
|
||||
|
||||
@@ -245,19 +245,19 @@ export default function ProvidersPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex min-w-0 flex-col gap-6 px-1 sm:px-0">
|
||||
{/* OAuth Providers */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold flex items-center gap-2">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<h2 className="text-lg sm:text-xl font-semibold flex items-center gap-2 leading-tight">
|
||||
OAuth Providers
|
||||
</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:items-center">
|
||||
<ModelAvailabilityBadge />
|
||||
<button
|
||||
onClick={() => handleBatchTest("oauth")}
|
||||
disabled={!!testingMode}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors ${
|
||||
className={`flex w-full items-center justify-center gap-1.5 rounded-lg border px-3 py-2 text-xs font-medium transition-colors sm:w-auto sm:py-1.5 ${
|
||||
testingMode === "oauth"
|
||||
? "bg-primary/20 border-primary/40 text-primary animate-pulse"
|
||||
: "bg-bg border-border text-text-muted hover:text-text-main hover:border-primary/40"
|
||||
@@ -274,7 +274,7 @@ export default function ProvidersPage() {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{Object.entries(OAUTH_PROVIDERS).map(([key, info]) => (
|
||||
<ProviderCard
|
||||
key={key}
|
||||
@@ -288,16 +288,16 @@ export default function ProvidersPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Free & Free Tier Providers */}
|
||||
{/* Free Tier Providers */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold flex items-center gap-2">
|
||||
Free & Free Tier Providers
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<h2 className="text-lg sm:text-xl font-semibold flex items-center gap-2 leading-tight">
|
||||
Free Tier Providers
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => handleBatchTest("free")}
|
||||
disabled={!!testingMode}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors ${
|
||||
className={`flex w-full items-center justify-center gap-1.5 rounded-lg border px-3 py-2 text-xs font-medium transition-colors sm:w-auto sm:py-1.5 ${
|
||||
testingMode === "free"
|
||||
? "bg-primary/20 border-primary/40 text-primary animate-pulse"
|
||||
: "bg-bg border-border text-text-muted hover:text-text-main hover:border-primary/40"
|
||||
@@ -313,7 +313,7 @@ export default function ProvidersPage() {
|
||||
{testingMode === "free" ? "Testing..." : "Test All"}
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{Object.entries(FREE_PROVIDERS).map(([key, info]) => (
|
||||
<ProviderCard
|
||||
key={key}
|
||||
@@ -339,14 +339,14 @@ export default function ProvidersPage() {
|
||||
|
||||
{/* API Key Providers — fixed list */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold flex items-center gap-2">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<h2 className="text-lg sm:text-xl font-semibold flex items-center gap-2 leading-tight">
|
||||
API Key Providers{" "}
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => handleBatchTest("apikey")}
|
||||
disabled={!!testingMode}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors ${
|
||||
className={`flex w-full items-center justify-center gap-1.5 rounded-lg border px-3 py-2 text-xs font-medium transition-colors sm:w-auto sm:py-1.5 ${
|
||||
testingMode === "apikey"
|
||||
? "bg-primary/20 border-primary/40 text-primary animate-pulse"
|
||||
: "bg-bg border-border text-text-muted hover:text-text-main hover:border-primary/40"
|
||||
@@ -362,7 +362,7 @@ export default function ProvidersPage() {
|
||||
{testingMode === "apikey" ? "Testing..." : "Test All"}
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{Object.entries(APIKEY_PROVIDERS)
|
||||
.filter(([, info]) => (info.serviceKinds ?? ["llm"]).includes("llm"))
|
||||
.map(([key, info]) => (
|
||||
@@ -401,11 +401,11 @@ export default function ProvidersPage() {
|
||||
|
||||
{/* API Key Compatible Providers — dynamic (OpenAI/Anthropic compatible) */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold flex items-center gap-2">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<h2 className="text-lg sm:text-xl font-semibold flex items-center gap-2 leading-tight">
|
||||
API Key Compatible Providers{" "}
|
||||
</h2>
|
||||
<div className="flex gap-2">
|
||||
<div className="grid grid-cols-1 gap-2 sm:flex sm:w-auto">
|
||||
{/* {(compatibleProviders.length > 0 || anthropicCompatibleProviders.length > 0) && (
|
||||
<button
|
||||
onClick={() => handleBatchTest("compatible")}
|
||||
@@ -426,6 +426,7 @@ export default function ProvidersPage() {
|
||||
size="sm"
|
||||
icon="add"
|
||||
onClick={() => setShowAddAnthropicCompatibleModal(true)}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
Add Anthropic Compatible
|
||||
</Button>
|
||||
@@ -434,7 +435,7 @@ export default function ProvidersPage() {
|
||||
variant="secondary"
|
||||
icon="add"
|
||||
onClick={() => setShowAddCompatibleModal(true)}
|
||||
className="!bg-white !text-black hover:!bg-gray-100"
|
||||
className="w-full !bg-white !text-black hover:!bg-gray-100 sm:w-auto"
|
||||
>
|
||||
Add OpenAI Compatible
|
||||
</Button>
|
||||
@@ -455,7 +456,7 @@ export default function ProvidersPage() {
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{[...compatibleProviders, ...anthropicCompatibleProviders].map(
|
||||
(info) => (
|
||||
<ApiKeyProviderCard
|
||||
@@ -494,12 +495,12 @@ export default function ProvidersPage() {
|
||||
{/* Test Results Modal */}
|
||||
{testResults && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-start justify-center pt-[10vh]"
|
||||
className="fixed inset-0 z-50 flex items-start justify-center px-3 pt-[6vh] sm:pt-[10vh]"
|
||||
onClick={() => setTestResults(null)}
|
||||
>
|
||||
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
|
||||
<div
|
||||
className="relative bg-surface border border-border rounded-xl w-full max-w-[600px] max-h-[80vh] overflow-y-auto shadow-2xl"
|
||||
className="relative bg-surface border border-border rounded-xl w-full max-w-[600px] max-h-[86vh] sm:max-h-[80vh] overflow-y-auto shadow-2xl"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="sticky top-0 z-10 flex items-center justify-between px-5 py-3 border-b border-border bg-surface/95 backdrop-blur-sm rounded-t-xl">
|
||||
@@ -540,15 +541,15 @@ function ProviderCard({ providerId, provider, stats, authType, onToggle }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Link href={`/dashboard/providers/${providerId}`} className="group">
|
||||
<Link href={`/dashboard/providers/${providerId}`} className="group min-w-0">
|
||||
<Card
|
||||
padding="xs"
|
||||
className={`h-full hover:bg-black/[0.01] dark:hover:bg-white/[0.01] transition-colors cursor-pointer ${allDisabled ? "opacity-50" : ""}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex min-w-0 items-center justify-between gap-3">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div
|
||||
className="size-8 rounded-lg flex items-center justify-center"
|
||||
className="size-8 shrink-0 rounded-lg flex items-center justify-center"
|
||||
style={{
|
||||
backgroundColor: `${provider.color?.length > 7 ? provider.color : provider.color + "15"}`,
|
||||
}}
|
||||
@@ -564,9 +565,9 @@ function ProviderCard({ providerId, provider, stats, authType, onToggle }) {
|
||||
fallbackColor={provider.color}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold">{provider.name}</h3>
|
||||
<div className="flex items-center gap-2 text-xs flex-wrap">
|
||||
<div className="min-w-0">
|
||||
<h3 className="truncate font-semibold">{provider.name}</h3>
|
||||
<div className="flex min-w-0 items-center gap-1.5 text-xs flex-wrap">
|
||||
{allDisabled ? (
|
||||
<Badge variant="default" size="sm">
|
||||
<span className="flex items-center gap-1">
|
||||
@@ -589,10 +590,10 @@ function ProviderCard({ providerId, provider, stats, authType, onToggle }) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
{stats.total > 0 && (
|
||||
<div
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
className="opacity-100 transition-opacity sm:opacity-0 sm:group-hover:opacity-100"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -668,15 +669,15 @@ function ApiKeyProviderCard({
|
||||
};
|
||||
|
||||
return (
|
||||
<Link href={`/dashboard/providers/${providerId}`} className="group">
|
||||
<Link href={`/dashboard/providers/${providerId}`} className="group min-w-0">
|
||||
<Card
|
||||
padding="xs"
|
||||
className={`h-full hover:bg-black/[0.01] dark:hover:bg-white/[0.01] transition-colors cursor-pointer ${allDisabled ? "opacity-50" : ""}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex min-w-0 items-center justify-between gap-3">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div
|
||||
className="size-8 rounded-lg flex items-center justify-center"
|
||||
className="size-8 shrink-0 rounded-lg flex items-center justify-center"
|
||||
style={{
|
||||
backgroundColor: `${provider.color?.length > 7 ? provider.color : provider.color + "15"}`,
|
||||
}}
|
||||
@@ -692,9 +693,9 @@ function ApiKeyProviderCard({
|
||||
fallbackColor={provider.color}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold">{provider.name}</h3>
|
||||
<div className="flex items-center gap-2 text-xs flex-wrap">
|
||||
<div className="min-w-0">
|
||||
<h3 className="truncate font-semibold">{provider.name}</h3>
|
||||
<div className="flex min-w-0 items-center gap-1.5 text-xs flex-wrap">
|
||||
{allDisabled ? (
|
||||
<Badge variant="default" size="sm">
|
||||
<span className="flex items-center gap-1">
|
||||
@@ -727,10 +728,10 @@ function ApiKeyProviderCard({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
{stats.total > 0 && (
|
||||
<div
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
className="opacity-100 transition-opacity sm:opacity-0 sm:group-hover:opacity-100"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -927,17 +928,18 @@ function AddOpenAICompatibleModal({ isOpen, onClose, onCreated }) {
|
||||
placeholder="e.g. gpt-4, claude-3-opus"
|
||||
hint="If provider lacks /models endpoint, enter a model ID to validate via chat/completions instead."
|
||||
/>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
||||
<Button
|
||||
onClick={handleValidate}
|
||||
disabled={!checkKey || validating || !formData.baseUrl.trim()}
|
||||
variant="secondary"
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
{validating ? "Checking..." : "Check"}
|
||||
</Button>
|
||||
{renderValidationResult()}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
fullWidth
|
||||
@@ -1108,17 +1110,18 @@ function AddAnthropicCompatibleModal({ isOpen, onClose, onCreated }) {
|
||||
placeholder="e.g. claude-3-opus"
|
||||
hint="If provider lacks /models endpoint, enter a model ID to validate via chat/completions instead."
|
||||
/>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
||||
<Button
|
||||
onClick={handleValidate}
|
||||
disabled={!checkKey || validating || !formData.baseUrl.trim()}
|
||||
variant="secondary"
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
{validating ? "Checking..." : "Check"}
|
||||
</Button>
|
||||
{renderValidationResult()}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
fullWidth
|
||||
@@ -1170,9 +1173,9 @@ function ProviderTestResultsView({ results }) {
|
||||
}[mode] || mode;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex min-w-0 flex-col gap-3">
|
||||
{summary && (
|
||||
<div className="flex items-center gap-3 text-xs mb-1">
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs mb-1 sm:gap-3">
|
||||
<span className="text-text-muted">{modeLabel} Test</span>
|
||||
<span className="px-2 py-0.5 rounded bg-emerald-500/15 text-emerald-400 font-medium">
|
||||
{summary.passed} passed
|
||||
@@ -1182,7 +1185,7 @@ function ProviderTestResultsView({ results }) {
|
||||
{summary.failed} failed
|
||||
</span>
|
||||
)}
|
||||
<span className="text-text-muted ml-auto">
|
||||
<span className="text-text-muted sm:ml-auto">
|
||||
{summary.total} tested
|
||||
</span>
|
||||
</div>
|
||||
@@ -1190,24 +1193,28 @@ function ProviderTestResultsView({ results }) {
|
||||
{items.map((r, i) => (
|
||||
<div
|
||||
key={r.connectionId || i}
|
||||
className="flex items-center gap-2 text-xs px-3 py-2 rounded-lg bg-black/[0.03] dark:bg-white/[0.03]"
|
||||
className="flex min-w-0 flex-wrap items-center gap-2 rounded-lg bg-black/[0.03] px-3 py-2 text-xs dark:bg-white/[0.03] sm:flex-nowrap"
|
||||
>
|
||||
<span
|
||||
className={`material-symbols-outlined text-[16px] ${r.valid ? "text-emerald-500" : "text-red-500"}`}
|
||||
>
|
||||
{r.valid ? "check_circle" : "error"}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="font-medium">{r.connectionName}</span>
|
||||
<span className="text-text-muted ml-1.5">({r.provider})</span>
|
||||
<div className="min-w-0 flex-[1_1_160px]">
|
||||
<span className="block truncate font-medium sm:inline">
|
||||
{r.connectionName}
|
||||
</span>
|
||||
<span className="block truncate text-text-muted sm:ml-1.5 sm:inline">
|
||||
({r.provider})
|
||||
</span>
|
||||
</div>
|
||||
{r.latencyMs !== undefined && (
|
||||
<span className="text-text-muted font-mono tabular-nums">
|
||||
<span className="shrink-0 text-text-muted font-mono tabular-nums">
|
||||
{r.latencyMs}ms
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className={`text-[10px] uppercase font-bold px-1.5 py-0.5 rounded ${
|
||||
className={`shrink-0 text-[10px] uppercase font-bold px-1.5 py-0.5 rounded ${
|
||||
r.valid
|
||||
? "bg-emerald-500/15 text-emerald-400"
|
||||
: "bg-red-500/15 text-red-400"
|
||||
|
||||
@@ -325,7 +325,7 @@ export default function ProxyPoolsPage() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="mx-auto flex w-full max-w-5xl flex-col gap-4 px-1 sm:gap-6 sm:px-0">
|
||||
<CardSkeleton />
|
||||
<CardSkeleton />
|
||||
</div>
|
||||
@@ -333,16 +333,16 @@ export default function ProxyPoolsPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold">Proxy Pools</h1>
|
||||
<div className="mx-auto flex w-full max-w-5xl flex-col gap-4 px-1 sm:gap-6 sm:px-0">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-xl font-semibold sm:text-2xl">Proxy Pools</h1>
|
||||
<p className="text-sm text-text-muted mt-1">
|
||||
Manage reusable per-connection proxies and bind them to provider connections.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="grid grid-cols-1 gap-2 sm:flex sm:items-center">
|
||||
<Button variant="secondary" icon="cloud_upload" onClick={openVercelModal}>
|
||||
Vercel Relay
|
||||
</Button>
|
||||
@@ -354,8 +354,8 @@ export default function ProxyPoolsPage() {
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="mb-4 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge variant="default">Total: {proxyPools.length}</Badge>
|
||||
<Badge variant="success">Active: {activeCount}</Badge>
|
||||
</div>
|
||||
@@ -372,10 +372,10 @@ export default function ProxyPoolsPage() {
|
||||
) : (
|
||||
<div className="flex flex-col divide-y divide-black/[0.04] dark:divide-white/[0.05]">
|
||||
{proxyPools.map((pool) => (
|
||||
<div key={pool.id} className="py-3 flex items-center justify-between gap-3 group">
|
||||
<div key={pool.id} className="group flex flex-col gap-3 py-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<p className="text-sm font-medium truncate">{pool.name}</p>
|
||||
<p className="min-w-0 max-w-full truncate text-sm font-medium sm:max-w-[18rem]">{pool.name}</p>
|
||||
<Badge variant={getStatusVariant(pool.testStatus)} size="sm" dot>
|
||||
{pool.testStatus || "unknown"}
|
||||
</Badge>
|
||||
@@ -399,7 +399,7 @@ export default function ProxyPoolsPage() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<div className="flex items-center justify-end gap-1 opacity-100 transition-opacity sm:opacity-0 sm:group-hover:opacity-100">
|
||||
<button
|
||||
onClick={() => handleTest(pool.id)}
|
||||
className="p-2 rounded hover:bg-black/5 dark:hover:bg-white/5 text-text-muted hover:text-primary"
|
||||
@@ -453,7 +453,7 @@ export default function ProxyPoolsPage() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<Button fullWidth onClick={handleBatchImport} disabled={!batchImportText.trim() || importing}>
|
||||
{importing ? "Importing..." : "Import"}
|
||||
</Button>
|
||||
@@ -497,7 +497,7 @@ export default function ProxyPoolsPage() {
|
||||
placeholder="my-relay"
|
||||
hint="Unique name for your Vercel project. Leave empty for auto-generated name."
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<Button
|
||||
fullWidth
|
||||
onClick={handleVercelDeploy}
|
||||
@@ -538,7 +538,7 @@ export default function ProxyPoolsPage() {
|
||||
hint="Comma-separated hosts/domains to bypass proxy"
|
||||
/>
|
||||
|
||||
<div className="rounded-lg border border-border/50 p-3 flex items-center justify-between">
|
||||
<div className="flex flex-col gap-3 rounded-lg border border-border/50 p-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p className="font-medium text-sm">Active</p>
|
||||
<p className="text-xs text-text-muted">Inactive pools are ignored by runtime resolution.</p>
|
||||
@@ -550,7 +550,7 @@ export default function ProxyPoolsPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-border/50 p-3 flex items-center justify-between">
|
||||
<div className="flex flex-col gap-3 rounded-lg border border-border/50 p-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p className="font-medium text-sm">Strict Proxy</p>
|
||||
<p className="text-xs text-text-muted">Fail request if proxy is unreachable instead of falling back to direct.</p>
|
||||
@@ -562,7 +562,7 @@ export default function ProxyPoolsPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<Button
|
||||
fullWidth
|
||||
onClick={handleSave}
|
||||
|
||||
@@ -8,22 +8,22 @@ const fmtCost = (n) => `$${(n || 0).toFixed(2)}`;
|
||||
|
||||
export default function OverviewCards({ stats }) {
|
||||
return (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<Card className="px-4 py-3 flex flex-col gap-1">
|
||||
<div className="grid min-w-0 grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-4 sm:gap-4">
|
||||
<Card className="flex min-w-0 flex-col gap-1 px-4 py-3">
|
||||
<span className="text-text-muted text-sm uppercase font-semibold">Total Requests</span>
|
||||
<span className="text-2xl font-bold">{fmt(stats.totalRequests)}</span>
|
||||
<span className="truncate text-2xl font-bold">{fmt(stats.totalRequests)}</span>
|
||||
</Card>
|
||||
<Card className="px-4 py-3 flex flex-col gap-1">
|
||||
<Card className="flex min-w-0 flex-col gap-1 px-4 py-3">
|
||||
<span className="text-text-muted text-sm uppercase font-semibold">Total Input Tokens</span>
|
||||
<span className="text-2xl font-bold text-primary">{fmt(stats.totalPromptTokens)}</span>
|
||||
<span className="truncate text-2xl font-bold text-primary">{fmt(stats.totalPromptTokens)}</span>
|
||||
</Card>
|
||||
<Card className="px-4 py-3 flex flex-col gap-1">
|
||||
<Card className="flex min-w-0 flex-col gap-1 px-4 py-3">
|
||||
<span className="text-text-muted text-sm uppercase font-semibold">Output Tokens</span>
|
||||
<span className="text-2xl font-bold text-success">{fmt(stats.totalCompletionTokens)}</span>
|
||||
<span className="truncate text-2xl font-bold text-success">{fmt(stats.totalCompletionTokens)}</span>
|
||||
</Card>
|
||||
<Card className="px-4 py-3 flex flex-col gap-1">
|
||||
<Card className="flex min-w-0 flex-col gap-1 px-4 py-3">
|
||||
<span className="text-text-muted text-sm uppercase font-semibold">Est. Cost</span>
|
||||
<span className="text-2xl font-bold text-warning">~{fmtCost(stats.totalCost)}</span>
|
||||
<span className="truncate text-2xl font-bold text-warning">~{fmtCost(stats.totalCost)}</span>
|
||||
<span className="text-[10px] text-text-muted">Estimated, not actual billing</span>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -29,6 +29,7 @@ export default function ProviderLimits() {
|
||||
const [proxyPools, setProxyPools] = useState([]);
|
||||
const [providerFilter, setProviderFilter] = useState("all");
|
||||
const [expiringFirst, setExpiringFirst] = useState(false);
|
||||
const [providerMenuOpen, setProviderMenuOpen] = useState(false);
|
||||
|
||||
const intervalRef = useRef(null);
|
||||
const countdownRef = useRef(null);
|
||||
@@ -389,6 +390,7 @@ export default function ProviderLimits() {
|
||||
});
|
||||
|
||||
const providerOptions = Array.from(new Set(filteredConnections.map((conn) => conn.provider))).sort();
|
||||
const selectedProviderLabel = providerFilter === "all" ? "All providers" : providerFilter;
|
||||
|
||||
// Calculate summary stats
|
||||
const totalProviders = sortedConnections.length;
|
||||
@@ -431,8 +433,8 @@ export default function ProviderLimits() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header Controls */}
|
||||
<div className="flex items-center justify-between flex-wrap gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:gap-3">
|
||||
<h2 className="text-xl font-semibold text-text-primary">
|
||||
Provider Limits
|
||||
</h2>
|
||||
@@ -441,31 +443,89 @@ export default function ProviderLimits() {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<select
|
||||
value={providerFilter}
|
||||
onChange={(event) => setProviderFilter(event.target.value)}
|
||||
className="h-10 rounded-lg border border-black/10 bg-transparent px-3 text-sm text-text-primary dark:border-white/10"
|
||||
aria-label="Filter quota providers"
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setProviderMenuOpen((prev) => !prev)}
|
||||
className="flex h-10 min-w-[116px] items-center justify-between gap-2 rounded-xl border border-black/10 bg-black/[0.02] px-3 text-sm text-text-primary transition-colors hover:bg-black/5 dark:border-white/10 dark:bg-white/[0.03] dark:hover:bg-white/10 sm:min-w-[180px]"
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={providerMenuOpen}
|
||||
title="Filter quota providers"
|
||||
>
|
||||
<option value="all">All providers</option>
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
{providerFilter === "all" ? (
|
||||
<span className="material-symbols-outlined text-[20px] text-text-muted">apps</span>
|
||||
) : (
|
||||
<ProviderIcon
|
||||
src={`/providers/${providerFilter}.png`}
|
||||
alt={providerFilter}
|
||||
size={22}
|
||||
className="size-[22px] rounded-md object-contain"
|
||||
fallbackText={providerFilter.slice(0, 2).toUpperCase()}
|
||||
/>
|
||||
)}
|
||||
<span className="truncate capitalize hidden sm:inline">{selectedProviderLabel}</span>
|
||||
</span>
|
||||
<span className="material-symbols-outlined text-[18px] text-text-muted">expand_more</span>
|
||||
</button>
|
||||
|
||||
{providerMenuOpen && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="fixed inset-0 z-30 bg-transparent"
|
||||
aria-label="Close provider filter"
|
||||
onClick={() => setProviderMenuOpen(false)}
|
||||
/>
|
||||
<div className="absolute left-0 z-40 mt-2 w-64 overflow-hidden rounded-2xl border border-black/10 bg-surface/95 p-1.5 shadow-xl shadow-black/10 backdrop-blur dark:border-white/10 dark:bg-surface/95 sm:w-72">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setProviderFilter("all"); setProviderMenuOpen(false); }}
|
||||
className={`flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-left text-sm transition-colors ${providerFilter === "all" ? "bg-primary/10 text-primary" : "text-text-primary hover:bg-black/5 dark:hover:bg-white/10"}`}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[22px]">apps</span>
|
||||
<span className="font-medium">All providers</span>
|
||||
{providerFilter === "all" && <span className="material-symbols-outlined ml-auto text-[20px]">check</span>}
|
||||
</button>
|
||||
<div className="my-1 h-px bg-black/10 dark:bg-white/10" />
|
||||
<div className="max-h-72 overflow-y-auto pr-1">
|
||||
{providerOptions.map((provider) => (
|
||||
<option key={provider} value={provider}>{provider}</option>
|
||||
<button
|
||||
key={provider}
|
||||
type="button"
|
||||
onClick={() => { setProviderFilter(provider); setProviderMenuOpen(false); }}
|
||||
className={`flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-left text-sm transition-colors ${providerFilter === provider ? "bg-primary/10 text-primary" : "text-text-primary hover:bg-black/5 dark:hover:bg-white/10"}`}
|
||||
>
|
||||
<ProviderIcon
|
||||
src={`/providers/${provider}.png`}
|
||||
alt={provider}
|
||||
size={24}
|
||||
className="size-6 rounded-md object-contain"
|
||||
fallbackText={provider.slice(0, 2).toUpperCase()}
|
||||
/>
|
||||
<span className="font-medium capitalize">{provider}</span>
|
||||
{providerFilter === provider && <span className="material-symbols-outlined ml-auto text-[20px]">check</span>}
|
||||
</button>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpiringFirst((prev) => !prev)}
|
||||
className={`flex items-center gap-2 rounded-lg border px-3 py-2 text-sm transition-colors ${expiringFirst ? "border-amber-500/40 bg-amber-500/10 text-amber-500" : "border-black/10 text-text-primary hover:bg-black/5 dark:border-white/10 dark:hover:bg-white/5"}`}
|
||||
className={`flex shrink-0 items-center gap-1.5 rounded-lg border px-3 py-2 text-sm transition-colors ${expiringFirst ? "border-amber-500/40 bg-amber-500/10 text-amber-500" : "border-black/10 text-text-primary hover:bg-black/5 dark:border-white/10 dark:hover:bg-white/5"}`}
|
||||
title="Sort accounts by earliest quota reset time"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[18px]">hourglass_top</span>
|
||||
Expiring first
|
||||
<span className="hidden sm:inline">Expiring first</span>
|
||||
</button>
|
||||
{/* Auto-refresh toggle */}
|
||||
<button
|
||||
onClick={() => setAutoRefresh((prev) => !prev)}
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-lg border border-black/10 dark:border-white/10 hover:bg-black/5 dark:hover:bg-white/5 transition-colors"
|
||||
className="flex shrink-0 items-center gap-2 rounded-lg border border-black/10 px-3 py-2 transition-colors hover:bg-black/5 dark:border-white/10 dark:hover:bg-white/5"
|
||||
title={autoRefresh ? "Disable auto-refresh" : "Enable auto-refresh"}
|
||||
>
|
||||
<span
|
||||
@@ -475,7 +535,7 @@ export default function ProviderLimits() {
|
||||
>
|
||||
{autoRefresh ? "toggle_on" : "toggle_off"}
|
||||
</span>
|
||||
<span className="text-sm text-text-primary">Auto-refresh</span>
|
||||
<span className="hidden text-sm text-text-primary sm:inline">Auto-refresh</span>
|
||||
{autoRefresh && (
|
||||
<span className="text-xs text-text-muted">({countdown}s)</span>
|
||||
)}
|
||||
|
||||
@@ -221,7 +221,7 @@ export default function ProviderTopology({ providers = [], activeRequests = [],
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="w-full rounded-lg border border-border bg-bg-subtle/30" style={{ height: 480 }}>
|
||||
<div className="h-[320px] w-full min-w-0 rounded-lg border border-border bg-bg-subtle/30 sm:h-[480px]">
|
||||
{providers.length === 0 ? (
|
||||
<div className="h-full flex items-center justify-center text-text-muted text-sm">
|
||||
No providers connected
|
||||
|
||||
@@ -169,10 +169,10 @@ export default function RequestDetailsTab() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex min-w-0 flex-col gap-6">
|
||||
<Card padding="md">
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div className="flex min-w-0 flex-col gap-2">
|
||||
<label htmlFor="provider-filter" className="text-sm font-medium text-text-main">Provider</label>
|
||||
<select
|
||||
id="provider-filter"
|
||||
@@ -181,7 +181,7 @@ export default function RequestDetailsTab() {
|
||||
className={cn(
|
||||
"h-9 px-3 rounded-lg border border-black/10 dark:border-white/10 bg-surface",
|
||||
"text-sm text-text-main focus:outline-none focus:ring-2 focus:ring-primary/20",
|
||||
"cursor-pointer min-w-[150px]"
|
||||
"w-full min-w-0 cursor-pointer"
|
||||
)}
|
||||
>
|
||||
<option value="">All Providers</option>
|
||||
@@ -193,7 +193,7 @@ export default function RequestDetailsTab() {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex min-w-0 flex-col gap-2">
|
||||
<label htmlFor="start-date-filter" className="text-sm font-medium text-text-main">Start Date</label>
|
||||
<input
|
||||
id="start-date-filter"
|
||||
@@ -202,12 +202,12 @@ export default function RequestDetailsTab() {
|
||||
onChange={(e) => setFilters({ ...filters, startDate: e.target.value })}
|
||||
className={cn(
|
||||
"h-9 px-3 rounded-lg border border-black/10 dark:border-white/10 bg-surface",
|
||||
"text-sm text-text-main focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
"w-full min-w-0 text-sm text-text-main focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex min-w-0 flex-col gap-2">
|
||||
<label htmlFor="end-date-filter" className="text-sm font-medium text-text-main">End Date</label>
|
||||
<input
|
||||
id="end-date-filter"
|
||||
@@ -216,17 +216,18 @@ export default function RequestDetailsTab() {
|
||||
onChange={(e) => setFilters({ ...filters, endDate: e.target.value })}
|
||||
className={cn(
|
||||
"h-9 px-3 rounded-lg border border-black/10 dark:border-white/10 bg-surface",
|
||||
"text-sm text-text-main focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
"w-full min-w-0 text-sm text-text-main focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-sm font-medium text-text-main opacity-0" aria-hidden="true">Clear</span>
|
||||
<div className="flex min-w-0 flex-col gap-2 sm:col-span-2 lg:col-span-1">
|
||||
<span className="hidden text-sm font-medium text-text-main opacity-0 lg:block" aria-hidden="true">Clear</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={handleClearFilters}
|
||||
disabled={!filters.provider && !filters.startDate && !filters.endDate}
|
||||
className="w-full"
|
||||
>
|
||||
Clear Filters
|
||||
</Button>
|
||||
@@ -236,7 +237,7 @@ export default function RequestDetailsTab() {
|
||||
|
||||
<Card padding="none">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<table className="w-full min-w-[880px]">
|
||||
<thead>
|
||||
<tr className="border-b border-black/5 dark:border-white/5">
|
||||
<th className="text-left p-4 text-sm font-semibold text-text-main">Timestamp</th>
|
||||
@@ -270,13 +271,13 @@ export default function RequestDetailsTab() {
|
||||
key={`${detail.id}-${index}`}
|
||||
className="border-b border-black/5 dark:border-white/5 last:border-b-0 hover:bg-black/[0.02] dark:hover:bg-white/[0.02] transition-colors"
|
||||
>
|
||||
<td className="p-4 text-sm text-text-main">
|
||||
<td className="whitespace-nowrap p-4 text-sm text-text-main">
|
||||
{new Date(detail.timestamp).toLocaleString()}
|
||||
</td>
|
||||
<td className="p-4 text-sm text-text-main font-mono">
|
||||
<td className="max-w-[260px] truncate p-4 font-mono text-sm text-text-main">
|
||||
{detail.model}
|
||||
</td>
|
||||
<td className="p-4 text-sm text-text-main">
|
||||
<td className="max-w-[180px] truncate p-4 text-sm text-text-main">
|
||||
<span className="font-medium">
|
||||
{getProviderName(detail.provider, providerNameCache)}
|
||||
</span>
|
||||
@@ -330,10 +331,10 @@ export default function RequestDetailsTab() {
|
||||
>
|
||||
{selectedDetail && (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div className="grid min-w-0 grid-cols-1 gap-4 text-sm sm:grid-cols-2">
|
||||
<div>
|
||||
<span className="text-text-muted">ID:</span>{" "}
|
||||
<span className="text-text-main font-mono">{selectedDetail.id}</span>
|
||||
<span className="break-all font-mono text-text-main">{selectedDetail.id}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-text-muted">Timestamp:</span>{" "}
|
||||
@@ -378,14 +379,14 @@ export default function RequestDetailsTab() {
|
||||
|
||||
<div className="space-y-4">
|
||||
<CollapsibleSection title="1. Client Request (Input)" defaultOpen={true} icon="input">
|
||||
<pre className="bg-black/5 dark:bg-white/5 p-4 rounded-lg overflow-auto max-h-[300px] text-xs font-mono text-text-main border border-black/5 dark:border-white/5">
|
||||
<pre className="max-h-[300px] max-w-full overflow-auto rounded-lg border border-black/5 bg-black/5 p-3 font-mono text-xs text-text-main dark:border-white/5 dark:bg-white/5 sm:p-4">
|
||||
{JSON.stringify(selectedDetail.request, null, 2)}
|
||||
</pre>
|
||||
</CollapsibleSection>
|
||||
|
||||
{selectedDetail.providerRequest && (
|
||||
<CollapsibleSection title="2. Provider Request (Translated)" icon="translate">
|
||||
<pre className="bg-black/5 dark:bg-white/5 p-4 rounded-lg overflow-auto max-h-[300px] text-xs font-mono text-text-main border border-black/5 dark:border-white/5">
|
||||
<pre className="max-h-[300px] max-w-full overflow-auto rounded-lg border border-black/5 bg-black/5 p-3 font-mono text-xs text-text-main dark:border-white/5 dark:bg-white/5 sm:p-4">
|
||||
{JSON.stringify(selectedDetail.providerRequest, null, 2)}
|
||||
</pre>
|
||||
</CollapsibleSection>
|
||||
@@ -393,7 +394,7 @@ export default function RequestDetailsTab() {
|
||||
|
||||
{selectedDetail.providerResponse && (
|
||||
<CollapsibleSection title="3. Provider Response (Raw)" icon="data_object">
|
||||
<pre className="bg-black/5 dark:bg-white/5 p-4 rounded-lg overflow-auto max-h-[300px] text-xs font-mono text-text-main border border-black/5 dark:border-white/5">
|
||||
<pre className="max-h-[300px] max-w-full overflow-auto rounded-lg border border-black/5 bg-black/5 p-3 font-mono text-xs text-text-main dark:border-white/5 dark:bg-white/5 sm:p-4">
|
||||
{typeof selectedDetail.providerResponse === 'object'
|
||||
? JSON.stringify(selectedDetail.providerResponse, null, 2)
|
||||
: selectedDetail.providerResponse
|
||||
@@ -409,7 +410,7 @@ export default function RequestDetailsTab() {
|
||||
<span className="material-symbols-outlined text-[16px]">psychology</span>
|
||||
Thinking Process
|
||||
</h4>
|
||||
<pre className="bg-amber-50 dark:bg-amber-950/30 p-4 rounded-lg overflow-auto max-h-[200px] text-xs font-mono text-amber-900 dark:text-amber-100 border border-amber-200 dark:border-amber-800">
|
||||
<pre className="max-h-[200px] max-w-full overflow-auto rounded-lg border border-amber-200 bg-amber-50 p-3 font-mono text-xs text-amber-900 dark:border-amber-800 dark:bg-amber-950/30 dark:text-amber-100 sm:p-4">
|
||||
{selectedDetail.response.thinking}
|
||||
</pre>
|
||||
</div>
|
||||
@@ -418,7 +419,7 @@ export default function RequestDetailsTab() {
|
||||
<h4 className="font-semibold text-text-main mb-2 text-xs uppercase tracking-wide opacity-70">
|
||||
Content
|
||||
</h4>
|
||||
<pre className="bg-black/5 dark:bg-white/5 p-4 rounded-lg overflow-auto max-h-[300px] text-xs font-mono text-text-main border border-black/5 dark:border-white/5">
|
||||
<pre className="max-h-[300px] max-w-full overflow-auto rounded-lg border border-black/5 bg-black/5 p-3 font-mono text-xs text-text-main dark:border-white/5 dark:bg-white/5 sm:p-4">
|
||||
{selectedDetail.response?.content || "[No content]"}
|
||||
</pre>
|
||||
</CollapsibleSection>
|
||||
|
||||
@@ -49,8 +49,8 @@ export default function UsageChart({ period = "7d" }) {
|
||||
const hasData = data.some((d) => d.tokens > 0 || d.cost > 0);
|
||||
|
||||
return (
|
||||
<Card className="p-4 flex flex-col gap-3">
|
||||
<div className="flex items-center gap-1 bg-bg-subtle rounded-lg p-1 border border-border self-start">
|
||||
<Card className="flex min-w-0 flex-col gap-3 p-3 sm:p-4">
|
||||
<div className="grid w-full grid-cols-2 items-center gap-1 rounded-lg border border-border bg-bg-subtle p-1 sm:w-auto sm:self-start">
|
||||
<button
|
||||
onClick={() => setViewMode("tokens")}
|
||||
className={`px-3 py-1 rounded-md text-sm font-medium transition-colors ${viewMode === "tokens" ? "bg-primary text-white shadow-sm" : "text-text-muted hover:text-text hover:bg-bg-hover"}`}
|
||||
@@ -70,7 +70,7 @@ export default function UsageChart({ period = "7d" }) {
|
||||
) : !hasData ? (
|
||||
<div className="h-48 flex items-center justify-center text-text-muted text-sm">No data for this period</div>
|
||||
) : (
|
||||
<ResponsiveContainer width="100%" height={200}>
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<AreaChart data={data} margin={{ top: 4, right: 8, left: 0, bottom: 0 }}>
|
||||
<defs>
|
||||
<linearGradient id="gradTokens" x1="0" y1="0" x2="0" y2="1">
|
||||
|
||||
@@ -35,7 +35,7 @@ function UsageContent() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex min-w-0 flex-col gap-6 px-1 sm:px-0">
|
||||
<SegmentedControl
|
||||
options={[
|
||||
{ value: "overview", label: "Overview" },
|
||||
@@ -43,6 +43,7 @@ function UsageContent() {
|
||||
]}
|
||||
value={activeTab}
|
||||
onChange={handleTabChange}
|
||||
className="w-full sm:w-auto"
|
||||
/>
|
||||
|
||||
{tabLoading ? (
|
||||
|
||||
@@ -87,6 +87,13 @@ const getPageInfo = (pathname) => {
|
||||
icon: "bar_chart",
|
||||
breadcrumbs: [],
|
||||
};
|
||||
if (pathname.includes("/auth-files"))
|
||||
return {
|
||||
title: "Auth Files",
|
||||
description: "Map provider credentials stored in the local database",
|
||||
icon: "vpn_key",
|
||||
breadcrumbs: [],
|
||||
};
|
||||
if (pathname.includes("/quota"))
|
||||
return {
|
||||
title: "Quota Tracker",
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function SegmentedControl({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"inline-flex items-center p-1 rounded-lg",
|
||||
"inline-flex items-center p-1 rounded-lg overflow-x-auto",
|
||||
"bg-black/5 dark:bg-white/5",
|
||||
className
|
||||
)}
|
||||
@@ -28,7 +28,7 @@ export default function SegmentedControl({
|
||||
key={option.value}
|
||||
onClick={() => onChange(option.value)}
|
||||
className={cn(
|
||||
"px-4 rounded-md font-medium transition-all",
|
||||
"shrink-0 px-4 rounded-md font-medium transition-all",
|
||||
sizes[size],
|
||||
value === option.value
|
||||
? "bg-white dark:bg-white/10 text-text-main shadow-sm"
|
||||
|
||||
@@ -22,6 +22,7 @@ const navItems = [
|
||||
// { href: "/dashboard/basic-chat", label: "Basic Chat", icon: "chat" }, // Hidden
|
||||
{ href: "/dashboard/combos", label: "Combos", icon: "layers" },
|
||||
{ href: "/dashboard/usage", label: "Usage", icon: "bar_chart" },
|
||||
{ href: "/dashboard/auth-files", label: "Auth Files", icon: "vpn_key" },
|
||||
{ href: "/dashboard/quota", label: "Quota Tracker", icon: "data_usage" },
|
||||
{ href: "/dashboard/mitm", label: "MITM", icon: "security" },
|
||||
{ href: "/dashboard/cli-tools", label: "CLI Tools", icon: "terminal" },
|
||||
|
||||
@@ -32,7 +32,7 @@ function TimeAgo({ timestamp }) {
|
||||
|
||||
function RecentRequests({ requests = [] }) {
|
||||
return (
|
||||
<Card className="flex flex-col overflow-hidden" padding="sm" style={{ height: 480 }}>
|
||||
<Card className="flex min-w-0 flex-col overflow-hidden" padding="sm" style={{ height: 480 }}>
|
||||
{/* Header */}
|
||||
<div className="px-1 py-2 border-b border-border shrink-0">
|
||||
<span className="text-xs font-semibold text-text-muted uppercase tracking-wide">Recent Requests</span>
|
||||
@@ -42,7 +42,7 @@ function RecentRequests({ requests = [] }) {
|
||||
<div className="flex-1 flex items-center justify-center text-text-muted text-sm">No requests yet.</div>
|
||||
) : (
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<table className="w-full text-xs border-collapse">
|
||||
<table className="w-full min-w-[420px] border-collapse text-xs">
|
||||
<thead className="sticky top-0 bg-bg z-10">
|
||||
<tr className="border-b border-border">
|
||||
<th className="py-1.5 text-left font-semibold text-text-muted w-2"></th>
|
||||
@@ -397,16 +397,16 @@ export default function UsageStats() {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex min-w-0 flex-col gap-6">
|
||||
{/* Period selector */}
|
||||
<div className="flex items-center gap-2 self-end">
|
||||
<div className="flex items-center gap-1 bg-bg-subtle rounded-lg p-1 border border-border">
|
||||
<div className="flex w-full items-center gap-2 sm:w-auto sm:self-end">
|
||||
<div className="grid flex-1 grid-cols-4 items-center gap-1 rounded-lg border border-border bg-bg-subtle p-1 sm:flex sm:flex-none">
|
||||
{PERIODS.map((p) => (
|
||||
<button
|
||||
key={p.value}
|
||||
onClick={() => setPeriod(p.value)}
|
||||
disabled={fetching}
|
||||
className={`px-3 py-1 rounded-md text-sm font-medium transition-colors ${period === p.value ? "bg-primary text-white shadow-sm" : "text-text-muted hover:text-text hover:bg-bg-hover"}`}
|
||||
className={`rounded-md px-3 py-1 text-sm font-medium transition-colors ${period === p.value ? "bg-primary text-white shadow-sm" : "text-text-muted hover:bg-bg-hover hover:text-text"}`}
|
||||
>
|
||||
{p.label}
|
||||
</button>
|
||||
@@ -422,7 +422,7 @@ export default function UsageStats() {
|
||||
|
||||
{/* Provider topology + Recent Requests */}
|
||||
{loading ? spinner : (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-[2fr_1fr] gap-2 items-stretch">
|
||||
<div className="grid min-w-0 grid-cols-1 items-stretch gap-2 lg:grid-cols-[minmax(0,2fr)_minmax(280px,1fr)]">
|
||||
<ProviderTopology
|
||||
providers={providers}
|
||||
activeRequests={stats.activeRequests || []}
|
||||
@@ -438,17 +438,17 @@ export default function UsageStats() {
|
||||
|
||||
{/* Table with dropdown selector */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<select
|
||||
value={tableView}
|
||||
onChange={(e) => setTableView(e.target.value)}
|
||||
className="px-3 py-1.5 rounded-lg border border-border bg-bg-subtle text-sm font-medium text-text focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
className="w-full rounded-lg border border-border bg-bg-subtle px-3 py-1.5 text-sm font-medium text-text focus:outline-none focus:ring-2 focus:ring-primary/50 sm:w-auto"
|
||||
>
|
||||
{TABLE_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="flex items-center gap-1 bg-bg-subtle rounded-lg p-1 border border-border">
|
||||
<div className="grid grid-cols-2 items-center gap-1 rounded-lg border border-border bg-bg-subtle p-1 sm:flex">
|
||||
<button
|
||||
onClick={() => setViewMode("costs")}
|
||||
className={`px-3 py-1 rounded-md text-sm font-medium transition-colors ${viewMode === "costs" ? "bg-primary text-white shadow-sm" : "text-text-muted hover:text-text hover:bg-bg-hover"}`}
|
||||
|
||||
Reference in New Issue
Block a user