mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-23 04:09:49 +00:00
Enhance layout
This commit is contained in:
@@ -191,23 +191,23 @@ export default function ClaudeToolCard({
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="overflow-hidden">
|
||||
<Card padding="sm" className="overflow-hidden">
|
||||
<div className="flex items-center justify-between hover:cursor-pointer" onClick={onToggle}>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="size-12 flex items-center justify-center">
|
||||
<Image src="/providers/claude.png" alt={tool.name} width={40} height={40} className="size-12 object-contain rounded-xl max-w-[48px] max-h-[48px]" sizes="48px" onError={(e) => { e.target.style.display = "none"; }} />
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="size-8 flex items-center justify-center shrink-0">
|
||||
<Image src="/providers/claude.png" alt={tool.name} width={32} height={32} className="size-8 object-contain rounded-lg" sizes="32px" onError={(e) => { e.target.style.display = "none"; }} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-semibold text-lg">{tool.name}</h3>
|
||||
{configStatus === "configured" && <span className="px-2 py-0.5 text-xs font-medium bg-green-500/10 text-green-600 dark:text-green-400 rounded-full">Connected</span>}
|
||||
{configStatus === "not_configured" && <span className="px-2 py-0.5 text-xs font-medium bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 rounded-full">Not configured</span>}
|
||||
{configStatus === "other" && <span className="px-2 py-0.5 text-xs font-medium bg-blue-500/10 text-blue-600 dark:text-blue-400 rounded-full">Other endpoint</span>}
|
||||
<h3 className="font-medium text-sm">{tool.name}</h3>
|
||||
{configStatus === "configured" && <span className="px-1.5 py-0.5 text-[10px] font-medium bg-green-500/10 text-green-600 dark:text-green-400 rounded-full">Connected</span>}
|
||||
{configStatus === "not_configured" && <span className="px-1.5 py-0.5 text-[10px] font-medium bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 rounded-full">Not configured</span>}
|
||||
{configStatus === "other" && <span className="px-1.5 py-0.5 text-[10px] font-medium bg-blue-500/10 text-blue-600 dark:text-blue-400 rounded-full">Other</span>}
|
||||
</div>
|
||||
<p className="text-sm text-text-muted">{tool.description}</p>
|
||||
<p className="text-xs text-text-muted truncate">{tool.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className={`material-symbols-outlined text-text-muted transition-transform ${isExpanded ? "rotate-180" : ""}`}>expand_more</span>
|
||||
<span className={`material-symbols-outlined text-text-muted text-[20px] transition-transform ${isExpanded ? "rotate-180" : ""}`}>expand_more</span>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
|
||||
@@ -164,23 +164,23 @@ wire_api = "responses"
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="overflow-hidden">
|
||||
<Card padding="sm" className="overflow-hidden">
|
||||
<div className="flex items-center justify-between hover:cursor-pointer" onClick={onToggle}>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="size-12 flex items-center justify-center">
|
||||
<Image src="/providers/codex.png" alt={tool.name} width={40} height={40} className="size-12 object-contain rounded-xl max-w-[48px] max-h-[48px]" sizes="48px" onError={(e) => { e.target.style.display = "none"; }} />
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="size-8 flex items-center justify-center shrink-0">
|
||||
<Image src="/providers/codex.png" alt={tool.name} width={32} height={32} className="size-8 object-contain rounded-lg" sizes="32px" onError={(e) => { e.target.style.display = "none"; }} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-semibold text-lg">{tool.name}</h3>
|
||||
{configStatus === "configured" && <span className="px-2 py-0.5 text-xs font-medium bg-green-500/10 text-green-600 dark:text-green-400 rounded-full">Connected</span>}
|
||||
{configStatus === "not_configured" && <span className="px-2 py-0.5 text-xs font-medium bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 rounded-full">Not configured</span>}
|
||||
{configStatus === "other" && <span className="px-2 py-0.5 text-xs font-medium bg-blue-500/10 text-blue-600 dark:text-blue-400 rounded-full">Other endpoint</span>}
|
||||
<h3 className="font-medium text-sm">{tool.name}</h3>
|
||||
{configStatus === "configured" && <span className="px-1.5 py-0.5 text-[10px] font-medium bg-green-500/10 text-green-600 dark:text-green-400 rounded-full">Connected</span>}
|
||||
{configStatus === "not_configured" && <span className="px-1.5 py-0.5 text-[10px] font-medium bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 rounded-full">Not configured</span>}
|
||||
{configStatus === "other" && <span className="px-1.5 py-0.5 text-[10px] font-medium bg-blue-500/10 text-blue-600 dark:text-blue-400 rounded-full">Other</span>}
|
||||
</div>
|
||||
<p className="text-sm text-text-muted">{tool.description}</p>
|
||||
<p className="text-xs text-text-muted truncate">{tool.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className={`material-symbols-outlined text-text-muted transition-transform ${isExpanded ? "rotate-180" : ""}`}>expand_more</span>
|
||||
<span className={`material-symbols-outlined text-text-muted text-[20px] transition-transform ${isExpanded ? "rotate-180" : ""}`}>expand_more</span>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
|
||||
@@ -233,43 +233,43 @@ export default function DefaultToolCard({ toolId, tool, isExpanded, onToggle, ba
|
||||
<Image
|
||||
src={tool.image}
|
||||
alt={tool.name}
|
||||
width={40}
|
||||
height={40}
|
||||
className="size-12 object-contain rounded-xl bg-gray-500 max-w-[48px] max-h-[48px]"
|
||||
sizes="48px"
|
||||
width={32}
|
||||
height={32}
|
||||
className="size-8 object-contain rounded-lg"
|
||||
sizes="32px"
|
||||
onError={(e) => { e.target.style.display = "none"; }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (tool.icon) {
|
||||
return <span className="material-symbols-outlined text-3xl" style={{ color: tool.color }}>{tool.icon}</span>;
|
||||
return <span className="material-symbols-outlined text-xl" style={{ color: tool.color }}>{tool.icon}</span>;
|
||||
}
|
||||
return (
|
||||
<Image
|
||||
src={`/providers/${toolId}.png`}
|
||||
alt={tool.name}
|
||||
width={40}
|
||||
height={40}
|
||||
className="size-10 object-contain rounded-xl max-w-[40px] max-h-[40px]"
|
||||
sizes="40px"
|
||||
width={32}
|
||||
height={32}
|
||||
className="size-8 object-contain rounded-lg"
|
||||
sizes="32px"
|
||||
onError={(e) => { e.target.style.display = "none"; }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="overflow-hidden">
|
||||
<Card padding="sm" className="overflow-hidden">
|
||||
<div className="flex items-center justify-between hover:cursor-pointer" onClick={onToggle}>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="size-12 rounded-xl flex items-center justify-center">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="size-8 rounded-lg flex items-center justify-center shrink-0">
|
||||
{renderIcon()}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg">{tool.name}</h3>
|
||||
<p className="text-sm text-text-muted">{tool.description}</p>
|
||||
<div className="min-w-0">
|
||||
<h3 className="font-medium text-sm">{tool.name}</h3>
|
||||
<p className="text-xs text-text-muted truncate">{tool.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className={`material-symbols-outlined text-text-muted transition-transform ${isExpanded ? "rotate-180" : ""}`}>expand_more</span>
|
||||
<span className={`material-symbols-outlined text-text-muted text-[20px] transition-transform ${isExpanded ? "rotate-180" : ""}`}>expand_more</span>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
|
||||
@@ -120,12 +120,13 @@ export default function CombosPage() {
|
||||
{combos.length === 0 ? (
|
||||
<Card>
|
||||
<div className="text-center py-12">
|
||||
<span className="material-symbols-outlined text-5xl text-text-muted mb-3 block">
|
||||
layers
|
||||
</span>
|
||||
<p className="text-text-muted mb-4">No combos yet</p>
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 text-primary mb-4">
|
||||
<span className="material-symbols-outlined text-[32px]">layers</span>
|
||||
</div>
|
||||
<p className="text-text-main font-medium mb-1">No combos yet</p>
|
||||
<p className="text-sm text-text-muted mb-4">Create model combos with fallback support</p>
|
||||
<Button icon="add" onClick={() => setShowCreateModal(true)}>
|
||||
Create your first combo
|
||||
Create Combo
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -168,62 +169,57 @@ export default function CombosPage() {
|
||||
|
||||
function ComboCard({ combo, copied, onCopy, onEdit, onDelete }) {
|
||||
return (
|
||||
<Card>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
{/* Name + Copy */}
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="material-symbols-outlined text-primary">layers</span>
|
||||
<code className="text-lg font-semibold font-mono">{combo.name}</code>
|
||||
<button
|
||||
onClick={() => onCopy(combo.name, `combo-${combo.id}`)}
|
||||
className="p-1 hover:bg-sidebar rounded text-text-muted hover:text-primary"
|
||||
title="Copy combo name"
|
||||
>
|
||||
<span className="material-symbols-outlined text-sm">
|
||||
{copied === `combo-${combo.id}` ? "check" : "content_copy"}
|
||||
</span>
|
||||
</button>
|
||||
<Card padding="sm" className="group">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
<div className="size-8 rounded-lg bg-primary/10 flex items-center justify-center shrink-0">
|
||||
<span className="material-symbols-outlined text-primary text-[18px]">layers</span>
|
||||
</div>
|
||||
|
||||
{/* Models list */}
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{combo.models.length === 0 ? (
|
||||
<p className="text-sm text-text-muted italic">No models added</p>
|
||||
) : (
|
||||
combo.models.map((model, index) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
<span className="text-xs text-text-muted w-5">{index + 1}.</span>
|
||||
<code className="text-sm font-mono bg-sidebar px-2 py-0.5 rounded">
|
||||
{model}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="text-sm font-medium font-mono truncate">{combo.name}</code>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onCopy(combo.name, `combo-${combo.id}`); }}
|
||||
className="p-0.5 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary transition-colors opacity-0 group-hover:opacity-100"
|
||||
title="Copy combo name"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">
|
||||
{copied === `combo-${combo.id}` ? "check" : "content_copy"}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 mt-0.5 flex-wrap">
|
||||
{combo.models.length === 0 ? (
|
||||
<span className="text-xs text-text-muted italic">No models</span>
|
||||
) : (
|
||||
combo.models.slice(0, 3).map((model, index) => (
|
||||
<code key={index} className="text-[10px] font-mono bg-black/5 dark:bg-white/5 px-1.5 py-0.5 rounded text-text-muted">
|
||||
{model.split("/").pop()}
|
||||
</code>
|
||||
{index === 0 && (
|
||||
<span className="text-xs text-primary font-medium">Primary</span>
|
||||
)}
|
||||
{index > 0 && (
|
||||
<span className="text-xs text-text-muted">Fallback</span>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
))
|
||||
)}
|
||||
{combo.models.length > 3 && (
|
||||
<span className="text-[10px] text-text-muted">+{combo.models.length - 3} more</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-1">
|
||||
<div className="flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity shrink-0">
|
||||
<button
|
||||
onClick={onEdit}
|
||||
className="p-2 hover:bg-sidebar rounded text-text-muted hover:text-primary"
|
||||
className="p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary transition-colors"
|
||||
title="Edit"
|
||||
>
|
||||
<span className="material-symbols-outlined text-lg">edit</span>
|
||||
<span className="material-symbols-outlined text-[16px]">edit</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={onDelete}
|
||||
className="p-2 hover:bg-red-50 rounded text-red-500"
|
||||
className="p-1.5 hover:bg-red-500/10 rounded text-red-500 transition-colors"
|
||||
title="Delete"
|
||||
>
|
||||
<span className="material-symbols-outlined text-lg">delete</span>
|
||||
<span className="material-symbols-outlined text-[16px]">delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -321,9 +317,8 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={isEdit ? "Edit Combo" : "Create Combo"}
|
||||
size="md"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* Name */}
|
||||
<div>
|
||||
<Input
|
||||
@@ -333,87 +328,94 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
|
||||
placeholder="my-combo"
|
||||
error={nameError}
|
||||
/>
|
||||
<p className="text-xs text-text-muted mt-1">
|
||||
<p className="text-[10px] text-text-muted mt-0.5">
|
||||
Only letters, numbers, - and _ allowed
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Models */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="text-sm font-medium">Models</label>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon="add"
|
||||
onClick={() => setShowModelSelect(true)}
|
||||
>
|
||||
Add Model
|
||||
</Button>
|
||||
</div>
|
||||
<label className="text-sm font-medium mb-1.5 block">Models</label>
|
||||
|
||||
{models.length === 0 ? (
|
||||
<div className="text-center py-6 border border-dashed border-border rounded-lg">
|
||||
<p className="text-sm text-text-muted">No models added</p>
|
||||
<p className="text-xs text-text-muted mt-1">Click "Add Model" to add</p>
|
||||
<div className="text-center py-4 border border-dashed border-black/10 dark:border-white/10 rounded-lg bg-black/[0.01] dark:bg-white/[0.01]">
|
||||
<span className="material-symbols-outlined text-text-muted text-xl mb-1">layers</span>
|
||||
<p className="text-xs text-text-muted">No models added yet</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2 max-h-[240px] overflow-y-auto">
|
||||
<div className="flex flex-col gap-1 max-h-[200px] overflow-y-auto">
|
||||
{models.map((model, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center gap-2"
|
||||
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"
|
||||
>
|
||||
{/* Priority arrows */}
|
||||
<div className="flex flex-col gap-0">
|
||||
{/* Index badge */}
|
||||
<span className="text-[10px] font-medium text-text-muted w-3 text-center shrink-0">{index + 1}</span>
|
||||
|
||||
{/* Model Input */}
|
||||
<input
|
||||
type="text"
|
||||
value={model}
|
||||
onChange={(e) => handleModelChange(index, e.target.value)}
|
||||
placeholder="provider/model"
|
||||
className="flex-1 min-w-0 px-1.5 py-0.5 text-xs font-mono bg-transparent border-0 focus:outline-none text-text-main placeholder:text-text-muted/50"
|
||||
/>
|
||||
|
||||
{/* Priority arrows - horizontal, always visible */}
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button
|
||||
onClick={() => handleMoveUp(index)}
|
||||
disabled={index === 0}
|
||||
className={`p-0.5 rounded ${index === 0 ? "text-text-muted/30" : "hover:bg-surface text-text-muted hover:text-primary"}`}
|
||||
className={`p-0.5 rounded ${index === 0 ? "text-text-muted/20 cursor-not-allowed" : "text-text-muted hover:text-primary hover:bg-black/5 dark:hover:bg-white/5"}`}
|
||||
title="Move up"
|
||||
>
|
||||
<span className="material-symbols-outlined text-sm leading-none">keyboard_arrow_up</span>
|
||||
<span className="material-symbols-outlined text-[12px]">arrow_upward</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleMoveDown(index)}
|
||||
disabled={index === models.length - 1}
|
||||
className={`p-0.5 rounded ${index === models.length - 1 ? "text-text-muted/30" : "hover:bg-surface text-text-muted hover:text-primary"}`}
|
||||
className={`p-0.5 rounded ${index === models.length - 1 ? "text-text-muted/20 cursor-not-allowed" : "text-text-muted hover:text-primary hover:bg-black/5 dark:hover:bg-white/5"}`}
|
||||
title="Move down"
|
||||
>
|
||||
<span className="material-symbols-outlined text-sm leading-none">keyboard_arrow_down</span>
|
||||
<span className="material-symbols-outlined text-[12px]">arrow_downward</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Model Input */}
|
||||
<Input
|
||||
value={model}
|
||||
onChange={(e) => handleModelChange(index, e.target.value)}
|
||||
placeholder="model-name"
|
||||
className="flex-1"
|
||||
/>
|
||||
|
||||
{/* Remove */}
|
||||
{/* Remove - always visible */}
|
||||
<button
|
||||
onClick={() => handleRemoveModel(index)}
|
||||
className="p-2 hover:bg-red-50 rounded text-red-500"
|
||||
className="p-0.5 hover:bg-red-500/10 rounded text-text-muted hover:text-red-500 transition-all"
|
||||
title="Remove"
|
||||
>
|
||||
<span className="material-symbols-outlined text-sm">close</span>
|
||||
<span className="material-symbols-outlined text-[12px]">close</span>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add Model button - moved to bottom */}
|
||||
<button
|
||||
onClick={() => setShowModelSelect(true)}
|
||||
className="w-full mt-2 py-2 border border-dashed border-black/10 dark:border-white/10 rounded-lg text-xs text-text-muted hover:text-primary hover:border-primary/30 transition-colors flex items-center justify-center gap-1"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">add</span>
|
||||
Add Model
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-2 pt-2">
|
||||
<div className="flex gap-2 pt-1">
|
||||
<Button onClick={onClose} variant="ghost" fullWidth size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
fullWidth
|
||||
size="sm"
|
||||
disabled={!name.trim() || !!nameError || saving}
|
||||
>
|
||||
{saving ? "Saving..." : "Apply"}
|
||||
</Button>
|
||||
<Button onClick={onClose} variant="ghost" fullWidth>
|
||||
Cancel
|
||||
{saving ? "Saving..." : isEdit ? "Save" : "Create"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -293,58 +293,48 @@ export default function APIPageClient({ machineId }) {
|
||||
</div>
|
||||
|
||||
{keys.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<span className="material-symbols-outlined text-4xl text-text-muted mb-2">
|
||||
vpn_key
|
||||
</span>
|
||||
<p className="text-sm text-text-muted">No API keys yet</p>
|
||||
<div className="text-center py-12">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 text-primary mb-4">
|
||||
<span className="material-symbols-outlined text-[32px]">vpn_key</span>
|
||||
</div>
|
||||
<p className="text-text-main font-medium mb-1">No API keys yet</p>
|
||||
<p className="text-sm text-text-muted mb-4">Create your first API key to get started</p>
|
||||
<Button icon="add" onClick={() => setShowAddModal(true)}>
|
||||
Create Key
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-border">
|
||||
<th className="text-left p-3 font-medium">Name</th>
|
||||
<th className="text-left p-3 font-medium">Key</th>
|
||||
<th className="text-left p-3 font-medium">Created</th>
|
||||
<th className="text-left p-3 font-medium">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{keys.map((key) => (
|
||||
<tr key={key.id} className="border-b border-border hover:bg-sidebar/30">
|
||||
<td className="p-3 text-sm">{key.name}</td>
|
||||
<td className="p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-xs text-text-muted">
|
||||
{key.key}
|
||||
</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon={copied === key.id ? "check" : "content_copy"}
|
||||
onClick={() => copy(key.key, key.id)}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-3 text-sm text-text-muted">
|
||||
{new Date(key.createdAt).toLocaleDateString()}
|
||||
</td>
|
||||
<td className="p-3">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon="delete"
|
||||
className="text-red-500"
|
||||
onClick={() => handleDeleteKey(key.id)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="flex flex-col">
|
||||
{keys.map((key) => (
|
||||
<div
|
||||
key={key.id}
|
||||
className="group flex items-center justify-between py-3 border-b border-black/[0.03] dark:border-white/[0.03] last:border-b-0"
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium">{key.name}</p>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<code className="text-xs text-text-muted font-mono">{key.key}</code>
|
||||
<button
|
||||
onClick={() => copy(key.key, key.id)}
|
||||
className="p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary opacity-0 group-hover:opacity-100 transition-all"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">
|
||||
{copied === key.id ? "check" : "content_copy"}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-text-muted mt-1">
|
||||
Created {new Date(key.createdAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleDeleteKey(key.id)}
|
||||
className="p-2 hover:bg-red-500/10 rounded text-red-500 opacity-0 group-hover:opacity-100 transition-all"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[18px]">delete</span>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card, Button, Badge, Toggle, Input } from "@/shared/components";
|
||||
import { useTheme } from "@/shared/hooks/useTheme";
|
||||
import { cn } from "@/shared/utils/cn";
|
||||
import { APP_CONFIG } from "@/shared/constants/config";
|
||||
|
||||
export default function ProfilePage() {
|
||||
@@ -117,7 +118,12 @@ export default function ProfilePage() {
|
||||
|
||||
{/* Routing Preferences */}
|
||||
<Card>
|
||||
<h3 className="text-lg font-semibold mb-4">Security</h3>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 rounded-lg bg-primary/10 text-primary">
|
||||
<span className="material-symbols-outlined text-[20px]">shield</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold">Security</h3>
|
||||
</div>
|
||||
<form onSubmit={handlePasswordChange} className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">Current Password</label>
|
||||
@@ -168,7 +174,12 @@ export default function ProfilePage() {
|
||||
|
||||
{/* Routing Preferences */}
|
||||
<Card>
|
||||
<h3 className="text-lg font-semibold mb-4">Routing Strategy</h3>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 rounded-lg bg-blue-500/10 text-blue-500">
|
||||
<span className="material-symbols-outlined text-[20px]">route</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold">Routing Strategy</h3>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
@@ -215,7 +226,12 @@ export default function ProfilePage() {
|
||||
|
||||
{/* Theme Preferences */}
|
||||
<Card>
|
||||
<h3 className="text-lg font-semibold mb-4">Appearance</h3>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 rounded-lg bg-purple-500/10 text-purple-500">
|
||||
<span className="material-symbols-outlined text-[20px]">palette</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold">Appearance</h3>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
@@ -231,34 +247,38 @@ export default function ProfilePage() {
|
||||
</div>
|
||||
|
||||
{/* Theme Options */}
|
||||
<div className="flex gap-3 pt-4 border-t border-border">
|
||||
{["light", "dark", "system"].map((option) => (
|
||||
<button
|
||||
key={option}
|
||||
onClick={() => setTheme(option)}
|
||||
className={`flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border transition-all ${
|
||||
theme === option
|
||||
? "border-primary bg-primary/5"
|
||||
: "border-border hover:border-primary/50"
|
||||
}`}
|
||||
>
|
||||
<span className="material-symbols-outlined text-2xl">
|
||||
{option === "light"
|
||||
? "light_mode"
|
||||
: option === "dark"
|
||||
? "dark_mode"
|
||||
: "contrast"}
|
||||
</span>
|
||||
<span className="text-sm font-medium capitalize">{option}</span>
|
||||
</button>
|
||||
))}
|
||||
<div className="pt-4 border-t border-border">
|
||||
<div className="inline-flex p-1 rounded-lg bg-black/5 dark:bg-white/5">
|
||||
{["light", "dark", "system"].map((option) => (
|
||||
<button
|
||||
key={option}
|
||||
onClick={() => setTheme(option)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-4 py-2 rounded-md font-medium transition-all",
|
||||
theme === option
|
||||
? "bg-white dark:bg-white/10 text-text-main shadow-sm"
|
||||
: "text-text-muted hover:text-text-main"
|
||||
)}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[20px]">
|
||||
{option === "light" ? "light_mode" : option === "dark" ? "dark_mode" : "contrast"}
|
||||
</span>
|
||||
<span className="capitalize">{option}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Data Management */}
|
||||
<Card>
|
||||
<h3 className="text-lg font-semibold mb-4">Data</h3>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 rounded-lg bg-green-500/10 text-green-500">
|
||||
<span className="material-symbols-outlined text-[20px]">database</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold">Data</h3>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between p-4 rounded-lg bg-bg border border-border">
|
||||
<div>
|
||||
|
||||
@@ -303,14 +303,18 @@ export default function ProviderDetailPage() {
|
||||
</div>
|
||||
|
||||
{connections.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<span className="material-symbols-outlined text-4xl text-text-muted mb-2">
|
||||
{isOAuth ? "lock" : "key"}
|
||||
</span>
|
||||
<p className="text-sm text-text-muted">No connections yet</p>
|
||||
<div className="text-center py-12">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 text-primary mb-4">
|
||||
<span className="material-symbols-outlined text-[32px]">{isOAuth ? "lock" : "key"}</span>
|
||||
</div>
|
||||
<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>
|
||||
<Button icon="add" onClick={() => isOAuth ? setShowOAuthModal(true) : setShowAddApiKeyModal(true)}>
|
||||
Add Connection
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col divide-y divide-black/[0.03] dark:divide-white/[0.03]">
|
||||
{connections
|
||||
.sort((a, b) => (a.priority || 0) - (b.priority || 0))
|
||||
.map((conn, index) => (
|
||||
@@ -618,7 +622,7 @@ function ConnectionRow({ connection, isOAuth, isFirst, isLast, onMoveUp, onMoveD
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`flex items-center justify-between p-3 rounded-lg border border-border hover:bg-sidebar/50 ${connection.isActive === false ? 'opacity-60' : ''}`}>
|
||||
<div className={`group flex items-center justify-between p-3 rounded-lg hover:bg-black/[0.02] dark:hover:bg-white/[0.02] transition-colors ${connection.isActive === false ? "opacity-60" : ""}`}>
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
{/* Priority arrows */}
|
||||
<div className="flex flex-col">
|
||||
@@ -666,12 +670,12 @@ function ConnectionRow({ connection, isOAuth, isFirst, isLast, onMoveUp, onMoveD
|
||||
onChange={onToggleActive}
|
||||
title={(connection.isActive ?? true) ? "Disable connection" : "Enable connection"}
|
||||
/>
|
||||
<div className="flex gap-1 ml-1">
|
||||
<button onClick={onEdit} className="p-2 hover:bg-sidebar rounded">
|
||||
<span className="material-symbols-outlined text-base">edit</span>
|
||||
<div className="flex gap-1 ml-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button onClick={onEdit} className="p-2 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary">
|
||||
<span className="material-symbols-outlined text-[18px]">edit</span>
|
||||
</button>
|
||||
<button onClick={onDelete} className="p-2 hover:bg-red-50 rounded text-red-500">
|
||||
<span className="material-symbols-outlined text-base">delete</span>
|
||||
<button onClick={onDelete} className="p-2 hover:bg-red-500/10 rounded text-red-500">
|
||||
<span className="material-symbols-outlined text-[18px]">delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -134,8 +134,8 @@ function ProviderCard({ providerId, provider, stats }) {
|
||||
const [imgError, setImgError] = useState(false);
|
||||
|
||||
return (
|
||||
<Link href={`/dashboard/providers/${providerId}`}>
|
||||
<Card padding="sm" className="h-full hover:border-primary/50 transition-colors cursor-pointer">
|
||||
<Link href={`/dashboard/providers/${providerId}`} className="group">
|
||||
<Card padding="sm" className="h-full hover:bg-black/[0.01] dark:hover:bg-white/[0.01] transition-colors cursor-pointer">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
@@ -169,7 +169,7 @@ function ProviderCard({ providerId, provider, stats }) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="material-symbols-outlined text-text-muted">
|
||||
<span className="material-symbols-outlined text-text-muted opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
chevron_right
|
||||
</span>
|
||||
</div>
|
||||
@@ -199,8 +199,8 @@ function ApiKeyProviderCard({ providerId, provider, stats }) {
|
||||
const { connected, error, errorCode, errorTime } = stats;
|
||||
|
||||
return (
|
||||
<Link href={`/dashboard/providers/${providerId}`}>
|
||||
<Card padding="sm" className="h-full hover:border-primary/50 transition-colors cursor-pointer">
|
||||
<Link href={`/dashboard/providers/${providerId}`} className="group">
|
||||
<Card padding="sm" className="h-full hover:bg-black/[0.01] dark:hover:bg-white/[0.01] transition-colors cursor-pointer">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
@@ -222,7 +222,7 @@ function ApiKeyProviderCard({ providerId, provider, stats }) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="material-symbols-outlined text-text-muted">
|
||||
<span className="material-symbols-outlined text-text-muted opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
chevron_right
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,36 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { useState, Suspense } from "react";
|
||||
import { UsageStats, RequestLogger, CardSkeleton } from "@/shared/components";
|
||||
import { UsageStats, RequestLogger, CardSkeleton, SegmentedControl } from "@/shared/components";
|
||||
|
||||
export default function UsagePage() {
|
||||
const [activeTab, setActiveTab] = useState("overview");
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Tabs */}
|
||||
<div className="flex border-b border-border">
|
||||
<button
|
||||
onClick={() => setActiveTab("overview")}
|
||||
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === "overview"
|
||||
? "border-primary text-primary"
|
||||
: "border-transparent text-text-muted hover:text-text-primary"
|
||||
}`}
|
||||
>
|
||||
Overview
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("logs")}
|
||||
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === "logs"
|
||||
? "border-primary text-primary"
|
||||
: "border-transparent text-text-muted hover:text-text-primary"
|
||||
}`}
|
||||
>
|
||||
Logger
|
||||
</button>
|
||||
</div>
|
||||
<SegmentedControl
|
||||
options={[
|
||||
{ value: "overview", label: "Overview" },
|
||||
{ value: "logs", label: "Logger" },
|
||||
]}
|
||||
value={activeTab}
|
||||
onChange={setActiveTab}
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
{activeTab === "overview" ? (
|
||||
|
||||
+50
-15
@@ -3,7 +3,7 @@
|
||||
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
/* Claude-inspired Color Palette */
|
||||
/* macOS-inspired Color Palette with Terracotta Primary */
|
||||
:root {
|
||||
/* Primary - Warm Coral/Terracotta */
|
||||
--color-primary: #D97757;
|
||||
@@ -13,15 +13,15 @@
|
||||
--color-bg: #FBF9F6;
|
||||
--color-bg-alt: #F5F1ED;
|
||||
--color-surface: #FFFFFF;
|
||||
--color-sidebar: #F0EFEC;
|
||||
--color-border: #E6E4DD;
|
||||
--color-sidebar: rgba(246, 246, 246, 0.8);
|
||||
--color-border: rgba(0, 0, 0, 0.1);
|
||||
--color-text-main: #383733;
|
||||
--color-text-muted: #75736E;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-soft: 0 2px 10px rgba(0, 0, 0, 0.03), 0 10px 25px rgba(0, 0, 0, 0.02);
|
||||
--shadow-warm: 0 4px 20px -2px rgba(217, 119, 87, 0.15);
|
||||
--shadow-elevated: 0 20px 40px -4px rgba(60, 50, 45, 0.08);
|
||||
/* Shadows - subtle macOS style */
|
||||
--shadow-soft: 0 1px 3px rgba(0, 0, 0, 0.02), 0 4px 12px rgba(0, 0, 0, 0.015);
|
||||
--shadow-warm: 0 2px 12px -2px rgba(217, 119, 87, 0.12);
|
||||
--shadow-elevated: 0 12px 28px -4px rgba(60, 50, 45, 0.06);
|
||||
}
|
||||
|
||||
.dark {
|
||||
@@ -29,15 +29,15 @@
|
||||
--color-bg: #191918;
|
||||
--color-bg-alt: #1F1F1E;
|
||||
--color-surface: #242423;
|
||||
--color-sidebar: #1F1F1E;
|
||||
--color-border: #333331;
|
||||
--color-sidebar: rgba(30, 30, 30, 0.8);
|
||||
--color-border: rgba(255, 255, 255, 0.1);
|
||||
--color-text-main: #ECEBE8;
|
||||
--color-text-muted: #9E9D99;
|
||||
|
||||
/* Dark shadows */
|
||||
--shadow-soft: 0 2px 10px rgba(0, 0, 0, 0.2), 0 10px 25px rgba(0, 0, 0, 0.15);
|
||||
--shadow-warm: 0 4px 20px -2px rgba(217, 119, 87, 0.2);
|
||||
--shadow-elevated: 0 20px 40px -4px rgba(0, 0, 0, 0.4);
|
||||
/* Dark shadows - subtle macOS style */
|
||||
--shadow-soft: 0 1px 3px rgba(0, 0, 0, 0.15), 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
--shadow-warm: 0 2px 12px -2px rgba(217, 119, 87, 0.15);
|
||||
--shadow-elevated: 0 12px 28px -4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
@@ -72,8 +72,8 @@
|
||||
--shadow-warm: var(--shadow-warm);
|
||||
--shadow-elevated: var(--shadow-elevated);
|
||||
|
||||
/* Font */
|
||||
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
||||
/* Font - macOS system fonts */
|
||||
--font-sans: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'SF Pro Display', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
/* Base styles */
|
||||
@@ -182,3 +182,38 @@ body {
|
||||
.animate-border-glow {
|
||||
animation: border-glow 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* macOS Vibrancy/Blur Effect */
|
||||
.bg-vibrancy {
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
.dark .bg-vibrancy {
|
||||
background: rgba(30, 30, 30, 0.72);
|
||||
}
|
||||
|
||||
/* macOS Traffic Lights */
|
||||
.traffic-lights {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.traffic-light {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.traffic-light.red {
|
||||
background: #FF5F56;
|
||||
}
|
||||
|
||||
.traffic-light.yellow {
|
||||
background: #FFBD2E;
|
||||
}
|
||||
|
||||
.traffic-light.green {
|
||||
background: #27C93F;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user