feat(models): add refresh button to model selector bar for 9router
This commit is contained in:
parent
e7514c08cc
commit
df6345c30a
1 changed files with 80 additions and 53 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef, useEffect } from 'react';
|
||||||
import { ChevronDown, Globe } from 'lucide-react';
|
import { ChevronDown, Globe, RefreshCw } from 'lucide-react';
|
||||||
import type { LLMProvider } from '../../../../types/app';
|
import type { LLMProvider } from '../../../../types/app';
|
||||||
import SessionProviderLogo from '../../../llm-logo-provider/SessionProviderLogo';
|
import SessionProviderLogo from '../../../llm-logo-provider/SessionProviderLogo';
|
||||||
|
|
||||||
|
|
@ -19,6 +19,7 @@ interface ModelSelectorBarProps {
|
||||||
codexModelOptions: ModelOption[];
|
codexModelOptions: ModelOption[];
|
||||||
geminiModelOptions: ModelOption[];
|
geminiModelOptions: ModelOption[];
|
||||||
cursorModelOptions: ModelOption[];
|
cursorModelOptions: ModelOption[];
|
||||||
|
onRefreshModels?: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function useCurrentModel(
|
function useCurrentModel(
|
||||||
|
|
@ -57,9 +58,11 @@ export default function ModelSelectorBar({
|
||||||
codexModelOptions,
|
codexModelOptions,
|
||||||
geminiModelOptions,
|
geminiModelOptions,
|
||||||
cursorModelOptions,
|
cursorModelOptions,
|
||||||
|
onRefreshModels,
|
||||||
}: ModelSelectorBarProps) {
|
}: ModelSelectorBarProps) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
const searchRef = useRef<HTMLInputElement>(null);
|
const searchRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
|
@ -101,6 +104,16 @@ export default function ModelSelectorBar({
|
||||||
setSearch('');
|
setSearch('');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
if (!onRefreshModels || isRefreshing) return;
|
||||||
|
setIsRefreshing(true);
|
||||||
|
try {
|
||||||
|
await onRefreshModels();
|
||||||
|
} finally {
|
||||||
|
setIsRefreshing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-40 flex-shrink-0 flex items-center justify-between px-4 py-1.5 border-b border-border/40 bg-muted/20 backdrop-blur-sm">
|
<div className="relative z-40 flex-shrink-0 flex items-center justify-between px-4 py-1.5 border-b border-border/40 bg-muted/20 backdrop-blur-sm">
|
||||||
{/* Left: provider label */}
|
{/* Left: provider label */}
|
||||||
|
|
@ -109,59 +122,73 @@ export default function ModelSelectorBar({
|
||||||
<span className="capitalize">{provider}</span>
|
<span className="capitalize">{provider}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right: model picker */}
|
{/* Right: model picker + refresh */}
|
||||||
<div className="relative" ref={dropdownRef}>
|
<div className="flex items-center gap-1.5">
|
||||||
<button
|
{provider === 'claude' && onRefreshModels && (
|
||||||
type="button"
|
<button
|
||||||
onClick={() => setOpen((o) => !o)}
|
type="button"
|
||||||
className="flex items-center gap-1.5 rounded-md border border-border/50 bg-background/60 px-2.5 py-1 text-xs text-foreground hover:bg-accent hover:border-border transition-all"
|
onClick={handleRefresh}
|
||||||
>
|
disabled={isRefreshing}
|
||||||
<Globe className="h-3 w-3 text-muted-foreground" />
|
title="Refresh model list from 9router"
|
||||||
<span className="max-w-[180px] truncate">{label}</span>
|
className="rounded-md border border-border/50 bg-background/60 p-1 text-muted-foreground hover:bg-accent hover:text-foreground hover:border-border transition-all disabled:opacity-50"
|
||||||
<ChevronDown className={`h-3 w-3 text-muted-foreground transition-transform duration-150 ${open ? 'rotate-180' : ''}`} />
|
>
|
||||||
</button>
|
<RefreshCw className={`h-3 w-3 ${isRefreshing ? 'animate-spin' : ''}`} />
|
||||||
|
</button>
|
||||||
{open && (
|
|
||||||
<div className="absolute right-0 top-full mt-1.5 z-[100] w-64 rounded-xl border border-border/60 bg-card shadow-2xl overflow-hidden">
|
|
||||||
<div className="p-2 border-b border-border/40">
|
|
||||||
<input
|
|
||||||
ref={searchRef}
|
|
||||||
type="text"
|
|
||||||
value={search}
|
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
placeholder="Search models..."
|
|
||||||
className="w-full rounded-lg bg-muted/50 px-2.5 py-1.5 text-xs text-foreground placeholder:text-muted-foreground/50 outline-none border border-border/40 focus:border-border"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="max-h-64 overflow-y-auto py-1">
|
|
||||||
{filtered.length === 0 ? (
|
|
||||||
<p className="px-3 py-2 text-xs text-muted-foreground">No models found</p>
|
|
||||||
) : (
|
|
||||||
filtered.map((opt) => (
|
|
||||||
<button
|
|
||||||
key={opt.value}
|
|
||||||
type="button"
|
|
||||||
onMouseDown={(e) => {
|
|
||||||
// Fire on mousedown so selection wins before any blur/outside-click handler
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
handleSelect(opt.value);
|
|
||||||
}}
|
|
||||||
className={`w-full text-left px-3 py-1.5 text-xs transition-colors hover:bg-accent/60 cursor-pointer ${
|
|
||||||
opt.value === value ? 'text-primary font-medium' : 'text-foreground'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span className="truncate block">{opt.label}</span>
|
|
||||||
{opt.value === value && (
|
|
||||||
<span className="text-[10px] text-muted-foreground block">{opt.value}</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className="relative" ref={dropdownRef}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpen((o) => !o)}
|
||||||
|
className="flex items-center gap-1.5 rounded-md border border-border/50 bg-background/60 px-2.5 py-1 text-xs text-foreground hover:bg-accent hover:border-border transition-all"
|
||||||
|
>
|
||||||
|
<Globe className="h-3 w-3 text-muted-foreground" />
|
||||||
|
<span className="max-w-[180px] truncate">{label}</span>
|
||||||
|
<ChevronDown className={`h-3 w-3 text-muted-foreground transition-transform duration-150 ${open ? 'rotate-180' : ''}`} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{open && (
|
||||||
|
<div className="absolute right-0 top-full mt-1.5 z-[100] w-64 rounded-xl border border-border/60 bg-card shadow-2xl overflow-hidden">
|
||||||
|
<div className="p-2 border-b border-border/40">
|
||||||
|
<input
|
||||||
|
ref={searchRef}
|
||||||
|
type="text"
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
placeholder="Search models..."
|
||||||
|
className="w-full rounded-lg bg-muted/50 px-2.5 py-1.5 text-xs text-foreground placeholder:text-muted-foreground/50 outline-none border border-border/40 focus:border-border"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="max-h-64 overflow-y-auto py-1">
|
||||||
|
{filtered.length === 0 ? (
|
||||||
|
<p className="px-3 py-2 text-xs text-muted-foreground">No models found</p>
|
||||||
|
) : (
|
||||||
|
filtered.map((opt) => (
|
||||||
|
<button
|
||||||
|
key={opt.value}
|
||||||
|
type="button"
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
// Fire on mousedown so selection wins before any blur/outside-click handler
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
handleSelect(opt.value);
|
||||||
|
}}
|
||||||
|
className={`w-full text-left px-3 py-1.5 text-xs transition-colors hover:bg-accent/60 cursor-pointer ${
|
||||||
|
opt.value === value ? 'text-primary font-medium' : 'text-foreground'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="truncate block">{opt.label}</span>
|
||||||
|
{opt.value === value && (
|
||||||
|
<span className="text-[10px] text-muted-foreground block">{opt.value}</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue