ui: providers new design (#1446)

This commit is contained in:
Lily Delalande
2025-02-28 21:04:50 -05:00
committed by GitHub
parent c6d4fe6041
commit 7439b8199e
71 changed files with 682 additions and 604 deletions

5
ui/desktop/image.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.svg';
declare module '*.gif';

View File

@@ -10,7 +10,7 @@
"license": {
"name": "Apache-2.0"
},
"version": "1.0.9"
"version": "1.0.10"
},
"paths": {
"/config": {

View File

@@ -17,7 +17,7 @@ import SettingsView, { type SettingsViewOptions } from './components/settings/Se
import SettingsViewV2 from './components/settings_v2/SettingsView';
import MoreModelsView from './components/settings/models/MoreModelsView';
import ConfigureProvidersView from './components/settings/providers/ConfigureProvidersView';
import ProviderSettings from './components/settings/providers/providers/NewProviderSettingsPage';
import ProviderSettings from './components/settings_v2/providers/ProviderSettingsPage';
import 'react-toastify/dist/ReactToastify.css';

View File

@@ -1,47 +0,0 @@
import React from 'react';
import { ProviderCard } from './subcomponents/ProviderCard';
import ProviderState from './interfaces/ProviderState';
import OnShowModal from './callbacks/ShowModal';
import OnAdd from './callbacks/AddProviderParameters';
import OnDelete from './callbacks/DeleteProviderParameters';
import OnShowSettings from './callbacks/UpdateProviderParameters';
import OnRefresh from './callbacks/RefreshActiveProviders';
import DefaultProviderActions from './subcomponents/actions/DefaultProviderActions';
function GridLayout({ children }: { children: React.ReactNode }) {
return (
<div className="grid grid-cols-3 sm:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7 gap-3 auto-rows-fr max-w-full [&_*]:z-20">
{children}
</div>
);
}
function ProviderCards({ providers }: { providers: ProviderState[] }) {
const providerCallbacks = {
onShowModal: OnShowModal,
onAdd: OnAdd,
onDelete: OnDelete,
onShowSettings: OnShowSettings,
onRefresh: OnRefresh,
};
return (
<>
{providers.map((provider) => (
<ProviderCard
key={provider.name} // helps React efficiently update and track components when rendering lists
provider={provider}
providerCallbacks={providerCallbacks}
/>
))}
</>
);
}
export default function ProviderGrid({ providers }: { providers: ProviderState[] }) {
console.log('got these providers', providers);
return (
<GridLayout>
<ProviderCards providers={providers} />
</GridLayout>
);
}

View File

@@ -1,242 +0,0 @@
import React from 'react';
import ProviderDetails from './interfaces/ProviderDetails';
import DefaultProviderActions from './subcomponents/actions/DefaultProviderActions';
import OllamaActions from './subcomponents/actions/OllamaActions';
export interface ProviderRegistry {
name: string;
details: ProviderDetails;
}
export const PROVIDER_REGISTRY: ProviderRegistry[] = [
{
name: 'OpenAI',
details: {
id: 'openai',
name: 'OpenAI',
description: 'Access GPT-4, GPT-3.5 Turbo, and other OpenAI models',
parameters: [
{
name: 'OPENAI_API_KEY',
is_secret: true,
},
],
getActions: (provider, callbacks) => {
const { onAdd, onDelete, onShowSettings } = callbacks || {};
return [
{
id: 'default-provider-actions',
renderButton: () => (
<DefaultProviderActions
name={provider.name}
isConfigured={provider.isConfigured}
onAdd={onAdd}
onDelete={onDelete}
onShowSettings={onShowSettings}
/>
),
},
];
},
},
},
{
name: 'Anthropic',
details: {
id: 'anthropic',
name: 'Anthropic',
description: 'Access Claude and other Anthropic models',
parameters: [
{
name: 'ANTHROPIC_API_KEY',
is_secret: true,
},
],
getActions: (provider, callbacks) => {
const { onAdd, onDelete, onShowSettings } = callbacks || {};
return [
{
id: 'default-provider-actions',
renderButton: () => (
<DefaultProviderActions
name={provider.name}
isConfigured={provider.isConfigured}
onAdd={onAdd}
onDelete={onDelete}
onShowSettings={onShowSettings}
/>
),
},
];
},
},
},
{
name: 'Google',
details: {
id: 'google',
name: 'Google',
description: 'Access Gemini and other Google AI models',
parameters: [
{
name: 'GOOGLE_API_KEY',
is_secret: true,
},
],
getActions: (provider, callbacks) => {
const { onAdd, onDelete, onShowSettings } = callbacks || {};
return [
{
id: 'default-provider-actions',
renderButton: () => (
<DefaultProviderActions
name={provider.name}
isConfigured={provider.isConfigured}
onAdd={onAdd}
onDelete={onDelete}
onShowSettings={onShowSettings}
/>
),
},
];
},
},
},
{
name: 'Groq',
details: {
id: 'groq',
name: 'Groq',
description: 'Access Mixtral and other Groq-hosted models',
parameters: [
{
name: 'GROQ_API_KEY',
is_secret: true,
},
],
getActions: (provider, callbacks) => {
const { onAdd, onDelete, onShowSettings } = callbacks || {};
return [
{
id: 'default-provider-actions',
renderButton: () => (
<DefaultProviderActions
name={provider.name}
isConfigured={provider.isConfigured}
onAdd={onAdd}
onDelete={onDelete}
onShowSettings={onShowSettings}
/>
),
},
];
},
},
},
{
name: 'Databricks',
details: {
id: 'databricks',
name: 'Databricks',
description: 'Access models hosted on your Databricks instance',
parameters: [
{
name: 'DATABRICKS_HOST',
is_secret: false,
},
],
getActions: (provider, callbacks) => {
const { onAdd, onDelete, onShowSettings } = callbacks || {};
return [
{
id: 'default-provider-actions',
renderButton: () => (
<DefaultProviderActions
name={provider.name}
isConfigured={provider.isConfigured}
onAdd={onAdd}
onDelete={onDelete}
onShowSettings={onShowSettings}
/>
),
},
];
},
},
},
{
name: 'OpenRouter',
details: {
id: 'openrouter',
name: 'OpenRouter',
description: 'Access a variety of AI models through OpenRouter',
parameters: [
{
name: 'OPENROUTER_API_KEY',
is_secret: true,
},
],
getActions: (provider, callbacks) => {
const { onAdd, onDelete, onShowSettings } = callbacks || {};
return [
{
id: 'default-provider-actions',
renderButton: () => (
<DefaultProviderActions
name={provider.name}
isConfigured={provider.isConfigured}
onAdd={onAdd}
onDelete={onDelete}
onShowSettings={onShowSettings}
/>
),
},
];
},
},
},
{
name: 'Ollama',
details: {
id: 'ollama',
name: 'Ollama',
description: 'Run and use open-source models locally',
parameters: [
{
name: 'OLLAMA_HOST',
is_secret: false,
},
],
getActions: (provider, callbacks) => {
const { onAdd, onDelete, onRefresh, onShowSettings } = callbacks || {};
return [
{
id: 'ollama-actions',
renderButton: () => (
<OllamaActions
isConfigured={provider.isConfigured}
ollamaMetadata={provider.metadata}
onAdd={onAdd}
onRefresh={onRefresh}
onDelete={onDelete}
onShowSettings={onShowSettings}
/>
),
},
];
},
},
},
];
// const ACTION_IMPLEMENTATIONS = {
// 'default': (provider, callbacks) => [{
// id: 'default-provider-actions',
// renderButton: () => <DefaultProviderActions {...} />
// }],
//
// 'ollama': (provider, callbacks) => [{
// id: 'ollama-actions',
// renderButton: () => <OllamaActions {...} />
// }]
// };

View File

@@ -1,7 +0,0 @@
export default interface ProviderCallbacks {
onShowModal?: () => void;
onAdd?: () => void;
onDelete?: () => void;
onShowSettings?: () => void;
onRefresh?: () => void;
}

View File

@@ -1,41 +0,0 @@
import React from 'react';
import { Card } from '../../../../ui/card';
import ProviderSetupOverlay from './configuration_modal_subcomponents/ProviderSetupOverlay';
import ProviderSetupHeader from './configuration_modal_subcomponents/ProviderSetupHeader';
import ProviderSetupForm from './configuration_modal_subcomponents/ProviderSetupForm';
import ProviderSetupActions from './configuration_modal_subcomponents/ProviderSetupActions';
import ProviderConfiguationModalProps from './interfaces/ProviderConfigurationModalProps';
export default function ProviderConfigurationModal({
provider,
title,
onSubmit,
onCancel,
}: ProviderConfiguationModalProps) {
const [configValues, setConfigValues] = React.useState<{ [key: string]: string }>({});
const headerText = title || `Setup ${provider}`;
const handleSubmitForm = (e: React.FormEvent) => {
e.preventDefault();
onSubmit(configValues);
};
return (
<ProviderSetupOverlay>
<Card className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[500px] bg-bgApp rounded-xl overflow-hidden shadow-none p-[16px] pt-[24px] pb-0">
<div className="px-4 pb-0 space-y-8">
<ProviderSetupHeader headerText={headerText} />
<ProviderSetupForm
configValues={configValues}
setConfigValues={setConfigValues}
onSubmit={handleSubmitForm}
provider={provider}
/>
<ProviderSetupActions onCancel={onCancel} />
</div>
</Card>
</ProviderSetupOverlay>
);
}

View File

@@ -1,50 +0,0 @@
import React from 'react';
import { Input } from '../../../../../ui/input';
import { Lock } from 'lucide-react';
import { isSecretKey } from '../../../../api_keys/utils';
import ProviderSetupFormProps from '../interfaces/ProviderSetupFormProps';
import ParameterSchema from '../../interfaces/ParameterSchema';
/**
* Renders the form with required input fields and the "lock" info row.
* The submit/cancel buttons are in a separate ProviderSetupActions component.
*/
export default function ProviderSetupForm({
configValues,
setConfigValues,
onSubmit,
provider,
}: ProviderSetupFormProps) {
const parameters: ParameterSchema[] = provider.parameters;
return (
<form onSubmit={onSubmit}>
<div className="mt-[24px] space-y-4">
{parameters.map((parameter) => (
<div key={parameter.name}>
<Input
type={parameter.is_secret ? 'password' : 'text'}
value={configValues[parameter.name] || ''}
onChange={(e) =>
setConfigValues((prev) => ({
...prev,
[parameter.name]: e.target.value,
}))
}
placeholder={parameter.name}
className="w-full h-14 px-4 font-regular rounded-lg border shadow-none border-gray-300 bg-white text-lg placeholder:text-gray-400 font-regular text-gray-900"
required
/>
</div>
))}
<div className="flex text-gray-600 dark:text-gray-300">
<Lock className="w-6 h-6" />
<span className="text-sm font-light ml-4 mt-[2px]">
Your configuration values will be stored securely in the keychain and used only for
making requests to {provider.name}.
</span>
</div>
</div>
{/* The action buttons are not in this form; they're in ProviderSetupActions. */}
</form>
);
}

View File

@@ -1,16 +0,0 @@
import React from 'react';
interface ProviderSetupHeaderProps {
headerText: string;
}
/**
* Renders the header (title) for the modal.
*/
export default function ProviderSetupHeader({ headerText }: ProviderSetupHeaderProps) {
return (
<div className="flex">
<h2 className="text-2xl font-regular text-textStandard">{headerText}</h2>
</div>
);
}

View File

@@ -1,67 +0,0 @@
import React from 'react';
import { AddButton, DeleteButton, GearSettingsButton } from './ActionButtons';
interface ProviderActionsProps {
name: string;
isConfigured: boolean;
onAdd?: () => void;
onConfigure?: () => void;
onDelete?: () => void;
onShowSettings?: () => void;
}
function getDefaultTooltipMessages(name: string, actionType: string) {
switch (actionType) {
case 'add':
return `Configure ${name} settings`;
case 'edit':
return `Edit ${name} settings`;
case 'delete':
return `Delete ${name} settings`;
default:
return null;
}
}
export default function DefaultProviderActions({
name,
isConfigured,
onAdd,
onDelete,
onShowSettings,
}: ProviderActionsProps) {
return (
<>
{/*Set up an unconfigured provider */}
{!isConfigured && (
<AddButton
tooltip={getDefaultTooltipMessages(name, 'add')}
onClick={(e) => {
e.stopPropagation();
onAdd?.();
}}
/>
)}
{/*Edit settings of configured provider*/}
{isConfigured && (
<GearSettingsButton
tooltip={getDefaultTooltipMessages(name, 'edit')}
onClick={(e) => {
e.stopPropagation();
onShowSettings?.();
}}
/>
)}
{/*Delete configuration*/}
{isConfigured && (
<DeleteButton
tooltip={getDefaultTooltipMessages(name, 'delete')}
onClick={(e) => {
e.stopPropagation();
onDelete?.();
}}
/>
)}
</>
);
}

View File

@@ -1,87 +0,0 @@
import React from 'react';
import { AddButton, DeleteButton, GearSettingsButton, RefreshButton } from './ActionButtons';
import OllamaMetadata from '../../interfaces/OllamaMetadata';
interface OllamaActionsProps {
isConfigured: boolean;
ollamaMetadata: OllamaMetadata;
onRefresh?: (e: React.MouseEvent) => void;
onAdd?: () => void;
onDelete?: () => void;
onShowSettings?: () => void;
}
export default function OllamaActions({
isConfigured,
ollamaMetadata,
onRefresh,
onAdd,
onDelete,
onShowSettings,
}: OllamaActionsProps) {
const showHostDeleteButton = isConfigured && ollamaMetadata.location === 'host' && !onDelete;
const showRefreshButton = !isConfigured && onRefresh;
// add host url to overwrite the app url OR if not configured at all yet
const showAddHostUrlButton =
(isConfigured && ollamaMetadata.location === 'app' && onAdd) || (!isConfigured && onAdd);
const showHostUrlSettingsButton =
isConfigured && ollamaMetadata.location === 'host' && onShowSettings;
// Well figure out which buttons to render:
// 1) Refresh button if not configured
// 2) If configured via app => show "plus" to switch to host config
// 3) If configured via host => show "X" to remove the host and "gear" to edit
return (
// TODO: is this the right class name?
<div className="flex items-center space-x-2">
{/* (1) Refresh button if not configured */}
{showRefreshButton && (
<RefreshButton
tooltip="Refresh to check if Ollama is running."
onClick={(e) => {
e.stopPropagation();
onRefresh?.(e);
}}
></RefreshButton>
)}
{/* (2) If configured location = 'app', show a plus button to switch / set host */}
{showAddHostUrlButton && (
<AddButton
tooltip="Switch to custom OLLAMA_HOST."
onClick={(e) => {
e.stopPropagation();
onAdd?.();
}}
></AddButton>
)}
{/* (3) If configured location = 'host', show an X to delete or revert config */}
{showHostDeleteButton && (
<DeleteButton
tooltip="Delete OLLAMA_HOST."
onClick={(e) => {
e.stopPropagation();
onDelete?.();
}}
></DeleteButton>
)}
{/* (4) If configured location = 'host', show a gear to view and edit config */}
{showHostUrlSettingsButton && (
<GearSettingsButton
tooltip={'View and edit OLLAMA_HOST'}
onClick={(e) => {
e.stopPropagation();
onShowSettings?.();
}}
></GearSettingsButton>
)}
</div>
);
}

View File

@@ -0,0 +1,74 @@
import React from 'react';
import { ProviderCard } from './subcomponents/ProviderCard';
import ProviderState from './interfaces/ProviderState';
import OnRefresh from './callbacks/RefreshActiveProviders';
import { ProviderModalProvider, useProviderModal } from './modal/ProviderModalProvider';
import ProviderConfigurationModal from './modal/ProviderConfiguationModal';
function GridLayout({ children }: { children: React.ReactNode }) {
return (
<div className="grid grid-cols-[repeat(auto-fill,_minmax(140px,_1fr))] gap-3 [&_*]:z-20">
{children}
</div>
);
}
function ProviderCards({
providers,
isOnboarding,
}: {
providers: ProviderState[];
isOnboarding: boolean;
}) {
const { openModal } = useProviderModal();
// Define the callbacks for provider actions
const providerCallbacks = {
// Replace your OnShowSettings with the modal opener
onConfigure: (provider: ProviderState) => {
console.log('Configure button clicked for:', provider.name);
openModal(provider, {
onSubmit: (values: any) => {
console.log(`Configuring ${provider.name}:`, values);
// Your logic to save the configuration
},
formProps: {},
});
console.log('openModal called'); // Check if this executes
},
onLaunch: (provider: ProviderState) => {
OnRefresh();
},
};
return (
<>
{providers.map((provider) => (
<ProviderCard
key={provider.name} // helps React efficiently update and track components when rendering lists
provider={provider}
buttonCallbacks={providerCallbacks}
isOnboarding={isOnboarding}
/>
))}
</>
);
}
export default function ProviderGrid({
providers,
isOnboarding,
}: {
providers: ProviderState[];
isOnboarding: boolean;
}) {
console.log('(1) Provider Grid -- is this the onboarding page?', isOnboarding);
return (
<GridLayout>
<ProviderModalProvider>
<ProviderCards providers={providers} isOnboarding={isOnboarding} />
<ProviderConfigurationModal /> {/* This is missing! */}
</ProviderModalProvider>
</GridLayout>
);
}

View File

@@ -0,0 +1,199 @@
import React from 'react';
import ProviderDetails from './interfaces/ProviderDetails';
import DefaultCardButtons from './subcomponents/buttons/DefaultCardButtons';
import ButtonCallbacks from '@/src/components/settings_v2/providers/interfaces/ButtonCallbacks';
import ProviderState from '@/src/components/settings_v2/providers/interfaces/ProviderState';
// Helper function to generate default actions for most providers
const getDefaultButtons = (
provider: ProviderState,
callbacks: ButtonCallbacks,
isOnboardingPage
) => {
return [
{
id: 'default-buttons',
renderButton: () => (
<DefaultCardButtons
provider={provider}
callbacks={callbacks}
isOnboardingPage={isOnboardingPage}
/>
),
},
];
};
export interface ProviderRegistry {
name: string;
details: ProviderDetails;
}
/**
* Provider Registry System
* ========================
*
* This registry defines all available providers and how they behave in the UI.
* It works with a dynamic modal system to create a flexible, extensible architecture
* for managing provider configurations.
*
* How the System Works:
* --------------------
*
* 1. Provider Definition:
* Each provider entry in the registry defines its core properties:
* - Basic info (id, name, description)
* - Parameters needed for configuration
* - Optional custom form component
* - Action buttons that appear on provider cards
*
* 2. Two-Level Configuration:
* a) Provider Card UI - What buttons appear on each provider card
* - Controlled by the provider's getActions() function
* - Most providers use default buttons (configure/launch)
* - Can be customized for special providers
*
* b) Modal Content - What form appears in the configuration modal
* - A single modal component exists in the app
* - Content changes dynamically based on the provider being configured
* - If provider has CustomForm property, that component is rendered
* - Otherwise, DefaultProviderForm renders based on parameters array
*
* 3. Modal Flow:
* - User clicks Configure button on a provider card
* - Button handler calls openModal() with the provider object
* - Modal context stores the current provider and opens the modal
* - ProviderConfigModal checks for CustomForm on the current provider
* - Appropriate form is rendered with provider data passed as props
*
* Adding a New Provider:
* ---------------------
*
* For a standard provider with simple configuration:
* - Define parameters array with all required fields
* - Use the default getActions function
* - No need to specify a CustomForm
*
* For a provider needing custom configuration:
* - Define parameters array (even if just for documentation)
* - Create a custom form component and assign to CustomForm property
* - Use the default or custom getActions function
*
* This architecture centralizes provider definitions while allowing
* flexibility for special cases, keeping the codebase maintainable.
*/
export const PROVIDER_REGISTRY: ProviderRegistry[] = [
{
name: 'OpenAI',
details: {
id: 'openai',
name: 'OpenAI',
description: 'Access GPT-4, GPT-3.5 Turbo, and other OpenAI models',
parameters: [
{
name: 'OPENAI_API_KEY',
is_secret: true,
},
],
getActions: (provider, callbacks, isOnboardingPage) =>
getDefaultButtons(provider, callbacks, isOnboardingPage),
},
},
{
name: 'Anthropic',
details: {
id: 'anthropic',
name: 'Anthropic',
description: 'Access Claude and other Anthropic models',
parameters: [
{
name: 'ANTHROPIC_API_KEY',
is_secret: true,
},
],
getActions: (provider, callbacks, isOnboardingPage) =>
getDefaultButtons(provider, callbacks, isOnboardingPage),
},
},
{
name: 'Google',
details: {
id: 'google',
name: 'Google',
description: 'Access Gemini and other Google AI models',
parameters: [
{
name: 'GOOGLE_API_KEY',
is_secret: true,
},
],
getActions: (provider, callbacks, isOnboardingPage) =>
getDefaultButtons(provider, callbacks, isOnboardingPage),
},
},
{
name: 'Groq',
details: {
id: 'groq',
name: 'Groq',
description: 'Access Mixtral and other Groq-hosted models',
parameters: [
{
name: 'GROQ_API_KEY',
is_secret: true,
},
],
getActions: (provider, callbacks, isOnboardingPage) =>
getDefaultButtons(provider, callbacks, isOnboardingPage),
},
},
{
name: 'Databricks',
details: {
id: 'databricks',
name: 'Databricks',
description: 'Access models hosted on your Databricks instance',
parameters: [
{
name: 'DATABRICKS_HOST',
is_secret: false,
},
],
getActions: (provider, callbacks, isOnboardingPage) =>
getDefaultButtons(provider, callbacks, isOnboardingPage),
},
},
{
name: 'OpenRouter',
details: {
id: 'openrouter',
name: 'OpenRouter',
description: 'Access a variety of AI models through OpenRouter',
parameters: [
{
name: 'OPENROUTER_API_KEY',
is_secret: true,
},
],
getActions: (provider, callbacks, isOnboardingPage) =>
getDefaultButtons(provider, callbacks, isOnboardingPage),
},
},
{
name: 'Ollama',
details: {
id: 'ollama',
name: 'Ollama',
description: 'Run and use open-source models locally',
parameters: [
{
name: 'OLLAMA_HOST',
is_secret: false,
},
],
getActions: (provider, callbacks, isOnboardingPage) =>
getDefaultButtons(provider, callbacks, isOnboardingPage),
},
},
];

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { ScrollArea } from '../../../ui/scroll-area';
import BackButton from '../../../ui/BackButton';
import { ScrollArea } from '../../ui/scroll-area';
import BackButton from '../../ui/BackButton';
import ProviderGrid from './ProviderGrid';
import ProviderState from './interfaces/ProviderState';
@@ -68,7 +68,7 @@ export default function ProviderSettings({ onClose }: { onClose: () => void }) {
{/* Content Area */}
<div className="max-w-5xl pt-4 px-8">
<div className="relative z-10">
<ProviderGrid providers={fakeProviderState} />
<ProviderGrid providers={fakeProviderState} isOnboarding={true} />
</div>
</div>
</div>

View File

@@ -0,0 +1,6 @@
import ProviderState from '../interfaces/ProviderState';
export default interface ButtonCallbacks {
onConfigure?: (provider: ProviderState) => void;
onLaunch?: (provider: ProviderState) => void;
}

View File

@@ -1,8 +1,8 @@
// metadata and action builder
import ProviderState from './ProviderState';
import ConfigurationAction from './ConfigurationAction';
import ParameterSchema from '../parameters/interfaces/ParameterSchema';
import ProviderCallbacks from './ConfigurationCallbacks';
import ParameterSchema from '../interfaces/ParameterSchema';
import ButtonCallbacks from './ButtonCallbacks';
export default interface ProviderDetails {
id: string;
@@ -10,5 +10,9 @@ export default interface ProviderDetails {
description: string;
parameters: ParameterSchema[];
getTags?: (name: string) => string[];
getActions?: (provider: ProviderState, callbacks: ProviderCallbacks) => ConfigurationAction[];
getActions?: (
provider: ProviderState,
callbacks: ButtonCallbacks,
isOnboardingPage: boolean
) => ConfigurationAction[];
}

View File

@@ -0,0 +1,84 @@
import React, { useEffect, useState } from 'react';
import ProviderSetupOverlay from './subcomponents/ProviderSetupOverlay';
import ProviderSetupHeader from './subcomponents/ProviderSetupHeader';
import DefaultProviderSetupForm from './subcomponents/forms/DefaultProviderSetupForm';
import ProviderSetupActions from './subcomponents/ProviderSetupActions';
import ProviderLogo from './subcomponents/ProviderLogo';
import ProviderConfiguationModalProps from './interfaces/ProviderConfigurationModalProps';
import { useProviderModal } from './ProviderModalProvider';
import { toast } from 'react-toastify';
export default function ProviderConfigurationModal() {
const { isOpen, currentProvider, modalProps, closeModal } = useProviderModal();
console.log('currentProvider', currentProvider);
const [configValues, setConfigValues] = useState({});
// Reset form values when provider changes
useEffect(() => {
if (currentProvider) {
// Initialize form with default values
const initialValues = {};
if (currentProvider.parameters) {
currentProvider.parameters.forEach((param) => {
initialValues[param.name] = param.defaultValue || '';
});
}
setConfigValues(initialValues);
} else {
setConfigValues({});
}
}, [currentProvider]);
if (!isOpen || !currentProvider) return null;
const headerText = `Configure ${currentProvider.name}`;
const descriptionText = `Add your generated api keys for this provider to integrate into Goose`;
// Use custom form component if provider specifies one, otherwise use default
const FormComponent = currentProvider.CustomForm || DefaultProviderSetupForm;
const handleSubmitForm = (e) => {
e.preventDefault();
// Use custom submit handler if provided in modalProps
if (modalProps.onSubmit) {
modalProps.onSubmit(configValues);
} else {
// Default submit behavior
toast('Submitted configuration!');
}
closeModal();
};
const handleCancel = () => {
// Use custom cancel handler if provided
if (modalProps.onCancel) {
modalProps.onCancel();
}
closeModal();
};
return (
<ProviderSetupOverlay>
<div className="space-y-1">
{/* Logo area - centered above title */}
<ProviderLogo providerName={currentProvider.id} />
{/* Title and some information - centered */}
<ProviderSetupHeader title={headerText} body={descriptionText} />
</div>
{/* Contains information used to set up each provider */}
<FormComponent
configValues={configValues}
setConfigValues={setConfigValues}
onSubmit={handleSubmitForm}
provider={currentProvider}
{...(modalProps.formProps || {})} // Spread any custom form props
/>
<ProviderSetupActions onCancel={handleCancel} />
</ProviderSetupOverlay>
);
}

View File

@@ -0,0 +1,46 @@
import React, { createContext, useContext, useState } from 'react';
const ProviderModalContext = createContext({
isOpen: false,
currentProvider: null,
modalProps: {},
openModal: (provider, additionalProps) => {},
closeModal: () => {},
});
export const useProviderModal = () => useContext(ProviderModalContext);
export const ProviderModalProvider = ({ children }) => {
const [isOpen, setIsOpen] = useState(false);
const [currentProvider, setCurrentProvider] = useState(null);
const [modalProps, setModalProps] = useState({});
const openModal = (provider, additionalProps = {}) => {
setCurrentProvider(provider);
setModalProps(additionalProps);
setIsOpen(true);
};
const closeModal = () => {
setIsOpen(false);
// Use a small timeout to prevent UI flicker
setTimeout(() => {
setCurrentProvider(null);
setModalProps({});
}, 200);
};
return (
<ProviderModalContext.Provider
value={{
isOpen,
currentProvider,
modalProps,
openModal,
closeModal,
}}
>
{children}
</ProviderModalContext.Provider>
);
};

View File

@@ -0,0 +1 @@
export const QUICKSTART_GUIDE_URL = 'https://block.github.io/goose/docs/quickstart';

View File

@@ -1,9 +1,9 @@
import React from 'react';
import ProviderDetails from '../../interfaces/ProviderDetails';
import ProviderState from '../../interfaces/ProviderState';
export default interface ProviderSetupFormProps {
configValues: { [key: string]: string };
setConfigValues: React.Dispatch<React.SetStateAction<{ [key: string]: string }>>;
onSubmit: (e: React.FormEvent) => void;
provider: ProviderDetails;
provider: ProviderState;
}

View File

@@ -0,0 +1,33 @@
import React from 'react';
import OpenAILogo from './icons/openai@3x.png';
import AnthropicLogo from './icons/anthropic@3x.png';
import GoogleLogo from './icons/google@3x.png';
import GroqLogo from './icons/groq@3x.png';
import OllamaLogo from './icons/ollama@3x.png';
import DatabricksLogo from './icons/databricks@3x.png';
import OpenRouterLogo from './icons/openrouter@3x.png';
// Map provider names to their logos
const providerLogos = {
openai: OpenAILogo,
anthropic: AnthropicLogo,
google: GoogleLogo,
groq: GroqLogo,
ollama: OllamaLogo,
databricks: DatabricksLogo,
openrouter: OpenRouterLogo,
};
export default function ProviderLogo({ providerName }) {
// Convert provider name to lowercase and fetch the logo
const logoKey = providerName.toLowerCase();
const logo = providerLogos[logoKey] || OpenAILogo; // TODO: need default icon
return (
<div className="flex justify-center mb-2">
<div className="w-12 h-12 bg-black rounded-full overflow-hidden flex items-center justify-center">
<img src={logo} alt={`${providerName} logo`} className="w-16 h-16 object-contain" />
</div>
</div>
);
}

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { Button } from '../../../../../ui/button';
import { Button } from '../../../../ui/button';
interface ProviderSetupActionsProps {
onCancel: () => void;
@@ -7,16 +7,16 @@ interface ProviderSetupActionsProps {
/**
* Renders the "Submit" and "Cancel" buttons at the bottom.
* Notice we rely on the parent's `onSubmit` in the form, so we only handle Cancel here.
* Updated to match the design from screenshots.
*/
export default function ProviderSetupActions({ onCancel }: ProviderSetupActionsProps) {
return (
<div className="mt-[8px] -ml-8 -mr-8 pt-8">
<div className="mt-8 -ml-8 -mr-8">
{/* We rely on the <form> "onSubmit" for the actual Submit logic */}
<Button
type="submit"
variant="ghost"
className="w-full h-[60px] rounded-none border-t border-borderSubtle text-md hover:bg-bgSubtle text-textProminent font-regular"
className="w-full h-[60px] rounded-none border-t border-borderSubtle text-md hover:bg-bgSubtle text-textProminent font-medium"
>
Submit
</Button>

View File

@@ -0,0 +1,29 @@
import React from 'react';
import { ExternalLink } from 'lucide-react';
import { QUICKSTART_GUIDE_URL } from '../constants';
interface ProviderSetupHeaderProps {
title: string;
body: string;
}
/**
* Renders the header (title + description + link to guide) for the modal.
*/
export default function ProviderSetupHeader({ title, body }: ProviderSetupHeaderProps) {
return (
<div className="text-center">
<h2 className="text-xl font-medium text-textStandard mb-3">{title}</h2>
<div className="text-lg text-gray-400 font-light mb-4">{body}</div>
<a
href={QUICKSTART_GUIDE_URL}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center text-textProminent text-sm"
>
<ExternalLink size={16} className="mr-1" />
View quick start guide
</a>
</div>
);
}

View File

@@ -1,4 +1,5 @@
import React from 'react';
import { Card } from '../../../../ui/card';
interface ProviderSetupOverlayProps {
children: React.ReactNode;
@@ -10,7 +11,9 @@ interface ProviderSetupOverlayProps {
export default function ProviderSetupOverlay({ children }: ProviderSetupOverlayProps) {
return (
<div className="fixed inset-0 bg-black/20 dark:bg-white/20 backdrop-blur-sm transition-colors animate-[fadein_200ms_ease-in_forwards]">
{children}
<Card className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[500px] bg-bgApp rounded-xl overflow-hidden shadow-none p-[16px] pt-[24px] pb-0">
<div className="px-4 pb-0 space-y-6">{children}</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,45 @@
import React from 'react';
import { Input } from '../../../../../ui/input';
import { Lock } from 'lucide-react';
import ProviderSetupFormProps from '../../interfaces/ProviderSetupFormProps';
import ParameterSchema from '../../../interfaces/ParameterSchema';
import { PROVIDER_REGISTRY } from '../../../ProviderRegistry';
export default function DefaultProviderSetupForm({
configValues,
setConfigValues,
onSubmit,
provider,
}: ProviderSetupFormProps) {
const providerEntry = PROVIDER_REGISTRY.find((p) => p.name === provider.name);
const parameters: ParameterSchema[] = providerEntry.details.parameters;
return (
<form onSubmit={onSubmit}>
<div className="mt-4 space-y-4">
{parameters.map((parameter) => (
<div key={parameter.name}>
<Input
type={parameter.is_secret ? 'password' : 'text'}
value={configValues[parameter.name] || ''}
onChange={(e) =>
setConfigValues((prev) => ({
...prev,
[parameter.name]: e.target.value,
}))
}
placeholder={parameter.name.replace(/_/g, ' ')}
className="w-full h-14 px-4 font-regular rounded-lg border shadow-none border-gray-300 bg-white text-lg placeholder:text-gray-400 font-regular text-gray-900"
required
/>
</div>
))}
<div className="flex items-start mt-2 text-gray-600 dark:text-gray-300">
<Lock className="w-5 h-5 mt-1" />
<span className="text-sm font-light ml-2">Keys are stored in a secure .env file</span>
</div>
</div>
</form>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -7,9 +7,8 @@ interface CardBodyProps {
}
export default function CardBody({ actions }: CardBodyProps) {
console.log('in card body');
return (
<div className="space-x-2 text-center flex items-center justify-between">
<div className="flex items-center justify-start">
<CardActions actions={actions} />
</div>
);

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { ExclamationButton, GreenCheckButton } from './actions/ActionButtons';
import { ExclamationButton, GreenCheckButton } from './buttons/CardButtons';
import {
ConfiguredProviderTooltipMessage,
OllamaNotConfiguredTooltipMessage,
@@ -28,7 +28,7 @@ function ProviderNameAndStatus({ name, isConfigured }: ProviderNameAndStatusProp
const ollamaNotConfigured = !isConfigured && name === 'Ollama';
return (
<div className="flex items-center">
<div className="flex items-center justify-between w-full">
<CardTitle name={name} />
{/* Configured state: Green check */}

View File

@@ -3,15 +3,16 @@ import CardContainer from './CardContainer';
import CardHeader from './CardHeader';
import ProviderState from '../interfaces/ProviderState';
import CardBody from './CardBody';
import ProviderCallbacks from '../interfaces/ConfigurationCallbacks';
import ButtonCallbacks from '../interfaces/ButtonCallbacks';
import { PROVIDER_REGISTRY } from '../ProviderRegistry';
interface ProviderCardProps {
provider: ProviderState;
providerCallbacks: ProviderCallbacks;
buttonCallbacks: ButtonCallbacks;
isOnboarding: boolean;
}
export function ProviderCard({ provider, providerCallbacks }: ProviderCardProps) {
export function ProviderCard({ provider, buttonCallbacks, isOnboarding }: ProviderCardProps) {
const providerEntry = PROVIDER_REGISTRY.find((p) => p.name === provider.name);
// Add safety check
@@ -29,7 +30,7 @@ export function ProviderCard({ provider, providerCallbacks }: ProviderCardProps)
console.log('provider details', providerDetails);
try {
const actions = providerDetails.getActions(provider, providerCallbacks);
const actions = providerDetails.getActions(provider, buttonCallbacks, isOnboarding);
return (
<CardContainer

View File

@@ -1,8 +1,8 @@
import React from 'react';
import { Button } from '../../../../../ui/button';
import { Button } from '../../../../ui/button';
import clsx from 'clsx';
import { TooltipWrapper } from './TooltipWrapper';
import { Check, CircleHelp, Plus, RefreshCw, Rocket, Settings, X } from 'lucide-react';
import { Check, CircleHelp, Plus, RefreshCw, Rocket, Sliders, X } from 'lucide-react';
interface ActionButtonProps extends React.ComponentProps<typeof Button> {
/** Icon component to render, e.g. `RefreshCw` from lucide-react */
@@ -11,11 +11,15 @@ interface ActionButtonProps extends React.ComponentProps<typeof Button> {
tooltip?: React.ReactNode;
/** Additional classes for styling. */
className?: string;
/** Text to display next to the icon */
text?: string;
/** Additional class for the icon specifically */
iconClassName?: string;
}
// className is the styling for the <Button/> component -- below is the default
// Base styles for all action buttons
const baseActionButtonClasses = `
rounded-full h-7 w-7 p-0
rounded-full
bg-bgApp hover:bg-bgApp shadow-none
text-textSubtle
border border-borderSubtle
@@ -24,22 +28,35 @@ const baseActionButtonClasses = `
transition-colors
`;
// Additional styles for icon-only buttons
const iconOnlyClasses = `
h-7 w-7 p-0
`;
// Additional styles for buttons with text and icon
const withTextClasses = `
px-3 py-1
`;
export function ActionButton({
icon: Icon,
size = 'sm',
variant = 'default',
tooltip,
className,
text,
iconClassName,
...props
}: ActionButtonProps) {
// Determine if this is an icon-only button or one with text
const buttonStyle = text
? clsx(baseActionButtonClasses, withTextClasses, className)
: clsx(baseActionButtonClasses, iconOnlyClasses, className);
const ButtonElement = (
<Button
size={size}
variant={variant}
className={clsx(baseActionButtonClasses, className)}
{...props}
>
{Icon && <Icon className="!size-4" />}
<Button size={size} variant={variant} className={buttonStyle} {...props}>
{Icon && <Icon className={clsx('!size-4', iconClassName)} />}
{text && <span>{text}</span>}
</Button>
);
@@ -62,17 +79,14 @@ export function GreenCheckButton({
icon={Check}
tooltip={tooltip}
className={`
bg-green-100
dark:bg-green-900/30
text-green-600
dark:text-green-500
hover:bg-green-100
hover:text-green-600
border-none
shadow-none
w-5 h-5
cursor-default
${className} // Removed the nullish coalescing operator as default is provided
${className}
`}
onClick={() => {}}
{...props}
@@ -84,8 +98,17 @@ export function ExclamationButton({ tooltip, className, ...props }: ActionButton
return <ActionButton icon={CircleHelp} tooltip={tooltip} onClick={() => {}} {...props} />;
}
export function GearSettingsButton({ tooltip, className, ...props }: ActionButtonProps) {
return <ActionButton icon={Settings} tooltip={tooltip} className={className} {...props} />;
export function ConfigureSettingsButton({ tooltip, className, ...props }: ActionButtonProps) {
return (
<ActionButton
icon={Sliders}
tooltip={tooltip}
className={className}
text={'Configure'}
iconClassName="rotate-90"
{...props}
/>
);
}
export function AddButton({ tooltip, className, ...props }: ActionButtonProps) {
@@ -101,5 +124,13 @@ export function RefreshButton({ tooltip, className, ...props }: ActionButtonProp
}
export function RocketButton({ tooltip, className, ...props }: ActionButtonProps) {
return <ActionButton icon={Rocket} tooltip={tooltip} className={className} {...props} />;
return (
<ActionButton
icon={Rocket}
tooltip={tooltip}
className={className}
text={'Launch'}
{...props}
/>
);
}

View File

@@ -0,0 +1,77 @@
import React from 'react';
import { ConfigureSettingsButton, RocketButton } from './CardButtons';
import ButtonCallbacks from '../../interfaces/ButtonCallbacks';
import ProviderState from '@/src/components/settings_v2/providers/interfaces/ProviderState';
// can define other optional callbacks as needed
interface CardButtonsProps {
provider: ProviderState;
isOnboardingPage?: boolean;
callbacks: ButtonCallbacks; // things like onConfigure, onDelete
}
function getDefaultTooltipMessages(name: string, actionType: string) {
switch (actionType) {
case 'add':
return `Configure ${name} settings`;
case 'edit':
return `Edit ${name} settings`;
case 'delete':
return `Delete ${name} settings`;
default:
return null;
}
}
/// This defines a group of buttons that will appear on the card
/// Controlled by if a provider is configured and which version of the grid page we're on (onboarding vs settings page)
/// This is the default button group
///
/// Settings page:
/// - show configure button
/// Onboarding page:
/// - show configure button if NOT configured
/// - show rocket launch button if configured
///
/// We inject what will happen if we click on a button via on<Function>
/// - onConfigure: pop open a modal -- modal is configured dynamically
/// - onLaunch: continue to chat window
export default function DefaultCardButtons({
provider,
isOnboardingPage,
callbacks,
}: CardButtonsProps) {
return (
<>
{/*Set up an unconfigured provider */}
{!provider.isConfigured && (
<ConfigureSettingsButton
tooltip={getDefaultTooltipMessages(provider.name, 'add')}
onClick={(e) => {
e.stopPropagation();
callbacks.onConfigure(provider);
}}
/>
)}
{/*show edit tooltip instead when hovering over button for configured providers*/}
{provider.isConfigured && !isOnboardingPage && (
<ConfigureSettingsButton
tooltip={getDefaultTooltipMessages(provider.name, 'edit')}
onClick={(e) => {
e.stopPropagation();
callbacks.onConfigure(provider);
}}
/>
)}
{/*show Launch button for configured providers on onboarding page*/}
{provider.isConfigured && isOnboardingPage && (
<RocketButton
onClick={(e) => {
e.stopPropagation();
callbacks.onLaunch(provider);
}}
/>
)}
</>
);
}

View File

@@ -1,11 +1,6 @@
// TooltipWrapper.tsx
import React from 'react';
import {
Tooltip,
TooltipTrigger,
TooltipContent,
TooltipProvider,
} from '../../../../../ui/Tooltip';
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '../../../../ui/Tooltip';
import { Portal } from '@radix-ui/react-portal';
interface TooltipWrapperProps {