mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-31 19:45:15 +02:00
feat: enhance model selector UI abd added github models icon
This commit is contained in:
parent
8fd7664f8f
commit
292fcb1a2c
4 changed files with 118 additions and 50 deletions
1
surfsense_web/components/icons/providers/github.svg
Normal file
1
surfsense_web/components/icons/providers/github.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Github</title><path d="M12 0c6.63 0 12 5.276 12 11.79-.001 5.067-3.29 9.567-8.175 11.187-.6.118-.825-.25-.825-.56 0-.398.015-1.665.015-3.242 0-1.105-.375-1.813-.81-2.181 2.67-.295 5.475-1.297 5.475-5.822 0-1.297-.465-2.344-1.23-3.169.12-.295.54-1.503-.12-3.125 0 0-1.005-.324-3.3 1.209a11.32 11.32 0 00-3-.398c-1.02 0-2.04.133-3 .398-2.295-1.518-3.3-1.209-3.3-1.209-.66 1.622-.24 2.83-.12 3.125-.765.825-1.23 1.887-1.23 3.169 0 4.51 2.79 5.527 5.46 5.822-.345.294-.66.81-.765 1.577-.69.31-2.415.81-3.495-.973-.225-.354-.9-1.223-1.845-1.209-1.005.015-.405.56.015.781.51.28 1.095 1.327 1.23 1.666.24.663 1.02 1.93 4.035 1.385 0 .988.015 1.916.015 2.196 0 .31-.225.664-.825.56C3.303 21.374-.003 16.867 0 11.791 0 5.276 5.37 0 12 0z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 907 B |
|
|
@ -10,6 +10,7 @@ export { default as DeepInfraIcon } from "./deepinfra.svg";
|
||||||
export { default as DeepSeekIcon } from "./deepseek.svg";
|
export { default as DeepSeekIcon } from "./deepseek.svg";
|
||||||
export { default as FireworksAiIcon } from "./fireworksai.svg";
|
export { default as FireworksAiIcon } from "./fireworksai.svg";
|
||||||
export { default as GeminiIcon } from "./gemini.svg";
|
export { default as GeminiIcon } from "./gemini.svg";
|
||||||
|
export { default as GitHubModelsIcon } from "./github.svg";
|
||||||
export { default as GroqIcon } from "./groq.svg";
|
export { default as GroqIcon } from "./groq.svg";
|
||||||
export { default as HuggingFaceIcon } from "./huggingface.svg";
|
export { default as HuggingFaceIcon } from "./huggingface.svg";
|
||||||
export { default as MiniMaxIcon } from "./minimax.svg";
|
export { default as MiniMaxIcon } from "./minimax.svg";
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,12 @@ import {
|
||||||
Bot,
|
Bot,
|
||||||
Check,
|
Check,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
ChevronUp,
|
||||||
Edit3,
|
Edit3,
|
||||||
Eye,
|
|
||||||
ImageIcon,
|
ImageIcon,
|
||||||
|
ScanEye,
|
||||||
Layers,
|
Layers,
|
||||||
Plus,
|
Plus,
|
||||||
Search,
|
Search,
|
||||||
|
|
@ -69,6 +72,7 @@ const PROVIDER_NAMES: Record<string, string> = {
|
||||||
DEEPSEEK: "DeepSeek",
|
DEEPSEEK: "DeepSeek",
|
||||||
MISTRAL: "Mistral",
|
MISTRAL: "Mistral",
|
||||||
COHERE: "Cohere",
|
COHERE: "Cohere",
|
||||||
|
GITHUB_MODELS: "GitHub Models",
|
||||||
GROQ: "Groq",
|
GROQ: "Groq",
|
||||||
OLLAMA: "Ollama",
|
OLLAMA: "Ollama",
|
||||||
TOGETHER_AI: "Together AI",
|
TOGETHER_AI: "Together AI",
|
||||||
|
|
@ -274,17 +278,40 @@ export function ModelSelector({
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [selectedProvider, setSelectedProvider] = useState<string>("all");
|
const [selectedProvider, setSelectedProvider] = useState<string>("all");
|
||||||
const [focusedIndex, setFocusedIndex] = useState(-1);
|
const [focusedIndex, setFocusedIndex] = useState(-1);
|
||||||
const [showScrollIndicator, setShowScrollIndicator] = useState(true);
|
const [modelScrollPos, setModelScrollPos] = useState<"top" | "middle" | "bottom">("top");
|
||||||
|
const [sidebarScrollPos, setSidebarScrollPos] = useState<"top" | "middle" | "bottom">("top");
|
||||||
const providerSidebarRef = useRef<HTMLDivElement>(null);
|
const providerSidebarRef = useRef<HTMLDivElement>(null);
|
||||||
const modelListRef = useRef<HTMLDivElement>(null);
|
const modelListRef = useRef<HTMLDivElement>(null);
|
||||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
const handleModelListScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
|
||||||
|
const el = e.currentTarget;
|
||||||
|
const atTop = el.scrollTop <= 2;
|
||||||
|
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight <= 2;
|
||||||
|
setModelScrollPos(atTop ? "top" : atBottom ? "bottom" : "middle");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSidebarScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
|
||||||
|
const el = e.currentTarget;
|
||||||
|
if (isMobile) {
|
||||||
|
const atStart = el.scrollLeft <= 2;
|
||||||
|
const atEnd = el.scrollWidth - el.scrollLeft - el.clientWidth <= 2;
|
||||||
|
setSidebarScrollPos(atStart ? "top" : atEnd ? "bottom" : "middle");
|
||||||
|
} else {
|
||||||
|
const atTop = el.scrollTop <= 2;
|
||||||
|
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight <= 2;
|
||||||
|
setSidebarScrollPos(atTop ? "top" : atBottom ? "bottom" : "middle");
|
||||||
|
}
|
||||||
|
}, [isMobile]);
|
||||||
|
|
||||||
// Reset search + provider when tab changes
|
// Reset search + provider when tab changes
|
||||||
|
// biome-ignore lint/correctness/useExhaustiveDependencies: activeTab is intentionally used as a trigger
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedProvider("all");
|
setSelectedProvider("all");
|
||||||
setSearchQuery("");
|
setSearchQuery("");
|
||||||
setFocusedIndex(-1);
|
setFocusedIndex(-1);
|
||||||
|
setModelScrollPos("top");
|
||||||
}, [activeTab]);
|
}, [activeTab]);
|
||||||
|
|
||||||
// Reset on open
|
// Reset on open
|
||||||
|
|
@ -295,8 +322,9 @@ export function ModelSelector({
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
// Cmd/Ctrl+M shortcut
|
// Cmd/Ctrl+M shortcut (desktop only)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isMobile) return;
|
||||||
const handler = (e: KeyboardEvent) => {
|
const handler = (e: KeyboardEvent) => {
|
||||||
if ((e.metaKey || e.ctrlKey) && e.key === "m") {
|
if ((e.metaKey || e.ctrlKey) && e.key === "m") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -305,9 +333,10 @@ export function ModelSelector({
|
||||||
};
|
};
|
||||||
document.addEventListener("keydown", handler);
|
document.addEventListener("keydown", handler);
|
||||||
return () => document.removeEventListener("keydown", handler);
|
return () => document.removeEventListener("keydown", handler);
|
||||||
}, []);
|
}, [isMobile]);
|
||||||
|
|
||||||
// Focus search input on open
|
// Focus search input on open
|
||||||
|
// biome-ignore lint/correctness/useExhaustiveDependencies: activeTab is intentionally used as a trigger to re-focus on tab switch
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open && !isMobile) {
|
if (open && !isMobile) {
|
||||||
requestAnimationFrame(() => searchInputRef.current?.focus());
|
requestAnimationFrame(() => searchInputRef.current?.focus());
|
||||||
|
|
@ -677,6 +706,7 @@ export function ModelSelector({
|
||||||
);
|
);
|
||||||
|
|
||||||
// ─── Keyboard navigation ───
|
// ─── Keyboard navigation ───
|
||||||
|
// biome-ignore lint/correctness/useExhaustiveDependencies: searchQuery and selectedProvider are intentional triggers to reset focus
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFocusedIndex(-1);
|
setFocusedIndex(-1);
|
||||||
}, [searchQuery, selectedProvider]);
|
}, [searchQuery, selectedProvider]);
|
||||||
|
|
@ -767,24 +797,35 @@ export function ModelSelector({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"shrink-0 border-border/50 relative flex flex-col",
|
"shrink-0 border-border/50 flex",
|
||||||
!isMobile && "w-10 border-r",
|
isMobile ? "flex-row items-center border-b border-border/40" : "flex-col w-10 border-r",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{!isMobile && sidebarScrollPos !== "top" && (
|
||||||
|
<div className="flex items-center justify-center py-0.5 pointer-events-none">
|
||||||
|
<ChevronUp className="size-3 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isMobile && sidebarScrollPos !== "top" && (
|
||||||
|
<div className="flex items-center justify-center px-0.5 shrink-0 pointer-events-none">
|
||||||
|
<ChevronLeft className="size-3 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
ref={providerSidebarRef}
|
ref={providerSidebarRef}
|
||||||
onScroll={(e) => {
|
onScroll={handleSidebarScroll}
|
||||||
const t = e.currentTarget;
|
|
||||||
setShowScrollIndicator(
|
|
||||||
t.scrollHeight - t.scrollTop >
|
|
||||||
t.clientHeight + 10,
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
isMobile
|
isMobile
|
||||||
? "flex flex-row gap-0.5 px-2 py-1.5 overflow-x-auto border-b border-border/40"
|
? "flex flex-row gap-0.5 px-1 py-1.5 overflow-x-auto [&::-webkit-scrollbar]:h-0 [&::-webkit-scrollbar-track]:bg-transparent"
|
||||||
: "flex flex-col gap-0.5 p-1 overflow-y-auto flex-1 [&::-webkit-scrollbar]:w-0 [&::-webkit-scrollbar-track]:bg-transparent",
|
: "flex flex-col gap-0.5 p-1 overflow-y-auto flex-1 [&::-webkit-scrollbar]:w-0 [&::-webkit-scrollbar-track]:bg-transparent",
|
||||||
)}
|
)}
|
||||||
|
style={isMobile ? {
|
||||||
|
maskImage: `linear-gradient(to right, ${sidebarScrollPos === "top" ? "black" : "transparent"}, black 24px, black calc(100% - 24px), ${sidebarScrollPos === "bottom" ? "black" : "transparent"})`,
|
||||||
|
WebkitMaskImage: `linear-gradient(to right, ${sidebarScrollPos === "top" ? "black" : "transparent"}, black 24px, black calc(100% - 24px), ${sidebarScrollPos === "bottom" ? "black" : "transparent"})`,
|
||||||
|
} : {
|
||||||
|
maskImage: `linear-gradient(to bottom, ${sidebarScrollPos === "top" ? "black" : "transparent"}, black 32px, black calc(100% - 32px), ${sidebarScrollPos === "bottom" ? "black" : "transparent"})`,
|
||||||
|
WebkitMaskImage: `linear-gradient(to bottom, ${sidebarScrollPos === "top" ? "black" : "transparent"}, black 32px, black calc(100% - 32px), ${sidebarScrollPos === "bottom" ? "black" : "transparent"})`,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{activeProviders.map((provider, idx) => {
|
{activeProviders.map((provider, idx) => {
|
||||||
const isAll = provider === "all";
|
const isAll = provider === "all";
|
||||||
|
|
@ -849,18 +890,23 @@ export function ModelSelector({
|
||||||
)}
|
)}
|
||||||
{isConfigured
|
{isConfigured
|
||||||
? ` (${count})`
|
? ` (${count})`
|
||||||
: " — not configured"}
|
: " (not configured)"}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
{!isMobile && showScrollIndicator && (
|
{!isMobile && sidebarScrollPos !== "bottom" && (
|
||||||
<div className="absolute bottom-0 left-0 right-0 h-6 bg-gradient-to-t from-background to-transparent pointer-events-none flex items-end justify-center pb-0.5">
|
<div className="flex items-center justify-center py-0.5 pointer-events-none">
|
||||||
<ChevronDown className="size-3 text-muted-foreground" />
|
<ChevronDown className="size-3 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{isMobile && sidebarScrollPos !== "bottom" && (
|
||||||
|
<div className="flex items-center justify-center px-0.5 shrink-0 pointer-events-none">
|
||||||
|
<ChevronRight className="size-3 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -889,19 +935,26 @@ export function ModelSelector({
|
||||||
key={`${activeTab}-${item.isGlobal ? "g" : "u"}-${config.id}`}
|
key={`${activeTab}-${item.isGlobal ? "g" : "u"}-${config.id}`}
|
||||||
data-model-index={index}
|
data-model-index={index}
|
||||||
role="option"
|
role="option"
|
||||||
|
tabIndex={isMobile ? -1 : 0}
|
||||||
aria-selected={isSelected}
|
aria-selected={isSelected}
|
||||||
onClick={() => handleSelectItem(item)}
|
onClick={() => handleSelectItem(item)}
|
||||||
|
onKeyDown={isMobile ? undefined : (e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSelectItem(item);
|
||||||
|
}
|
||||||
|
}}
|
||||||
onMouseEnter={() => setFocusedIndex(index)}
|
onMouseEnter={() => setFocusedIndex(index)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group flex items-start gap-2.5 px-2.5 py-2 rounded-lg cursor-pointer",
|
"group flex items-center gap-2.5 px-3 py-2 rounded-xl cursor-pointer",
|
||||||
"transition-all duration-150 mx-1",
|
"transition-all duration-150 mx-2",
|
||||||
"hover:bg-accent/40 active:scale-[0.99]",
|
"hover:bg-accent/40",
|
||||||
isSelected && "bg-primary/6 dark:bg-primary/8",
|
isSelected && "bg-primary/6 dark:bg-primary/8",
|
||||||
isFocused && "bg-accent/50 ring-1 ring-primary/20",
|
isFocused && "bg-accent/50",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Provider icon */}
|
{/* Provider icon */}
|
||||||
<div className="shrink-0 mt-0.5">
|
<div className="shrink-0">
|
||||||
{getProviderIcon(config.provider as string, {
|
{getProviderIcon(config.provider as string, {
|
||||||
isAutoMode,
|
isAutoMode,
|
||||||
className: "size-5",
|
className: "size-5",
|
||||||
|
|
@ -931,8 +984,8 @@ export function ModelSelector({
|
||||||
</span>
|
</span>
|
||||||
{!isAutoMode && hasCitations && (
|
{!isAutoMode && hasCitations && (
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="secondary"
|
||||||
className="text-[9px] px-1 py-0 h-3.5 bg-primary/10 text-primary border-primary/20"
|
className="text-[10px] px-1.5 py-0.5 border-0 text-muted-foreground bg-muted"
|
||||||
>
|
>
|
||||||
Citations
|
Citations
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
@ -981,7 +1034,7 @@ export function ModelSelector({
|
||||||
: "Add Vision Model";
|
: "Add Vision Model";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-full">
|
<div className="flex flex-col w-full overflow-hidden">
|
||||||
{/* Tab header */}
|
{/* Tab header */}
|
||||||
<div className="border-b border-border/80 dark:border-neutral-800">
|
<div className="border-b border-border/80 dark:border-neutral-800">
|
||||||
<div className="w-full grid grid-cols-3 h-11">
|
<div className="w-full grid grid-cols-3 h-11">
|
||||||
|
|
@ -999,7 +1052,7 @@ export function ModelSelector({
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "vision" as const,
|
value: "vision" as const,
|
||||||
icon: Eye,
|
icon: ScanEye,
|
||||||
label: "Vision",
|
label: "Vision",
|
||||||
},
|
},
|
||||||
] as const
|
] as const
|
||||||
|
|
@ -1028,7 +1081,7 @@ export function ModelSelector({
|
||||||
"flex",
|
"flex",
|
||||||
isMobile
|
isMobile
|
||||||
? "flex-col h-[60vh]"
|
? "flex-col h-[60vh]"
|
||||||
: "flex-row h-[420px]",
|
: "flex-row h-[380px]",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Provider sidebar */}
|
{/* Provider sidebar */}
|
||||||
|
|
@ -1037,33 +1090,30 @@ export function ModelSelector({
|
||||||
{/* Main content */}
|
{/* Main content */}
|
||||||
<div className="flex flex-col min-w-0 min-h-0 flex-1 overflow-hidden">
|
<div className="flex flex-col min-w-0 min-h-0 flex-1 overflow-hidden">
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<div className="relative px-3 py-2">
|
<div className="relative">
|
||||||
<Search className="absolute left-5 top-1/2 -translate-y-1/2 size-3.5 text-muted-foreground/50 pointer-events-none" />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 size-3.5 text-muted-foreground/100 pointer-events-none" />
|
||||||
<input
|
<input
|
||||||
ref={searchInputRef}
|
ref={searchInputRef}
|
||||||
placeholder="Search models..."
|
placeholder="Search models"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setSearchQuery(e.target.value)
|
setSearchQuery(e.target.value)
|
||||||
}
|
}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={isMobile ? undefined : handleKeyDown}
|
||||||
autoFocus={!isMobile}
|
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={true}
|
aria-expanded={true}
|
||||||
aria-controls="model-selector-list"
|
aria-controls="model-selector-list"
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full pl-8 pr-3 py-1.5 text-xs rounded-lg",
|
"w-full pl-8 pr-3 py-2.5 text-sm bg-transparent",
|
||||||
"bg-secondary/30 border border-border/40",
|
"focus:outline-none",
|
||||||
"focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary/40",
|
"placeholder:text-muted-foreground",
|
||||||
"placeholder:text-muted-foreground/50",
|
|
||||||
"transition-[box-shadow,border-color] duration-200",
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Provider header when filtered */}
|
{/* Provider header when filtered */}
|
||||||
{selectedProvider !== "all" && (
|
{selectedProvider !== "all" && (
|
||||||
<div className="flex items-center gap-2 px-3 py-1.5 border-b border-border/40">
|
<div className="flex items-center gap-2 px-3 py-1.5">
|
||||||
{getProviderIcon(selectedProvider, {
|
{getProviderIcon(selectedProvider, {
|
||||||
className: "size-4",
|
className: "size-4",
|
||||||
})}
|
})}
|
||||||
|
|
@ -1085,10 +1135,15 @@ export function ModelSelector({
|
||||||
id="model-selector-list"
|
id="model-selector-list"
|
||||||
ref={modelListRef}
|
ref={modelListRef}
|
||||||
role="listbox"
|
role="listbox"
|
||||||
className="overflow-y-auto flex-1 py-1"
|
className="overflow-y-auto flex-1 py-1 space-y-1 flex flex-col"
|
||||||
|
onScroll={handleModelListScroll}
|
||||||
|
style={{
|
||||||
|
maskImage: `linear-gradient(to bottom, ${modelScrollPos === "top" ? "black" : "transparent"}, black 16px, black calc(100% - 16px), ${modelScrollPos === "bottom" ? "black" : "transparent"})`,
|
||||||
|
WebkitMaskImage: `linear-gradient(to bottom, ${modelScrollPos === "top" ? "black" : "transparent"}, black 16px, black calc(100% - 16px), ${modelScrollPos === "bottom" ? "black" : "transparent"})`,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{currentDisplayItems.length === 0 ? (
|
{currentDisplayItems.length === 0 ? (
|
||||||
<div className="py-8 flex flex-col items-center gap-3 px-4">
|
<div className="flex-1 flex flex-col items-center justify-center gap-3 px-4">
|
||||||
{selectedProvider !== "all" &&
|
{selectedProvider !== "all" &&
|
||||||
!configuredProviderSet.has(
|
!configuredProviderSet.has(
|
||||||
selectedProvider,
|
selectedProvider,
|
||||||
|
|
@ -1116,22 +1171,21 @@ export function ModelSelector({
|
||||||
</p>
|
</p>
|
||||||
{addHandler && (
|
{addHandler && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="mt-1 gap-2"
|
className="mt-1"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
addHandler(selectedProvider !== "all" ? selectedProvider : undefined);
|
addHandler(selectedProvider !== "all" ? selectedProvider : undefined);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Plus className="size-3.5" />
|
|
||||||
{addLabel}
|
{addLabel}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : searchQuery ? (
|
||||||
<>
|
<>
|
||||||
<Search className="size-8 text-muted-foreground/40" />
|
<Search className="size-8 text-muted-foreground" />
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
No models found
|
No models found
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -1140,13 +1194,22 @@ export function ModelSelector({
|
||||||
term
|
term
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p className="text-sm font-medium text-muted-foreground">
|
||||||
|
No models configured
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground/60 text-center">
|
||||||
|
Configure models in your search space settings
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{globalItems.length > 0 && (
|
{globalItems.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-2 px-3 py-1.5 text-[10px] font-semibold text-muted-foreground/70 uppercase tracking-wider">
|
<div className="flex items-center gap-2 px-3 py-1.5 text-[12px] font-semibold text-muted-foreground tracking-wider">
|
||||||
Global Models
|
Global Models
|
||||||
</div>
|
</div>
|
||||||
{globalItems.map((item, i) =>
|
{globalItems.map((item, i) =>
|
||||||
|
|
@ -1163,7 +1226,7 @@ export function ModelSelector({
|
||||||
)}
|
)}
|
||||||
{userItems.length > 0 && (
|
{userItems.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-2 px-3 py-1.5 text-[10px] font-semibold text-muted-foreground/70 uppercase tracking-wider">
|
<div className="flex items-center gap-2 px-3 py-1.5 text-[12px] font-semibold text-muted-foreground tracking-wider">
|
||||||
Your Configurations
|
Your Configurations
|
||||||
</div>
|
</div>
|
||||||
{userItems.map((item, i) =>
|
{userItems.map((item, i) =>
|
||||||
|
|
@ -1180,7 +1243,7 @@ export function ModelSelector({
|
||||||
|
|
||||||
{/* Add model button */}
|
{/* Add model button */}
|
||||||
{addHandler && (
|
{addHandler && (
|
||||||
<div className="p-2 border-t border-border/40 bg-muted/20 dark:bg-neutral-900">
|
<div className="p-2">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -1271,7 +1334,7 @@ export function ModelSelector({
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Eye className="size-4 text-muted-foreground" />
|
<ScanEye className="size-4 text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1301,7 +1364,7 @@ export function ModelSelector({
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
<PopoverTrigger asChild>{triggerButton}</PopoverTrigger>
|
<PopoverTrigger asChild>{triggerButton}</PopoverTrigger>
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
className="w-[340px] md:w-[440px] p-0 rounded-lg shadow-lg bg-white border-border/60 dark:bg-neutral-900 dark:border dark:border-white/5 select-none"
|
className="w-[300px] md:w-[380px] p-0 rounded-lg shadow-lg overflow-hidden bg-white border-border/60 dark:bg-neutral-900 dark:border dark:border-white/5 select-none"
|
||||||
align="start"
|
align="start"
|
||||||
sideOffset={8}
|
sideOffset={8}
|
||||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
DeepSeekIcon,
|
DeepSeekIcon,
|
||||||
FireworksAiIcon,
|
FireworksAiIcon,
|
||||||
GeminiIcon,
|
GeminiIcon,
|
||||||
|
GitHubModelsIcon,
|
||||||
GroqIcon,
|
GroqIcon,
|
||||||
HuggingFaceIcon,
|
HuggingFaceIcon,
|
||||||
MiniMaxIcon,
|
MiniMaxIcon,
|
||||||
|
|
@ -82,6 +83,8 @@ export function getProviderIcon(
|
||||||
return <FireworksAiIcon className={cn(className)} />;
|
return <FireworksAiIcon className={cn(className)} />;
|
||||||
case "GOOGLE":
|
case "GOOGLE":
|
||||||
return <GeminiIcon className={cn(className)} />;
|
return <GeminiIcon className={cn(className)} />;
|
||||||
|
case "GITHUB_MODELS":
|
||||||
|
return <GitHubModelsIcon className={cn(className)} />;
|
||||||
case "GROQ":
|
case "GROQ":
|
||||||
return <GroqIcon className={cn(className)} />;
|
return <GroqIcon className={cn(className)} />;
|
||||||
case "HUGGINGFACE":
|
case "HUGGINGFACE":
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue