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