ui: set up model list component (#1936)
This commit is contained in:
@@ -6,7 +6,7 @@ import { ModelRadioList } from './settings/models/ModelRadioList';
|
|||||||
import { Document, ChevronUp, ChevronDown } from './icons';
|
import { Document, ChevronUp, ChevronDown } from './icons';
|
||||||
import type { View } from '../App';
|
import type { View } from '../App';
|
||||||
import { BottomMenuModeSelection } from './BottomMenuModeSelection';
|
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({
|
export default function BottomMenu({
|
||||||
hasMessages,
|
hasMessages,
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ import { Sliders } from 'lucide-react';
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useConfig } from '../../../ConfigContext';
|
import { useConfig } from '../../../ConfigContext';
|
||||||
import { getCurrentModelAndProviderForDisplay } from '../index';
|
import { getCurrentModelAndProviderForDisplay } from '../index';
|
||||||
import { AddModelModal } from './AddModelModal';
|
import { AddModelModal } from '../subcomponents/AddModelModal';
|
||||||
import type { View } from '../../../../App';
|
import type { View } from '../../../../App';
|
||||||
|
|
||||||
interface ModelsBottomBarProps {
|
interface ModelsBottomBarProps {
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { initializeAgent } from '../../../agent/index';
|
import { initializeAgent } from '../../../agent/index';
|
||||||
import { toastError, toastSuccess } from '../../../toasts';
|
import { toastError, toastSuccess } from '../../../toasts';
|
||||||
import { ProviderDetails } from '@/src/api';
|
import { ProviderDetails } from '@/src/api';
|
||||||
|
import { getProviderMetadata } from './modelInterface';
|
||||||
|
import { ProviderMetadata } from '../../../api';
|
||||||
|
|
||||||
// titles
|
// titles
|
||||||
const CHANGE_MODEL_TOAST_TITLE = 'Model selected';
|
const CHANGE_MODEL_TOAST_TITLE = 'Model selected';
|
||||||
@@ -137,19 +139,20 @@ export async function getCurrentModelAndProviderForDisplay({
|
|||||||
const gooseModel = modelProvider.model;
|
const gooseModel = modelProvider.model;
|
||||||
const gooseProvider = modelProvider.provider;
|
const gooseProvider = modelProvider.provider;
|
||||||
|
|
||||||
const providers = await getProviders(false);
|
|
||||||
|
|
||||||
// lookup display name
|
// 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({
|
toastError({
|
||||||
title: UNKNOWN_PROVIDER_TITLE,
|
title: UNKNOWN_PROVIDER_TITLE,
|
||||||
msg: UNKNOWN_PROVIDER_MSG,
|
msg: UNKNOWN_PROVIDER_MSG,
|
||||||
|
traceback: error,
|
||||||
});
|
});
|
||||||
return { model: gooseModel, provider: gooseProvider };
|
return { model: gooseModel, provider: gooseProvider };
|
||||||
}
|
}
|
||||||
const providerDisplayName = providerDetailsList[0].metadata.display_name;
|
const providerDisplayName = metadata.display_name;
|
||||||
|
|
||||||
return { model: gooseModel, provider: providerDisplayName };
|
return { model: gooseModel, provider: providerDisplayName };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user