Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions ui/desktop/src/components/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,13 @@ export default function Modal({
>
<Card
ref={modalRef}
className="relative w-[500px] max-w-full bg-bgApp rounded-xl shadow-none my-10 overflow-hidden max-h-[90vh] flex flex-col"
className="relative w-[500px] max-w-full bg-bgApp rounded-xl my-10 max-h-[90vh] flex flex-col"
>
<div className="p-6 overflow-y-auto max-h-[calc(90vh-180px)]">{children}</div>
<div className="p-8 max-h-[calc(90vh-180px)]">{children}</div>
{footer && (
<div className="border-t border-borderSubtle bg-bgApp w-full mt-auto">{footer}</div>
<div className="border-t border-borderSubtle bg-bgApp w-full rounded-b-xl overflow-hidden">
{footer}
</div>
)}
</Card>
</div>
Expand Down
6 changes: 2 additions & 4 deletions ui/desktop/src/components/settings_v2/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useConfig } from '../ConfigContext';
import { Button } from '../ui/button';
import { Plus, Sliders } from 'lucide-react';
import ExtensionsSection from './extensions/ExtensionsSection';
import { AddModelButton } from './models/AddModelButton';

interface ModelOption {
id: string;
Expand Down Expand Up @@ -101,10 +102,7 @@ export default function SettingsView({
))}
</div>
<div className="flex gap-4 pt-4 w-full">
<Button className="flex items-center gap-2 flex-1 justify-center text-textSubtle bg-black dark:bg-white hover:bg-subtle">
<Plus className="h-4 w-4" />
Add Model
</Button>
<AddModelButton />
<Button
className="flex items-center gap-2 flex-1 justify-center text-textSubtle bg-white dark:bg-black hover:bg-subtle dark:border dark:border-gray-500 dark:hover:border-gray-400"
onClick={() => {
Expand Down
21 changes: 21 additions & 0 deletions ui/desktop/src/components/settings_v2/models/AddModelButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React, { useState } from 'react';
import { Plus } from 'lucide-react';
import { Button } from '../../ui/button';
import { AddModelModal } from './AddModelModal';

export const AddModelButton = () => {
const [isAddModelModalOpen, setIsAddModelModalOpen] = useState(false);

return (
<>
<Button
className="flex items-center gap-2 flex-1 justify-center text-white dark:text-textSubtle bg-black dark:bg-white hover:bg-subtle"
onClick={() => setIsAddModelModalOpen(true)}
>
<Plus className="h-4 w-4" />
Add Model
</Button>
{isAddModelModalOpen ? <AddModelModal onClose={() => setIsAddModelModalOpen(false)} /> : null}
</>
);
};
116 changes: 116 additions & 0 deletions ui/desktop/src/components/settings_v2/models/AddModelModal.tsx
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noticed that if the list of configured providers is quite long, we the list gets cut off

Screen.Recording.2025-03-16.at.10.49.06.AM.mov

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch! I made some changes to Modal to allow for overflow. I checked some of the other modals in Settings V2 and they still look good to me.

image

Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import React, { useEffect, useState } from 'react';
import { ExternalLink, Plus } from 'lucide-react';

import Modal from '../../Modal';
import { Button } from '../../ui/button';
import { QUICKSTART_GUIDE_URL } from '../providers/modal/constants';
import { Input } from '../../ui/input';
import { Select } from '../../ui/Select';
import { useConfig } from '../../ConfigContext';
import { ToastFailureGeneral, ToastSuccessModelSwitch } from '../../settings/models/toasts';
import { initializeSystem } from '../../../../src/utils/providerUtils';

const ModalButtons = ({ onSubmit, onCancel }) => (
<div>
<Button
type="submit"
variant="ghost"
onClick={onSubmit}
className="w-full h-[60px] rounded-none border-borderSubtle text-base hover:bg-bgSubtle text-textProminent font-regular"
>
Add model
</Button>
<Button
type="button"
variant="ghost"
onClick={onCancel}
className="w-full h-[60px] rounded-none border-t border-borderSubtle hover:text-textStandard text-textSubtle hover:bg-bgSubtle text-base font-regular"
>
Cancel
</Button>
</div>
);

type AddModelModalProps = { onClose: () => void };
export const AddModelModal = ({ onClose }: AddModelModalProps) => {
const { getProviders, upsert } = useConfig();
const [providerOptions, setProviderOptions] = useState([]);
const [provider, setProvider] = useState<string | null>(null);
const [modelName, setModelName] = useState<string>('');

const changeModel = async () => {
try {
await upsert('GOOSE_PROVIDER', provider, false);
await upsert('GOOSE_MODEL', modelName, false);
await initializeSystem(provider, modelName);
ToastSuccessModelSwitch({ provider, name: modelName });
onClose();
} catch (e) {
ToastFailureGeneral(e.message);
}
};

useEffect(() => {
(async () => {
try {
const providersResponse = await getProviders(false);
const activeProviders = providersResponse.filter((provider) => provider.is_configured);
setProviderOptions(
activeProviders.map(({ metadata, name }) => ({
value: name,
label: metadata.display_name,
}))
);
} catch (error) {
console.error('Failed to load providers:', error);
}
})();
}, [getProviders]);

return (
<div className="z-10">
<Modal onClose={onClose} footer={<ModalButtons onSubmit={changeModel} onCancel={onClose} />}>
<div className="flex flex-col items-center gap-8">
<div className="flex flex-col items-center gap-3">
<Plus size={24} className="text-textStandard" />
<div className="text-textStandard font-medium">Add model</div>
<div className="text-textSubtle text-center">
Configure your AI model providers by adding their API keys. your Keys are stored
securely and encrypted locally.
</div>
<div>
<a
href={QUICKSTART_GUIDE_URL}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center text-textStandard font-medium text-sm"
>
<ExternalLink size={16} className="mr-1" />
View quick start guide
</a>
</div>
</div>

<div className="w-full flex flex-col gap-4">
<Select
options={providerOptions}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idea: maybe last option in dropdown list is Add Other Provider which, when clicked takes you to the Configure Providers Page -- haven't discussed this with design though, but curious your thoughts about if that would feel more natural to you instead of or in addition to the Configure Providers button

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a super good idea, I'm all for it.

value={providerOptions.find((option) => option.value === provider) || null}
onChange={(option) => {
setProvider(option?.value || null);
setModelName('');
}}
placeholder="Provider"
isClearable
/>
<Input
className="border-2 px-4 py-5"
placeholder="GPT"
onChange={(event) => setModelName(event.target.value)}
value={modelName}
/>
</div>
</div>
</Modal>
</div>
);
};
20 changes: 20 additions & 0 deletions ui/desktop/src/components/ui/Select.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react';
import ReactSelect from 'react-select';

export const Select = (props) => {
return (
<ReactSelect
{...props}
unstyled
classNames={{
container: () => 'w-full cursor-pointer',
indicatorSeparator: () => 'h-0',
control: ({ isFocused }) =>
`border-2 ${isFocused ? 'border-borderStandard' : 'border-borderSubtle'} focus:border-borderStandard hover:border-borderStandard rounded-md w-full px-4 py-2.5 text-sm text-textSubtle`,
menu: () => 'mt-3 bg-bgStandard shadow-xl rounded-md text-textSubtle overflow-hidden',
option: () =>
'py-4 px-4 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 text-textProminent font-medium',
}}
/>
);
};
Loading