mirror of
https://github.com/aljazceru/goose.git
synced 2026-01-10 09:54:23 +01:00
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 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,
|
||||
|
||||
@@ -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 {
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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