ui: set up model list component (#1936)

This commit is contained in:
Lily Delalande
2025-03-31 11:26:27 -04:00
committed by GitHub
parent 5e7f8a4673
commit 604d1b17cf
6 changed files with 182 additions and 7 deletions

View File

@@ -6,7 +6,7 @@ import { ModelRadioList } from './settings/models/ModelRadioList';
import { Document, ChevronUp, ChevronDown } from './icons';
import type { View } from '../App';
import { BottomMenuModeSelection } from './BottomMenuModeSelection';
import ModelsBottomBar from './settings_v2/models/subcomponents/ModelsBottomBar';
import ModelsBottomBar from './settings_v2/models/bottom_bar/ModelsBottomBar';
export default function BottomMenu({
hasMessages,

View File

@@ -3,7 +3,7 @@ import { Sliders } from 'lucide-react';
import React, { useEffect, useState } from 'react';
import { useConfig } from '../../../ConfigContext';
import { getCurrentModelAndProviderForDisplay } from '../index';
import { AddModelModal } from './AddModelModal';
import { AddModelModal } from '../subcomponents/AddModelModal';
import type { View } from '../../../../App';
interface ModelsBottomBarProps {

View File

@@ -1,6 +1,8 @@
import { initializeAgent } from '../../../agent/index';
import { toastError, toastSuccess } from '../../../toasts';
import { ProviderDetails } from '@/src/api';
import { getProviderMetadata } from './modelInterface';
import { ProviderMetadata } from '../../../api';
// titles
const CHANGE_MODEL_TOAST_TITLE = 'Model selected';
@@ -137,19 +139,20 @@ export async function getCurrentModelAndProviderForDisplay({
const gooseModel = modelProvider.model;
const gooseProvider = modelProvider.provider;
const providers = await getProviders(false);
// lookup display name
const providerDetailsList = providers.filter((provider) => provider.name === gooseProvider);
let metadata: ProviderMetadata;
if (providerDetailsList.length != 1) {
try {
metadata = await getProviderMetadata(gooseProvider, getProviders);
} catch (error) {
toastError({
title: UNKNOWN_PROVIDER_TITLE,
msg: UNKNOWN_PROVIDER_MSG,
traceback: error,
});
return { model: gooseModel, provider: gooseProvider };
}
const providerDisplayName = providerDetailsList[0].metadata.display_name;
const providerDisplayName = metadata.display_name;
return { model: gooseModel, provider: providerDisplayName };
}

View File

@@ -0,0 +1,41 @@
import { ProviderDetails } from '../../../api';
export default interface Model {
id?: number; // Make `id` optional to allow user-defined models
name: string;
provider: string;
lastUsed?: string;
alias?: string; // optional model display name
subtext?: string; // goes below model name if not the provider
}
export function createModelStruct(
modelName: string,
provider: string,
id?: number, // Make `id` optional to allow user-defined models
lastUsed?: string,
alias?: string, // optional model display name
subtext?: string
): Model {
// use the metadata to create a Model
return {
name: modelName,
provider: provider,
alias: alias,
id: id,
lastUsed: lastUsed,
subtext: subtext,
};
}
export async function getProviderMetadata(
providerName: string,
getProvidersFunc: (b: boolean) => Promise<ProviderDetails[]>
) {
const providers = await getProvidersFunc(false);
const matches = providers.find((providerMatch) => providerMatch.name === providerName);
if (!matches) {
throw Error(`No match for provider: ${providerName}`);
}
return matches[0].metadata;
}

View File

@@ -0,0 +1,101 @@
import React, { useEffect, useState } from 'react';
import Model from '../modelInterface';
import { useRecentModels } from './recentModels';
import { changeModel, getCurrentModelAndProvider } from '../index';
import { useConfig } from '../../../ConfigContext';
interface ModelRadioListProps {
renderItem: (props: {
model: Model;
isSelected: boolean;
onSelect: () => void;
}) => React.ReactNode;
className?: string;
providedModelList?: Model[];
}
export function BaseModelsList({
renderItem,
className = '',
providedModelList,
}: ModelRadioListProps) {
const { recentModels } = useRecentModels();
// allow for a custom model list to be passed if you don't want to use recent models
let modelList: Model[];
if (!providedModelList) {
modelList = recentModels;
} else {
modelList = providedModelList;
}
const { read, upsert } = useConfig();
const [selectedModel, setSelectedModel] = useState<string | null>(null);
const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
const [isInitialized, setIsInitialized] = useState(false);
// Load current model/provider once on component mount
useEffect(() => {
let isMounted = true;
const initializeCurrentModel = async () => {
try {
const result = await getCurrentModelAndProvider({ readFromConfig: read });
if (isMounted) {
setSelectedModel(result.model);
setSelectedProvider(result.provider);
setIsInitialized(true);
}
} catch (error) {
console.error('Failed to load current model:', error);
if (isMounted) {
setIsInitialized(true); // Still mark as initialized even on error
}
}
};
initializeCurrentModel();
return () => {
isMounted = false;
};
}, [read]);
const handleModelSelection = async (modelName: string, providerName: string) => {
await changeModel({ model: modelName, provider: providerName, writeToConfig: upsert });
};
// Updated to work with CustomRadio
const handleRadioChange = async (model: Model) => {
if (selectedModel === model.name) {
console.log(`Model "${model.name}" is already active.`);
return;
}
// Update local state immediately for UI feedback
setSelectedModel(model.name);
setSelectedProvider(model.provider);
try {
await handleModelSelection(model.name, model.provider);
} catch (error) {
console.error('Error selecting model:', error);
}
};
// Don't render until we've loaded the initial model/provider
if (!isInitialized) {
return <div>Loading models...</div>;
}
return (
<div className={className}>
{modelList.map((model) =>
renderItem({
model,
isSelected: selectedModel === model.name,
onSelect: () => handleRadioChange(model),
})
)}
</div>
);
}

View File

@@ -0,0 +1,30 @@
import React, { useEffect, useState } from 'react';
import Model from '../modelInterface';
const MAX_RECENT_MODELS = 3;
export function useRecentModels() {
const [recentModels, setRecentModels] = useState<Model[]>([]);
useEffect(() => {
const storedModels = localStorage.getItem('recentModels');
if (storedModels) {
setRecentModels(JSON.parse(storedModels));
}
}, []);
const addRecentModel = (model: Model) => {
const modelWithTimestamp = { ...model, lastUsed: new Date().toISOString() }; // Add lastUsed field
setRecentModels((prevModels) => {
const updatedModels = [
modelWithTimestamp,
...prevModels.filter((m) => m.name !== model.name),
].slice(0, MAX_RECENT_MODELS);
localStorage.setItem('recentModels', JSON.stringify(updatedModels));
return updatedModels;
});
};
return { recentModels, addRecentModel };
}