feat(app): Allow selecting the AI model at any time without opening the settings page.

Allow changing focus mode at any time while chatting.
Styling tweaks.
This commit is contained in:
Willie Zutz
2025-05-05 00:05:19 -06:00
parent 8241c87784
commit 6220822c7c
10 changed files with 430 additions and 236 deletions

View File

@@ -19,6 +19,8 @@ const Chat = ({
setFiles,
optimizationMode,
setOptimizationMode,
focusMode,
setFocusMode,
}: {
messages: Message[];
sendMessage: (
@@ -38,13 +40,15 @@ const Chat = ({
setFiles: (files: File[]) => void;
optimizationMode: string;
setOptimizationMode: (mode: string) => void;
focusMode: string;
setFocusMode: (mode: string) => void;
}) => {
const [dividerWidth, setDividerWidth] = useState(0);
const [isAtBottom, setIsAtBottom] = useState(true);
const [manuallyScrolledUp, setManuallyScrolledUp] = useState(false);
const dividerRef = useRef<HTMLDivElement | null>(null);
const messageEnd = useRef<HTMLDivElement | null>(null);
const SCROLL_THRESHOLD = 200; // pixels from bottom to consider "at bottom"
const SCROLL_THRESHOLD = 250; // pixels from bottom to consider "at bottom"
// Check if user is at bottom of page
useEffect(() => {
@@ -146,7 +150,6 @@ const Chat = ({
const position = window.innerHeight + window.scrollY;
const height = document.body.scrollHeight;
const atBottom = position >= height - SCROLL_THRESHOLD;
console.log('scrollTrigger', scrollTrigger);
setIsAtBottom(atBottom);
if (isAtBottom && !manuallyScrolledUp && messages.length > 0) {
@@ -155,7 +158,7 @@ const Chat = ({
}, [scrollTrigger, isAtBottom, messages.length, manuallyScrolledUp]);
return (
<div className="flex flex-col space-y-6 pt-8 pb-44 lg:pb-32 sm:mx-4 md:mx-8">
<div className="flex flex-col space-y-6 pt-8 pb-48 sm:mx-4 md:mx-8">
{messages.map((msg, i) => {
const isLast = i === messages.length - 1;
@@ -217,6 +220,7 @@ const Chat = ({
)}
<MessageInput
firstMessage={messages.length === 0}
loading={loading}
sendMessage={sendMessage}
fileIds={fileIds}
@@ -225,6 +229,8 @@ const Chat = ({
setFiles={setFiles}
optimizationMode={optimizationMode}
setOptimizationMode={setOptimizationMode}
focusMode={focusMode}
setFocusMode={setFocusMode}
/>
</div>
)}

View File

@@ -531,6 +531,15 @@ const ChatWindow = ({ id }: { id?: string }) => {
const ollamaContextWindow =
localStorage.getItem('ollamaContextWindow') || '2048';
// Get the latest model selection from localStorage
const currentChatModelProvider = localStorage.getItem('chatModelProvider');
const currentChatModel = localStorage.getItem('chatModel');
// Use the most current model selection from localStorage, falling back to the state if not available
const modelProvider =
currentChatModelProvider || chatModelProvider.provider;
const modelName = currentChatModel || chatModelProvider.name;
const res = await fetch('/api/chat', {
method: 'POST',
headers: {
@@ -549,8 +558,8 @@ const ChatWindow = ({ id }: { id?: string }) => {
optimizationMode: optimizationMode,
history: messageChatHistory,
chatModel: {
name: chatModelProvider.name,
provider: chatModelProvider.provider,
name: modelName,
provider: modelProvider,
...(chatModelProvider.provider === 'ollama' && {
ollamaContextWindow: parseInt(ollamaContextWindow),
}),
@@ -645,6 +654,8 @@ const ChatWindow = ({ id }: { id?: string }) => {
setFiles={setFiles}
optimizationMode={optimizationMode}
setOptimizationMode={setOptimizationMode}
focusMode={focusMode}
setFocusMode={setFocusMode}
/>
</>
) : (

View File

@@ -1,8 +1,8 @@
import { Settings } from 'lucide-react';
import EmptyChatMessageInput from './EmptyChatMessageInput';
import { useState } from 'react';
import { File } from './ChatWindow';
import Link from 'next/link';
import MessageInput from './MessageInput';
const EmptyChat = ({
sendMessage,
@@ -38,7 +38,9 @@ const EmptyChat = ({
<h2 className="text-black/70 dark:text-white/70 text-3xl font-medium -mt-8">
Research begins here.
</h2>
<EmptyChatMessageInput
<MessageInput
firstMessage={true}
loading={false}
sendMessage={sendMessage}
focusMode={focusMode}
setFocusMode={setFocusMode}

View File

@@ -1,114 +0,0 @@
import { ArrowRight } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import TextareaAutosize from 'react-textarea-autosize';
import CopilotToggle from './MessageInputActions/Copilot';
import Focus from './MessageInputActions/Focus';
import Optimization from './MessageInputActions/Optimization';
import Attach from './MessageInputActions/Attach';
import { File } from './ChatWindow';
const EmptyChatMessageInput = ({
sendMessage,
focusMode,
setFocusMode,
optimizationMode,
setOptimizationMode,
fileIds,
setFileIds,
files,
setFiles,
}: {
sendMessage: (message: string) => void;
focusMode: string;
setFocusMode: (mode: string) => void;
optimizationMode: string;
setOptimizationMode: (mode: string) => void;
fileIds: string[];
setFileIds: (fileIds: string[]) => void;
files: File[];
setFiles: (files: File[]) => void;
}) => {
const [copilotEnabled, setCopilotEnabled] = useState(false);
const [message, setMessage] = useState('');
const inputRef = useRef<HTMLTextAreaElement | null>(null);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const activeElement = document.activeElement;
const isInputFocused =
activeElement?.tagName === 'INPUT' ||
activeElement?.tagName === 'TEXTAREA' ||
activeElement?.hasAttribute('contenteditable');
if (e.key === '/' && !isInputFocused) {
e.preventDefault();
inputRef.current?.focus();
}
};
document.addEventListener('keydown', handleKeyDown);
inputRef.current?.focus();
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, []);
return (
<form
onSubmit={(e) => {
e.preventDefault();
sendMessage(message);
setMessage('');
}}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage(message);
setMessage('');
}
}}
className="w-full"
>
<div className="flex flex-col bg-light-secondary dark:bg-dark-secondary px-5 pt-5 pb-2 rounded-lg w-full border border-light-200 dark:border-dark-200">
<TextareaAutosize
ref={inputRef}
value={message}
onChange={(e) => setMessage(e.target.value)}
minRows={2}
className="bg-transparent placeholder:text-black/50 dark:placeholder:text-white/50 text-sm text-black dark:text-white resize-none focus:outline-none w-full max-h-24 lg:max-h-36 xl:max-h-48"
placeholder="Ask anything..."
/>
<div className="flex flex-row items-center justify-between mt-4">
<div className="flex flex-row items-center space-x-2 lg:space-x-4">
<Focus focusMode={focusMode} setFocusMode={setFocusMode} />
<Attach
fileIds={fileIds}
setFileIds={setFileIds}
files={files}
setFiles={setFiles}
showText
/>
</div>
<div className="flex flex-row items-center space-x-1 sm:space-x-4">
<Optimization
optimizationMode={optimizationMode}
setOptimizationMode={setOptimizationMode}
/>
<button
disabled={message.trim().length === 0}
className="bg-[#24A0ED] text-white disabled:text-black/50 dark:disabled:text-white/50 disabled:bg-[#e0e0dc] dark:disabled:bg-[#ececec21] hover:bg-opacity-85 transition duration-100 rounded-full p-2"
>
<ArrowRight className="bg-background" size={17} />
</button>
</div>
</div>
</div>
</form>
);
};
export default EmptyChatMessageInput;

View File

@@ -1,12 +1,11 @@
import { cn } from '@/lib/utils';
import { ArrowUp } from 'lucide-react';
import { ArrowRight, ArrowUp } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import TextareaAutosize from 'react-textarea-autosize';
import Attach from './MessageInputActions/Attach';
import CopilotToggle from './MessageInputActions/Copilot';
import Optimization from './MessageInputActions/Optimization';
import { File } from './ChatWindow';
import AttachSmall from './MessageInputActions/AttachSmall';
import Attach from './MessageInputActions/Attach';
import Focus from './MessageInputActions/Focus';
import ModelSelector from './MessageInputActions/ModelSelector';
import Optimization from './MessageInputActions/Optimization';
const MessageInput = ({
sendMessage,
@@ -17,6 +16,9 @@ const MessageInput = ({
setFiles,
optimizationMode,
setOptimizationMode,
focusMode,
setFocusMode,
firstMessage,
}: {
sendMessage: (message: string) => void;
loading: boolean;
@@ -26,19 +28,28 @@ const MessageInput = ({
setFiles: (files: File[]) => void;
optimizationMode: string;
setOptimizationMode: (mode: string) => void;
focusMode: string;
setFocusMode: (mode: string) => void;
firstMessage: boolean;
}) => {
const [copilotEnabled, setCopilotEnabled] = useState(false);
const [message, setMessage] = useState('');
const [textareaRows, setTextareaRows] = useState(1);
const [mode, setMode] = useState<'multi' | 'single'>('single');
const [selectedModel, setSelectedModel] = useState<{
provider: string;
model: string;
} | null>(null);
useEffect(() => {
if (textareaRows >= 2 && message && mode === 'single') {
setMode('multi');
} else if (!message && mode === 'multi') {
setMode('single');
// Load saved model preferences from localStorage
const chatModelProvider = localStorage.getItem('chatModelProvider');
const chatModel = localStorage.getItem('chatModel');
if (chatModelProvider && chatModel) {
setSelectedModel({
provider: chatModelProvider,
model: chatModel,
});
}
}, [textareaRows, mode, message]);
}, []);
const inputRef = useRef<HTMLTextAreaElement | null>(null);
@@ -60,117 +71,74 @@ const MessageInput = ({
};
}, []);
return (
// Function to handle message submission
const handleSubmitMessage = () => {
// Only submit if we have a non-empty message and not currently loading
if (loading || message.trim().length === 0) return;
// Make sure the selected model is used when sending a message
if (selectedModel) {
localStorage.setItem('chatModelProvider', selectedModel.provider);
localStorage.setItem('chatModel', selectedModel.model);
}
sendMessage(message);
setMessage('');
};
return (
<form
onSubmit={(e) => {
if (loading) return;
e.preventDefault();
sendMessage(message);
setMessage('');
handleSubmitMessage();
}}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey && !loading) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage(message);
setMessage('');
handleSubmitMessage();
}
}}
className={cn(
'bg-light-secondary dark:bg-dark-secondary p-4 flex items-center border border-light-200 dark:border-dark-200',
mode === 'multi'
? 'flex-col rounded-lg'
: 'flex-col md:flex-row rounded-lg md:rounded-full',
)}
className="w-full"
>
{mode === 'single' && (
<div className="flex flex-row items-center justify-between w-full mb-2 md:mb-0 md:w-auto">
<div className="flex flex-row items-center space-x-2">
<AttachSmall
fileIds={fileIds}
setFileIds={setFileIds}
files={files}
setFiles={setFiles}
/>
<Optimization
optimizationMode={optimizationMode}
setOptimizationMode={setOptimizationMode}
/>
</div>
<div className="md:hidden">
<CopilotToggle
copilotEnabled={copilotEnabled}
setCopilotEnabled={setCopilotEnabled}
/>
</div>
</div>
)}
<div className="flex flex-row items-center w-full">
<div className="flex flex-col bg-light-secondary dark:bg-dark-secondary px-5 pt-5 pb-2 rounded-lg w-full border border-light-200 dark:border-dark-200">
<TextareaAutosize
ref={inputRef}
value={message}
onChange={(e) => setMessage(e.target.value)}
onHeightChange={(height, props) => {
setTextareaRows(Math.ceil(height / props.rowHeight));
}}
className="transition bg-transparent dark:placeholder:text-white/50 placeholder:text-sm text-sm dark:text-white resize-none focus:outline-none w-full px-2 max-h-24 lg:max-h-36 xl:max-h-48 flex-grow flex-shrink"
placeholder="Ask a follow-up"
minRows={2}
className="bg-transparent placeholder:text-black/50 dark:placeholder:text-white/50 text-sm text-black dark:text-white resize-none focus:outline-none w-full max-h-24 lg:max-h-36 xl:max-h-48"
placeholder={firstMessage ? "Ask anything..." :"Ask a follow-up"}
/>
{mode === 'single' && (
<div className="flex flex-row items-center space-x-4">
<div className="hidden md:block">
<CopilotToggle
copilotEnabled={copilotEnabled}
setCopilotEnabled={setCopilotEnabled}
/>
</div>
<button
disabled={message.trim().length === 0 || loading}
className="bg-[#24A0ED] text-white disabled:text-black/50 dark:disabled:text-white/50 hover:bg-opacity-85 transition duration-100 disabled:bg-[#e0e0dc79] dark:disabled:bg-[#ececec21] rounded-full p-2"
>
<ArrowUp className="bg-background" size={17} />
</button>
<div className="flex flex-row items-center justify-between mt-4">
<div className="flex flex-row items-center space-x-2 lg:space-x-4">
<Focus focusMode={focusMode} setFocusMode={setFocusMode} />
<Attach
fileIds={fileIds}
setFileIds={setFileIds}
files={files}
setFiles={setFiles}
showText={firstMessage}
/>
<ModelSelector
selectedModel={selectedModel}
setSelectedModel={setSelectedModel}
/>
</div>
)}
</div>
{mode === 'multi' && (
<div className="flex flex-col md:flex-row items-start md:items-center justify-between w-full pt-2">
<div className="flex flex-row items-center justify-between w-full md:w-auto mb-2 md:mb-0">
<div className="flex flex-row items-center space-x-2">
<AttachSmall
fileIds={fileIds}
setFileIds={setFileIds}
files={files}
setFiles={setFiles}
/>
<Optimization
optimizationMode={optimizationMode}
setOptimizationMode={setOptimizationMode}
/>
</div>
<div className="md:hidden">
<CopilotToggle
copilotEnabled={copilotEnabled}
setCopilotEnabled={setCopilotEnabled}
/>
</div>
</div>
<div className="flex flex-row items-center space-x-4 self-end">
<div className="hidden md:block">
<CopilotToggle
copilotEnabled={copilotEnabled}
setCopilotEnabled={setCopilotEnabled}
/>
</div>
<div className="flex flex-row items-center space-x-1 sm:space-x-4">
<Optimization
optimizationMode={optimizationMode}
setOptimizationMode={setOptimizationMode}
/>
<button
disabled={message.trim().length === 0 || loading}
className="bg-[#24A0ED] text-white disabled:text-black/50 dark:disabled:text-white/50 hover:bg-opacity-85 transition duration-100 disabled:bg-[#e0e0dc79] dark:disabled:bg-[#ececec21] rounded-full p-2"
disabled={message.trim().length === 0}
className="bg-[#24A0ED] text-white disabled:text-black/50 dark:disabled:text-white/50 disabled:bg-[#e0e0dc] dark:disabled:bg-[#ececec21] hover:bg-opacity-85 transition duration-100 rounded-full p-2"
type="submit"
>
<ArrowUp className="bg-background" size={17} />
{firstMessage ? <ArrowRight className="bg-background" size={17} /> : <ArrowUp className="bg-background" size={17} />}
</button>
</div>
</div>
)}
</div>
</form>
);
};

View File

@@ -5,7 +5,7 @@ import {
PopoverPanel,
Transition,
} from '@headlessui/react';
import { CopyPlus, File, LoaderCircle, Plus, Trash } from 'lucide-react';
import { File, LoaderCircle, Paperclip, Plus, Trash } from 'lucide-react';
import { Fragment, useRef, useState } from 'react';
import { File as FileType } from '../ChatWindow';
@@ -176,8 +176,10 @@ const Attach = ({
multiple
hidden
/>
<CopyPlus size={showText ? 18 : undefined} />
{showText && <p className="text-xs font-medium pl-[1px]">Attach</p>}
<Paperclip size="18" />
{showText && (
<p className="text-xs font-medium pl-[1px] hidden lg:block">Attach</p>
)}
</button>
);
};

View File

@@ -93,13 +93,13 @@ const Focus = ({
<Transition
as={Fragment}
enter="transition ease-out duration-150"
enterFrom="opacity-0 translate-y-1"
enterFrom="opacity-0 -translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
leaveTo="opacity-0 -translate-y-1"
>
<PopoverPanel className="absolute z-10 w-64 md:w-[500px] left-0">
<PopoverPanel className="absolute z-10 w-64 md:w-[500px] left-0 bottom-full mb-2">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 w-full p-4 max-h-[200px] md:max-h-none overflow-y-auto">
{focusModes.map((mode, i) => (
<PopoverButton

View File

@@ -0,0 +1,305 @@
import { useEffect, useState } from 'react';
import { Cpu, ChevronDown, ChevronRight } from 'lucide-react';
import { cn } from '@/lib/utils';
import {
Popover,
PopoverButton,
PopoverPanel,
Transition,
} from '@headlessui/react';
import { Fragment } from 'react';
interface ModelOption {
provider: string;
model: string;
displayName: string;
}
interface ProviderModelMap {
[provider: string]: {
displayName: string;
models: ModelOption[];
};
}
const ModelSelector = ({
selectedModel,
setSelectedModel,
}: {
selectedModel: { provider: string; model: string } | null;
setSelectedModel: (model: { provider: string; model: string }) => void;
}) => {
const [providerModels, setProviderModels] = useState<ProviderModelMap>({});
const [providersList, setProvidersList] = useState<string[]>([]);
const [loading, setLoading] = useState(true);
const [selectedModelDisplay, setSelectedModelDisplay] = useState<string>('');
const [selectedProviderDisplay, setSelectedProviderDisplay] =
useState<string>('');
const [expandedProviders, setExpandedProviders] = useState<
Record<string, boolean>
>({});
useEffect(() => {
const fetchModels = async () => {
try {
const response = await fetch('/api/models', {
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch models: ${response.status}`);
}
const data = await response.json();
const providersData: ProviderModelMap = {};
// Organize models by provider
Object.entries(data.chatModelProviders).forEach(
([provider, models]: [string, any]) => {
const providerDisplayName =
provider.charAt(0).toUpperCase() + provider.slice(1);
providersData[provider] = {
displayName: providerDisplayName,
models: [],
};
Object.entries(models).forEach(
([modelKey, modelData]: [string, any]) => {
providersData[provider].models.push({
provider,
model: modelKey,
displayName: modelData.displayName || modelKey,
});
},
);
},
);
// Filter out providers with no models
Object.keys(providersData).forEach((provider) => {
if (providersData[provider].models.length === 0) {
delete providersData[provider];
}
});
// Sort providers by name (only those that have models)
const sortedProviders = Object.keys(providersData).sort();
setProvidersList(sortedProviders);
// Initialize expanded state for all providers
const initialExpandedState: Record<string, boolean> = {};
sortedProviders.forEach((provider) => {
initialExpandedState[provider] = selectedModel?.provider === provider;
});
// Expand the first provider if none is selected
if (sortedProviders.length > 0 && !selectedModel) {
initialExpandedState[sortedProviders[0]] = true;
}
setExpandedProviders(initialExpandedState);
setProviderModels(providersData);
// Find the current model in our options to display its name
if (selectedModel) {
const provider = providersData[selectedModel.provider];
if (provider) {
const currentModel = provider.models.find(
(option) => option.model === selectedModel.model,
);
if (currentModel) {
setSelectedModelDisplay(currentModel.displayName);
setSelectedProviderDisplay(provider.displayName);
}
}
}
setLoading(false);
} catch (error) {
console.error('Error fetching models:', error);
setLoading(false);
}
};
fetchModels();
}, [selectedModel, setSelectedModel]);
const toggleProviderExpanded = (provider: string) => {
setExpandedProviders((prev) => ({
...prev,
[provider]: !prev[provider],
}));
};
const handleSelectModel = (option: ModelOption) => {
setSelectedModel({
provider: option.provider,
model: option.model,
});
setSelectedModelDisplay(option.displayName);
setSelectedProviderDisplay(
providerModels[option.provider]?.displayName || option.provider,
);
// Save to localStorage for persistence
localStorage.setItem('chatModelProvider', option.provider);
localStorage.setItem('chatModel', option.model);
};
const getDisplayText = () => {
if (loading) return 'Loading...';
if (!selectedModelDisplay) return 'Select model';
return `${selectedModelDisplay} (${selectedProviderDisplay})`;
};
return (
<Popover className="relative">
{({ open }) => (
<>
<div className="relative">
<PopoverButton className="group flex items-center justify-center text-black/50 dark:text-white/50 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary active:scale-95 transition duration-200 hover:text-black dark:hover:text-white">
<Cpu size={18} />
<span className="mx-2 text-xs font-medium overflow-hidden text-ellipsis whitespace-nowrap max-w-44 hidden lg:block">
{getDisplayText()}
</span>
<ChevronDown
size={16}
className={cn(
'transition-transform',
open ? 'rotate-180' : 'rotate-0',
)}
/>
</PopoverButton>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<PopoverPanel className="absolute z-10 w-72 transform bottom-full mb-2">
<div className="overflow-hidden rounded-lg shadow-lg ring-1 ring-black/5 dark:ring-white/5 bg-white dark:bg-dark-secondary divide-y divide-light-200 dark:divide-dark-200">
<div className="px-4 py-3">
<h3 className="text-sm font-medium text-black/90 dark:text-white/90">
Select Model
</h3>
<p className="text-xs text-black/60 dark:text-white/60 mt-1">
Choose a provider and model for your conversation
</p>
</div>
<div className="max-h-72 overflow-y-auto">
{loading ? (
<div className="px-4 py-3 text-sm text-black/70 dark:text-white/70">
Loading available models...
</div>
) : providersList.length === 0 ? (
<div className="px-4 py-3 text-sm text-black/70 dark:text-white/70">
No models available
</div>
) : (
<div className="py-1">
{providersList.map((providerKey) => {
const provider = providerModels[providerKey];
const isExpanded = expandedProviders[providerKey];
return (
<div
key={providerKey}
className="border-t border-light-200 dark:border-dark-200 first:border-t-0"
>
{/* Provider header */}
<button
className={cn(
'w-full flex items-center justify-between px-4 py-2 text-sm text-left',
'hover:bg-light-100 dark:hover:bg-dark-100',
selectedModel?.provider === providerKey
? 'bg-light-50 dark:bg-dark-50'
: '',
)}
onClick={() =>
toggleProviderExpanded(providerKey)
}
>
<div className="font-medium flex items-center">
<Cpu
size={14}
className="mr-2 text-black/70 dark:text-white/70"
/>
{provider.displayName}
{selectedModel?.provider === providerKey && (
<span className="ml-2 text-xs text-[#24A0ED]">
(active)
</span>
)}
</div>
<ChevronRight
size={14}
className={cn(
'transition-transform',
isExpanded ? 'rotate-90' : '',
)}
/>
</button>
{/* Models list */}
{isExpanded && (
<div className="pl-6">
{provider.models.map((modelOption) => (
<button
key={`${modelOption.provider}-${modelOption.model}`}
className={cn(
'w-full text-left px-4 py-2 text-sm flex items-center',
selectedModel?.provider ===
modelOption.provider &&
selectedModel?.model ===
modelOption.model
? 'bg-light-100 dark:bg-dark-100 text-black dark:text-white'
: 'text-black/70 dark:text-white/70 hover:bg-light-100 dark:hover:bg-dark-100',
)}
onClick={() =>
handleSelectModel(modelOption)
}
>
<div className="flex flex-col flex-1">
<span className="font-medium">
{modelOption.displayName}
</span>
</div>
{/* Active indicator */}
{selectedModel?.provider ===
modelOption.provider &&
selectedModel?.model ===
modelOption.model && (
<div className="ml-auto bg-[#24A0ED] text-white text-xs px-1.5 py-0.5 rounded">
Active
</div>
)}
</button>
))}
</div>
)}
</div>
);
})}
</div>
)}
</div>
</div>
</PopoverPanel>
</Transition>
</>
)}
</Popover>
);
};
export default ModelSelector;

View File

@@ -56,12 +56,12 @@ const Optimization = ({
OptimizationModes.find((mode) => mode.key === optimizationMode)
?.icon
}
<p className="text-xs font-medium">
{/* <p className="text-xs font-medium hidden lg:block">
{
OptimizationModes.find((mode) => mode.key === optimizationMode)
?.title
}
</p>
</p> */}
<ChevronDown size={20} />
</div>
</PopoverButton>

View File

@@ -96,7 +96,14 @@ export const getAvailableChatModelProviders = async () => {
for (const provider in chatModelProviders) {
const providerModels = await chatModelProviders[provider]();
if (Object.keys(providerModels).length > 0) {
models[provider] = providerModels;
// Sort models alphabetically by their keys
const sortedModels: Record<string, ChatModel> = {};
Object.keys(providerModels)
.sort()
.forEach((key) => {
sortedModels[key] = providerModels[key];
});
models[provider] = sortedModels;
}
}
@@ -131,7 +138,14 @@ export const getAvailableEmbeddingModelProviders = async () => {
for (const provider in embeddingModelProviders) {
const providerModels = await embeddingModelProviders[provider]();
if (Object.keys(providerModels).length > 0) {
models[provider] = providerModels;
// Sort embedding models alphabetically by their keys
const sortedModels: Record<string, EmbeddingModel> = {};
Object.keys(providerModels)
.sort()
.forEach((key) => {
sortedModels[key] = providerModels[key];
});
models[provider] = sortedModels;
}
}