mirror of
https://github.com/aljazceru/goose.git
synced 2026-02-18 04:54:34 +01:00
ui: introduce form injection for provider modals (#1493)
This commit is contained in:
@@ -22,32 +22,28 @@ function ProviderCards({
|
||||
}) {
|
||||
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();
|
||||
},
|
||||
const configureProviderViaModal = (provider: ProviderState) => {
|
||||
openModal(provider, {
|
||||
onSubmit: (values: any) => {
|
||||
console.log(`Configuring ${provider.name}:`, values);
|
||||
// Your logic to save the configuration
|
||||
},
|
||||
formProps: {},
|
||||
});
|
||||
};
|
||||
|
||||
const handleLaunch = () => {
|
||||
OnRefresh();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{providers.map((provider) => (
|
||||
<ProviderCard
|
||||
key={provider.name} // helps React efficiently update and track components when rendering lists
|
||||
key={provider.name}
|
||||
provider={provider}
|
||||
buttonCallbacks={providerCallbacks}
|
||||
onConfigure={() => configureProviderViaModal(provider)}
|
||||
onLaunch={handleLaunch}
|
||||
isOnboarding={isOnboarding}
|
||||
/>
|
||||
))}
|
||||
@@ -67,7 +63,7 @@ export default function ProviderGrid({
|
||||
<GridLayout>
|
||||
<ProviderModalProvider>
|
||||
<ProviderCards providers={providers} isOnboarding={isOnboarding} />
|
||||
<ProviderConfigurationModal /> {/* This is missing! */}
|
||||
<ProviderConfigurationModal />
|
||||
</ProviderModalProvider>
|
||||
</GridLayout>
|
||||
);
|
||||
|
||||
@@ -1,28 +1,6 @@
|
||||
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}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
};
|
||||
import OllamaForm from './modal/subcomponents/forms/OllamaForm';
|
||||
import OllamaSubmitHandler from './modal/subcomponents/handlers/OllamaSubmitHandler';
|
||||
|
||||
export interface ProviderRegistry {
|
||||
name: string;
|
||||
@@ -47,13 +25,9 @@ export interface ProviderRegistry {
|
||||
* - 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
|
||||
* 2. Configuration submission:
|
||||
*
|
||||
* b) Modal Content - What form appears in the configuration modal
|
||||
* 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
|
||||
@@ -70,14 +44,12 @@ export interface ProviderRegistry {
|
||||
* ---------------------
|
||||
*
|
||||
* For a standard provider with simple configuration:
|
||||
* - Define parameters array with all required fields
|
||||
* - Use the default getActions function
|
||||
* - Define parameters array with all required fields and any defaults that should be supplied
|
||||
* - No need to specify a CustomForm
|
||||
*
|
||||
* For a provider needing custom configuration:
|
||||
* - Define parameters array (even if just for documentation)
|
||||
* - Define parameters array (if needed, otherwise leave as an empty list)
|
||||
* - 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.
|
||||
@@ -95,9 +67,17 @@ export const PROVIDER_REGISTRY: ProviderRegistry[] = [
|
||||
name: 'OPENAI_API_KEY',
|
||||
is_secret: true,
|
||||
},
|
||||
{
|
||||
name: 'OPENAI_HOST',
|
||||
is_secret: false,
|
||||
default: 'https://api.openai.com',
|
||||
},
|
||||
{
|
||||
name: 'OPENAI_BASE_PATH',
|
||||
is_secret: false,
|
||||
default: 'v1/chat/completions',
|
||||
},
|
||||
],
|
||||
getActions: (provider, callbacks, isOnboardingPage) =>
|
||||
getDefaultButtons(provider, callbacks, isOnboardingPage),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -112,8 +92,6 @@ export const PROVIDER_REGISTRY: ProviderRegistry[] = [
|
||||
is_secret: true,
|
||||
},
|
||||
],
|
||||
getActions: (provider, callbacks, isOnboardingPage) =>
|
||||
getDefaultButtons(provider, callbacks, isOnboardingPage),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -128,8 +106,6 @@ export const PROVIDER_REGISTRY: ProviderRegistry[] = [
|
||||
is_secret: true,
|
||||
},
|
||||
],
|
||||
getActions: (provider, callbacks, isOnboardingPage) =>
|
||||
getDefaultButtons(provider, callbacks, isOnboardingPage),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -144,8 +120,6 @@ export const PROVIDER_REGISTRY: ProviderRegistry[] = [
|
||||
is_secret: true,
|
||||
},
|
||||
],
|
||||
getActions: (provider, callbacks, isOnboardingPage) =>
|
||||
getDefaultButtons(provider, callbacks, isOnboardingPage),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -160,8 +134,6 @@ export const PROVIDER_REGISTRY: ProviderRegistry[] = [
|
||||
is_secret: false,
|
||||
},
|
||||
],
|
||||
getActions: (provider, callbacks, isOnboardingPage) =>
|
||||
getDefaultButtons(provider, callbacks, isOnboardingPage),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -176,8 +148,6 @@ export const PROVIDER_REGISTRY: ProviderRegistry[] = [
|
||||
is_secret: true,
|
||||
},
|
||||
],
|
||||
getActions: (provider, callbacks, isOnboardingPage) =>
|
||||
getDefaultButtons(provider, callbacks, isOnboardingPage),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -190,10 +160,50 @@ export const PROVIDER_REGISTRY: ProviderRegistry[] = [
|
||||
{
|
||||
name: 'OLLAMA_HOST',
|
||||
is_secret: false,
|
||||
default: 'localhost',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Azure OpenAI',
|
||||
details: {
|
||||
id: 'azure_openai',
|
||||
name: 'Azure OpenAI',
|
||||
description: 'Access Azure OpenAI models',
|
||||
parameters: [
|
||||
{
|
||||
name: 'AZURE_OPENAI_API_KEY',
|
||||
is_secret: true,
|
||||
},
|
||||
{
|
||||
name: 'AZURE_OPENAI_ENDPOINT',
|
||||
is_secret: false,
|
||||
},
|
||||
{
|
||||
name: 'AZURE_OPENAI_DEPLOYMENT_NAME',
|
||||
is_secret: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'GCP Vertex AI',
|
||||
details: {
|
||||
id: 'gcp_vertex_ai',
|
||||
name: 'GCP Vertex AI',
|
||||
description: 'GCP Vertex AI models',
|
||||
parameters: [
|
||||
{
|
||||
name: 'GCP_PROJECT_ID',
|
||||
is_secret: false,
|
||||
},
|
||||
{
|
||||
name: 'GCP_LOCATION',
|
||||
is_secret: false,
|
||||
default: 'us-central1',
|
||||
},
|
||||
],
|
||||
getActions: (provider, callbacks, isOnboardingPage) =>
|
||||
getDefaultButtons(provider, callbacks, isOnboardingPage),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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} isOnboarding={true} />
|
||||
<ProviderGrid providers={fakeProviderState} isOnboarding={false} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export default interface ParameterSchema {
|
||||
name: string;
|
||||
is_secret: boolean;
|
||||
location?: string; // env, config.yaml, and/or keychain
|
||||
required?: boolean;
|
||||
default?: string; // optional default values
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
// metadata and action builder
|
||||
import ProviderState from './ProviderState';
|
||||
import ConfigurationAction from './ConfigurationAction';
|
||||
import ParameterSchema from '../interfaces/ParameterSchema';
|
||||
import ButtonCallbacks from './ButtonCallbacks';
|
||||
import ProviderSetupFormProps from '../modal/interfaces/ProviderSetupFormProps';
|
||||
|
||||
export default interface ProviderDetails {
|
||||
id: string;
|
||||
@@ -10,9 +7,6 @@ export default interface ProviderDetails {
|
||||
description: string;
|
||||
parameters: ParameterSchema[];
|
||||
getTags?: (name: string) => string[];
|
||||
getActions?: (
|
||||
provider: ProviderState,
|
||||
callbacks: ButtonCallbacks,
|
||||
isOnboardingPage: boolean
|
||||
) => ConfigurationAction[];
|
||||
customForm?: React.ComponentType<ProviderSetupFormProps>;
|
||||
customSubmit?: (e: any) => void;
|
||||
}
|
||||
|
||||
@@ -6,22 +6,24 @@ import ProviderSetupActions from './subcomponents/ProviderSetupActions';
|
||||
import ProviderLogo from './subcomponents/ProviderLogo';
|
||||
import { useProviderModal } from './ProviderModalProvider';
|
||||
import { toast } from 'react-toastify';
|
||||
import { PROVIDER_REGISTRY } from '../ProviderRegistry';
|
||||
import { SecureStorageNotice } from './subcomponents/SecureStorageNotice';
|
||||
import DefaultSubmitHandler from './subcomponents/handlers/DefaultSubmitHandler';
|
||||
|
||||
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 || '';
|
||||
});
|
||||
}
|
||||
// FIXME
|
||||
// if (currentProvider.parameters) {
|
||||
// currentProvider.parameters.forEach((param) => {
|
||||
// initialValues[param.name] = param.default || '';
|
||||
// });
|
||||
// }
|
||||
setConfigValues(initialValues);
|
||||
} else {
|
||||
setConfigValues({});
|
||||
@@ -31,22 +33,31 @@ export default function ProviderConfigurationModal() {
|
||||
if (!isOpen || !currentProvider) return null;
|
||||
|
||||
const headerText = `Configure ${currentProvider.name}`;
|
||||
const descriptionText = `Add your generated api keys for this provider to integrate into Goose`;
|
||||
const descriptionText = `Add your API key(s) for this provider to integrate into Goose`;
|
||||
|
||||
// Use custom form component if provider specifies one, otherwise use default
|
||||
const FormComponent = currentProvider.CustomForm || DefaultProviderSetupForm;
|
||||
// Find the provider in the registry to get the details with customForm
|
||||
const providerEntry = PROVIDER_REGISTRY.find((p) => p.name === currentProvider.name);
|
||||
|
||||
// Get the custom submit handler from the provider details
|
||||
const customSubmitHandler = providerEntry?.details?.customSubmit;
|
||||
|
||||
// Use custom submit handler otherwise use default
|
||||
const SubmitHandler = customSubmitHandler || DefaultSubmitHandler;
|
||||
|
||||
// Get the custom form component from the provider details
|
||||
const CustomForm = providerEntry?.details?.customForm;
|
||||
|
||||
// Use custom form component if available, otherwise use default
|
||||
const FormComponent = CustomForm || DefaultProviderSetupForm;
|
||||
|
||||
const handleSubmitForm = (e) => {
|
||||
e.preventDefault();
|
||||
console.log('Form submitted for:', currentProvider.name);
|
||||
|
||||
// Use custom submit handler if provided in modalProps
|
||||
if (modalProps.onSubmit) {
|
||||
modalProps.onSubmit(configValues);
|
||||
} else {
|
||||
// Default submit behavior
|
||||
toast('Submitted configuration!');
|
||||
}
|
||||
SubmitHandler(configValues);
|
||||
|
||||
// Close the modal unless the custom handler explicitly returns false
|
||||
// This gives custom handlers the ability to keep the modal open if needed
|
||||
closeModal();
|
||||
};
|
||||
|
||||
@@ -72,12 +83,14 @@ export default function ProviderConfigurationModal() {
|
||||
<FormComponent
|
||||
configValues={configValues}
|
||||
setConfigValues={setConfigValues}
|
||||
onSubmit={handleSubmitForm}
|
||||
provider={currentProvider}
|
||||
{...(modalProps.formProps || {})} // Spread any custom form props
|
||||
/>
|
||||
|
||||
<ProviderSetupActions onCancel={handleCancel} />
|
||||
{providerEntry?.details?.parameters && providerEntry.details.parameters.length > 0 && (
|
||||
<SecureStorageNotice />
|
||||
)}
|
||||
<ProviderSetupActions onCancel={handleCancel} onSubmit={handleSubmitForm} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
import React, { createContext, useContext, useState } from 'react';
|
||||
import ProviderState from '../interfaces/ProviderState';
|
||||
|
||||
interface ProviderModalContextType {
|
||||
isOpen: boolean;
|
||||
currentProvider: ProviderState | null;
|
||||
modalProps: any;
|
||||
openModal: (provider: ProviderState, additionalProps: any) => void;
|
||||
closeModal: () => void;
|
||||
}
|
||||
|
||||
const ProviderModalContext = createContext({
|
||||
isOpen: false,
|
||||
@@ -8,7 +17,7 @@ const ProviderModalContext = createContext({
|
||||
closeModal: () => {},
|
||||
});
|
||||
|
||||
export const useProviderModal = () => useContext(ProviderModalContext);
|
||||
export const useProviderModal = () => useContext<ProviderModalContextType>(ProviderModalContext);
|
||||
|
||||
export const ProviderModalProvider = ({ children }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
@@ -3,19 +3,21 @@ import { Button } from '../../../../ui/button';
|
||||
|
||||
interface ProviderSetupActionsProps {
|
||||
onCancel: () => void;
|
||||
onSubmit: (e: any) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the "Submit" and "Cancel" buttons at the bottom.
|
||||
* Updated to match the design from screenshots.
|
||||
*/
|
||||
export default function ProviderSetupActions({ onCancel }: ProviderSetupActionsProps) {
|
||||
export default function ProviderSetupActions({ onCancel, onSubmit }: ProviderSetupActionsProps) {
|
||||
return (
|
||||
<div className="mt-8 -ml-8 -mr-8">
|
||||
{/* We rely on the <form> "onSubmit" for the actual Submit logic */}
|
||||
<Button
|
||||
type="submit"
|
||||
variant="ghost"
|
||||
onClick={onSubmit}
|
||||
className="w-full h-[60px] rounded-none border-t border-borderSubtle text-md hover:bg-bgSubtle text-textProminent font-medium"
|
||||
>
|
||||
Submit
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { Lock } from 'lucide-react';
|
||||
|
||||
/**
|
||||
* SecureStorageNotice - A reusable component that displays a message about secure storage
|
||||
*
|
||||
* @param {Object} props - Component props
|
||||
* @param {string} [props.className] - Optional additional CSS classes
|
||||
* @param {string} [props.message] - Optional custom message (defaults to keys stored in .env)
|
||||
* @returns {JSX.Element} - The secure storage notice component
|
||||
*/
|
||||
export function SecureStorageNotice({
|
||||
className = '',
|
||||
message = 'Keys are stored securely in the keychain',
|
||||
}) {
|
||||
return (
|
||||
<div className={`flex items-start mt-2 text-gray-600 dark:text-gray-300 ${className}`}>
|
||||
<Lock className="w-5 h-5 mt-1" />
|
||||
<span className="text-sm font-light ml-2">{message}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,45 +1,48 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } 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) {
|
||||
export default function DefaultProviderSetupForm({ configValues, setConfigValues, provider }) {
|
||||
const providerEntry = PROVIDER_REGISTRY.find((p) => p.name === provider.name);
|
||||
const parameters: ParameterSchema[] = providerEntry.details.parameters;
|
||||
const parameters = providerEntry?.details?.parameters || [];
|
||||
|
||||
// Initialize default values when the component mounts or provider changes
|
||||
useEffect(() => {
|
||||
const defaultValues = {};
|
||||
parameters.forEach((parameter) => {
|
||||
if (parameter.default !== undefined && !configValues[parameter.name]) {
|
||||
defaultValues[parameter.name] = parameter.default;
|
||||
}
|
||||
});
|
||||
|
||||
// Only update if there are default values to add
|
||||
if (Object.keys(defaultValues).length > 0) {
|
||||
setConfigValues((prev) => ({
|
||||
...prev,
|
||||
...defaultValues,
|
||||
}));
|
||||
}
|
||||
}, [provider.name, parameters, setConfigValues, configValues]);
|
||||
|
||||
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 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}
|
||||
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>
|
||||
</form>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { PROVIDER_REGISTRY } from '../../../ProviderRegistry';
|
||||
import { Input } from '../../../../../ui/input';
|
||||
import React from 'react';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Lock, RefreshCw } from 'lucide-react';
|
||||
import CustomRadio from '../../../../../ui/CustomRadio';
|
||||
|
||||
export default function OllamaForm({ configValues, setConfigValues, provider }) {
|
||||
const providerEntry = PROVIDER_REGISTRY.find((p) => p.name === provider.name);
|
||||
const parameters = providerEntry?.details?.parameters || [];
|
||||
const [isCheckingLocal, setIsCheckingLocal] = useState(false);
|
||||
const [isLocalAvailable, setIsLocalAvailable] = useState(false);
|
||||
|
||||
const handleConnectionTypeChange = (value) => {
|
||||
setConfigValues((prev) => ({
|
||||
...prev,
|
||||
connection_type: value,
|
||||
}));
|
||||
};
|
||||
|
||||
// Function to handle input changes and auto-select/deselect the host radio
|
||||
const handleInputChange = (paramName, value) => {
|
||||
// Update the parameter value
|
||||
setConfigValues((prev) => ({
|
||||
...prev,
|
||||
[paramName]: value,
|
||||
}));
|
||||
|
||||
// If the user is typing, auto-select the host radio button
|
||||
if (value && configValues.connection_type !== 'host') {
|
||||
handleConnectionTypeChange('host');
|
||||
}
|
||||
// If the input becomes empty and the host radio is selected, switch to local if available
|
||||
else if (!value && configValues.connection_type === 'host') {
|
||||
if (isLocalAvailable) {
|
||||
handleConnectionTypeChange('local');
|
||||
}
|
||||
// If local is not available, we keep the host selected but leave the input empty
|
||||
}
|
||||
};
|
||||
|
||||
const checkLocalAvailability = async () => {
|
||||
setIsCheckingLocal(true);
|
||||
|
||||
// Dummy implementation - simulates checking local availability
|
||||
try {
|
||||
console.log('Checking for local Ollama instance...');
|
||||
// Simulate a network request with a delay
|
||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||
|
||||
// Randomly determine if Ollama is available (for demo purposes)
|
||||
const isAvailable = Math.random() > 0.3;
|
||||
setIsLocalAvailable(isAvailable);
|
||||
|
||||
if (isAvailable) {
|
||||
console.log('Local Ollama instance found');
|
||||
// Enable local radio button
|
||||
} else {
|
||||
console.log('No local Ollama instance found');
|
||||
// If current selection is local, switch to host
|
||||
if (configValues.connection_type === 'local') {
|
||||
handleConnectionTypeChange('host');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking for local Ollama:', error);
|
||||
setIsLocalAvailable(false);
|
||||
} finally {
|
||||
setIsCheckingLocal(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Check local availability on initial load
|
||||
useEffect(() => {
|
||||
checkLocalAvailability();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="font-medium text-gray-900 dark:text-gray-100 mb-2">Connection</div>
|
||||
|
||||
{/* Local Option */}
|
||||
<div className="flex items-center mb-3 justify-between">
|
||||
<div className="flex items-center">
|
||||
<span className="text-gray-700 dark:text-gray-300">Background App</span>
|
||||
<button
|
||||
type="button"
|
||||
className="ml-2 p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
onClick={checkLocalAvailability}
|
||||
disabled={isCheckingLocal}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`w-4 h-4 ${isCheckingLocal ? 'animate-spin' : ''} text-gray-600 dark:text-gray-400`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<CustomRadio
|
||||
id="connection-local"
|
||||
name="connection_type"
|
||||
value="local"
|
||||
checked={configValues.connection_type === 'local'}
|
||||
onChange={() => handleConnectionTypeChange('local')}
|
||||
disabled={!isLocalAvailable}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Other Parameters */}
|
||||
{parameters
|
||||
.filter((param) => param.name !== 'host_url') // Skip host_url as we handle it above
|
||||
.map((parameter) => (
|
||||
<div key={parameter.name} className="flex items-center mb-4">
|
||||
<div className="flex-grow">
|
||||
<Input
|
||||
type={parameter.is_secret ? 'password' : 'text'}
|
||||
value={configValues[parameter.name] || ''}
|
||||
onChange={(e) => handleInputChange(parameter.name, e.target.value)}
|
||||
placeholder={
|
||||
parameter.default ? parameter.default : parameter.name.replace(/_/g, ' ')
|
||||
}
|
||||
className="w-full h-14 px-4 font-regular rounded-lg border shadow-none border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 text-lg placeholder:text-gray-400 dark:placeholder:text-gray-500 font-regular text-gray-900 dark:text-gray-100"
|
||||
required={parameter.default == null}
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<CustomRadio
|
||||
id={`connection-host-${parameter.name}`}
|
||||
name="connection_type"
|
||||
value="host"
|
||||
checked={configValues.connection_type === 'host'}
|
||||
onChange={() => handleConnectionTypeChange('host')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export default function DefaultSubmitHandler(configValues) {
|
||||
// Log each field value individually for clarity
|
||||
console.log('Field values:');
|
||||
Object.entries(configValues).forEach(([key, value]) => {
|
||||
console.log(`${key}: ${value}`);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export default function OllamaSubmitHandler(configValues) {
|
||||
// Log each field value individually for clarity
|
||||
console.log('Ollama field values:');
|
||||
Object.entries(configValues).forEach(([key, value]) => {
|
||||
console.log(`${key}: ${value}`);
|
||||
});
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import React, { createContext, useContext, useState, ReactNode, useEffect } from 'react';
|
||||
import { getActiveProviders } from './utils';
|
||||
|
||||
// Create a context for active keys
|
||||
const ActiveKeysContext = createContext<
|
||||
| {
|
||||
activeKeys: string[];
|
||||
setActiveKeys: (keys: string[]) => void;
|
||||
}
|
||||
| undefined
|
||||
>(undefined);
|
||||
|
||||
export const ActiveKeysProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [activeKeys, setActiveKeys] = useState<string[]>([]); // Start with an empty list
|
||||
const [isLoading, setIsLoading] = useState(true); // Track loading state
|
||||
|
||||
// Fetch active keys from the backend
|
||||
useEffect(() => {
|
||||
const fetchActiveProviders = async () => {
|
||||
try {
|
||||
const providers = await getActiveProviders(); // Fetch the active providers
|
||||
setActiveKeys(providers); // Update state with fetched providers
|
||||
} catch (error) {
|
||||
console.error('Error fetching active providers:', error);
|
||||
} finally {
|
||||
setIsLoading(false); // Ensure loading is marked as complete
|
||||
}
|
||||
};
|
||||
|
||||
fetchActiveProviders(); // Call the async function
|
||||
}, []);
|
||||
|
||||
// Provide active keys and ability to update them
|
||||
return (
|
||||
<ActiveKeysContext.Provider value={{ activeKeys, setActiveKeys }}>
|
||||
{!isLoading ? children : <div>Loading...</div>} {/* Conditional rendering */}
|
||||
</ActiveKeysContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
// Custom hook to access active keys
|
||||
export const useActiveKeys = () => {
|
||||
const context = useContext(ActiveKeysContext);
|
||||
if (!context) {
|
||||
throw new Error('useActiveKeys must be used within an ActiveKeysProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
export interface ProviderResponse {
|
||||
supported: boolean;
|
||||
name?: string;
|
||||
description?: string;
|
||||
models?: string[];
|
||||
config_status: Record<string, ConfigDetails>;
|
||||
}
|
||||
|
||||
export interface ConfigDetails {
|
||||
key: string;
|
||||
is_set: boolean;
|
||||
location?: string;
|
||||
}
|
||||
|
||||
export interface Provider {
|
||||
id: string; // Lowercase key (e.g., "openai")
|
||||
name: string; // Provider name (e.g., "OpenAI")
|
||||
description: string; // Description of the provider
|
||||
models: string[]; // List of supported models
|
||||
requiredKeys: string[]; // List of required keys
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
import { Provider, ProviderResponse } from './types';
|
||||
import { getApiUrl, getSecretKey } from '../../../config';
|
||||
import { special_provider_cases } from '../providers/utils';
|
||||
|
||||
export function isSecretKey(keyName: string): boolean {
|
||||
// Endpoints and hosts should not be stored as secrets
|
||||
const nonSecretKeys = [
|
||||
'DATABRICKS_HOST',
|
||||
'OLLAMA_HOST',
|
||||
'AZURE_OPENAI_ENDPOINT',
|
||||
'AZURE_OPENAI_DEPLOYMENT_NAME',
|
||||
];
|
||||
return !nonSecretKeys.includes(keyName);
|
||||
}
|
||||
|
||||
export async function getActiveProviders(): Promise<string[]> {
|
||||
try {
|
||||
// Fetch the secrets settings
|
||||
const configSettings = await getConfigSettings();
|
||||
console.log('[getActiveProviders]:', configSettings);
|
||||
|
||||
// Check for special provider cases (e.g. ollama running locally)
|
||||
const specialCasesResults = await Promise.all(
|
||||
Object.entries(special_provider_cases).map(async ([providerName, checkFunction]) => {
|
||||
const isActive = await checkFunction(); // Dynamically re-check status
|
||||
console.log(`Special case result for ${providerName}:`, isActive);
|
||||
return isActive ? providerName : null;
|
||||
})
|
||||
);
|
||||
// Extract active providers based on `is_set` in `secret_status` or providers with no keys
|
||||
const activeProviders = Object.values(configSettings) // Convert object to array
|
||||
.filter((provider) => {
|
||||
const apiKeyStatus = Object.values(provider.config_status || {}); // Get all key statuses
|
||||
|
||||
// Include providers if all required keys are set
|
||||
return apiKeyStatus.length > 0 && apiKeyStatus.every((key) => key.is_set);
|
||||
})
|
||||
.map((provider) => provider.name || 'Unknown Provider'); // Extract provider name
|
||||
|
||||
// Combine active providers from secrets settings and special cases (avoiding repeats)
|
||||
const allActiveProviders = activeProviders.concat(
|
||||
specialCasesResults.filter(
|
||||
(provider) => provider !== null && !activeProviders.includes(provider)
|
||||
)
|
||||
);
|
||||
|
||||
return allActiveProviders;
|
||||
} catch (error) {
|
||||
console.error('Failed to get active providers:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function getConfigSettings(): Promise<Record<string, ProviderResponse>> {
|
||||
const providerList = await getProvidersList();
|
||||
// Extract the list of IDs
|
||||
const providerIds = providerList.map((provider) => provider.id);
|
||||
|
||||
// Fetch configs state (set/unset) using the provider IDs
|
||||
const response = await fetch(getApiUrl('/configs/providers'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Secret-Key': getSecretKey(),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
providers: providerIds,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch secrets');
|
||||
}
|
||||
|
||||
const data = (await response.json()) as Record<string, ProviderResponse>;
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getProvidersList(): Promise<Provider[]> {
|
||||
const response = await fetch(getApiUrl('/agent/providers'), {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch providers: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Format the response into an array of providers
|
||||
return data.map((item: any) => ({
|
||||
id: item.id, // Root-level ID
|
||||
name: item.details?.name || 'Unknown Provider', // Nested name in details
|
||||
description: item.details?.description || 'No description available.', // Nested description
|
||||
models: item.details?.models || [], // Nested models array
|
||||
requiredKeys: item.details?.required_keys || [], // Nested required keys array
|
||||
}));
|
||||
}
|
||||
@@ -3,13 +3,9 @@ import CardActions from './CardActions';
|
||||
import ConfigurationAction from '../interfaces/ConfigurationAction';
|
||||
|
||||
interface CardBodyProps {
|
||||
actions: ConfigurationAction[];
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function CardBody({ actions }: CardBodyProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-start">
|
||||
<CardActions actions={actions} />
|
||||
</div>
|
||||
);
|
||||
export default function CardBody({ children }: CardBodyProps) {
|
||||
return <div className="flex items-center justify-start">{children}</div>;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ interface ProviderNameAndStatusProps {
|
||||
|
||||
function ProviderNameAndStatus({ name, isConfigured }: ProviderNameAndStatusProps) {
|
||||
console.log(`Provider Name: ${name}, Is Configured: ${isConfigured}`);
|
||||
const ollamaNotConfigured = !isConfigured && name === 'Ollama';
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between w-full">
|
||||
@@ -33,9 +32,6 @@ function ProviderNameAndStatus({ name, isConfigured }: ProviderNameAndStatusProp
|
||||
|
||||
{/* Configured state: Green check */}
|
||||
{isConfigured && <GreenCheckButton tooltip={ConfiguredProviderTooltipMessage(name)} />}
|
||||
|
||||
{/* Not Configured + Ollama => Exclamation */}
|
||||
{ollamaNotConfigured && <ExclamationButton tooltip={OllamaNotConfiguredTooltipMessage()} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,49 +3,84 @@ import CardContainer from './CardContainer';
|
||||
import CardHeader from './CardHeader';
|
||||
import ProviderState from '../interfaces/ProviderState';
|
||||
import CardBody from './CardBody';
|
||||
import ButtonCallbacks from '../interfaces/ButtonCallbacks';
|
||||
import { PROVIDER_REGISTRY } from '../ProviderRegistry';
|
||||
import DefaultCardButtons from './buttons/DefaultCardButtons';
|
||||
|
||||
interface ProviderCardProps {
|
||||
type ProviderCardProps = {
|
||||
provider: ProviderState;
|
||||
buttonCallbacks: ButtonCallbacks;
|
||||
onConfigure: () => void;
|
||||
onLaunch: () => void;
|
||||
isOnboarding: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export function ProviderCard({ provider, buttonCallbacks, isOnboarding }: ProviderCardProps) {
|
||||
// export function ProviderCard({ provider, buttonCallbacks, isOnboarding }: ProviderCardProps) {
|
||||
// const providerEntry = PROVIDER_REGISTRY.find((p) => p.name === provider.name);
|
||||
//
|
||||
// // Add safety check
|
||||
// if (!providerEntry) {
|
||||
// console.error(`Provider ${provider.name} not found in registry`);
|
||||
// return null;
|
||||
// }
|
||||
//
|
||||
// const providerDetails = providerEntry.details;
|
||||
// // Add another safety check
|
||||
// if (!providerDetails) {
|
||||
// console.error(`Provider ${provider.name} has no details`);
|
||||
// return null;
|
||||
// }
|
||||
// console.log('provider details', providerDetails);
|
||||
//
|
||||
// try {
|
||||
// const actions = providerDetails.getActions(provider, buttonCallbacks, isOnboarding);
|
||||
//
|
||||
// return (
|
||||
// <CardContainer
|
||||
// header={
|
||||
// <CardHeader
|
||||
// name={providerDetails.name}
|
||||
// description={providerDetails.description}
|
||||
// isConfigured={provider.isConfigured}
|
||||
// />
|
||||
// }
|
||||
// body={<CardBody actions={actions} />}
|
||||
// />
|
||||
// );
|
||||
// } catch (error) {
|
||||
// console.error(`Error rendering provider card for ${provider.name}:`, error);
|
||||
// return null;
|
||||
// }
|
||||
// }
|
||||
|
||||
export function ProviderCard({ provider, onConfigure, onLaunch, isOnboarding }: ProviderCardProps) {
|
||||
const providerEntry = PROVIDER_REGISTRY.find((p) => p.name === provider.name);
|
||||
|
||||
// Add safety check
|
||||
if (!providerEntry) {
|
||||
console.error(`Provider ${provider.name} not found in registry`);
|
||||
if (!providerEntry?.details) {
|
||||
console.error(`Provider ${provider.name} not found in registry or has no details`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const providerDetails = providerEntry.details;
|
||||
// Add another safety check
|
||||
if (!providerDetails) {
|
||||
console.error(`Provider ${provider.name} has no details`);
|
||||
return null;
|
||||
}
|
||||
console.log('provider details', providerDetails);
|
||||
|
||||
try {
|
||||
const actions = providerDetails.getActions(provider, buttonCallbacks, isOnboarding);
|
||||
|
||||
return (
|
||||
<CardContainer
|
||||
header={
|
||||
<CardHeader
|
||||
name={providerDetails.name}
|
||||
description={providerDetails.description}
|
||||
isConfigured={provider.isConfigured}
|
||||
return (
|
||||
<CardContainer
|
||||
header={
|
||||
<CardHeader
|
||||
name={providerDetails.name}
|
||||
description={providerDetails.description}
|
||||
isConfigured={provider.isConfigured}
|
||||
/>
|
||||
}
|
||||
body={
|
||||
<CardBody>
|
||||
<DefaultCardButtons
|
||||
provider={provider}
|
||||
onConfigure={onConfigure}
|
||||
onLaunch={onLaunch}
|
||||
isOnboardingPage={isOnboarding}
|
||||
/>
|
||||
}
|
||||
body={<CardBody actions={actions} />}
|
||||
/>
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`Error rendering provider card for ${provider.name}:`, error);
|
||||
return null;
|
||||
}
|
||||
</CardBody>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
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
|
||||
isOnboardingPage: boolean;
|
||||
onConfigure: (provider: ProviderState) => void;
|
||||
onLaunch: (provider: ProviderState) => void;
|
||||
}
|
||||
|
||||
function getDefaultTooltipMessages(name: string, actionType: string) {
|
||||
@@ -23,23 +23,11 @@ function getDefaultTooltipMessages(name: string, actionType: string) {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
onLaunch,
|
||||
onConfigure,
|
||||
}: CardButtonsProps) {
|
||||
return (
|
||||
<>
|
||||
@@ -49,7 +37,7 @@ export default function DefaultCardButtons({
|
||||
tooltip={getDefaultTooltipMessages(provider.name, 'add')}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
callbacks.onConfigure(provider);
|
||||
onConfigure(provider);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -59,7 +47,7 @@ export default function DefaultCardButtons({
|
||||
tooltip={getDefaultTooltipMessages(provider.name, 'edit')}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
callbacks.onConfigure(provider);
|
||||
onConfigure(provider);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -68,7 +56,7 @@ export default function DefaultCardButtons({
|
||||
<RocketButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
callbacks.onLaunch(provider);
|
||||
onLaunch(provider);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
72
ui/desktop/src/components/ui/CustomRadio.tsx
Normal file
72
ui/desktop/src/components/ui/CustomRadio.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* CustomRadio - A reusable radio button component with dark mode support
|
||||
* @param {Object} props - Component props
|
||||
* @param {string} props.id - Unique identifier for the radio input
|
||||
* @param {string} props.name - Name attribute for the radio input
|
||||
* @param {string} props.value - Value of the radio input
|
||||
* @param {boolean} props.checked - Whether the radio is checked
|
||||
* @param {function} props.onChange - Function to call when radio selection changes
|
||||
* @param {boolean} [props.disabled] - Whether the radio is disabled
|
||||
* @param {React.ReactNode} [props.label] - Primary label content
|
||||
* @param {React.ReactNode} [props.secondaryLabel] - Secondary/subtitle label content
|
||||
* @param {React.ReactNode} [props.rightContent] - Optional content to display on the right side
|
||||
* @param {string} [props.className] - Additional CSS classes for the main container
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const CustomRadio = ({
|
||||
id,
|
||||
name,
|
||||
value,
|
||||
checked,
|
||||
onChange,
|
||||
disabled = false,
|
||||
label = null,
|
||||
secondaryLabel = null,
|
||||
rightContent = null,
|
||||
className = '',
|
||||
}) => {
|
||||
return (
|
||||
<label
|
||||
htmlFor={id}
|
||||
className={`flex justify-between items-center py-2 cursor-pointer ${disabled ? 'opacity-50 cursor-not-allowed' : ''} ${className}`}
|
||||
>
|
||||
<div className="relative flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
id={id}
|
||||
name={name}
|
||||
value={value}
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<div
|
||||
className="h-4 w-4 rounded-full border border-gray-400 dark:border-gray-500 mr-4
|
||||
peer-checked:border-[6px] peer-checked:border-black dark:peer-checked:border-white
|
||||
peer-checked:bg-white dark:peer-checked:bg-black
|
||||
transition-all duration-200 ease-in-out"
|
||||
></div>
|
||||
|
||||
{(label || secondaryLabel) && (
|
||||
<div>
|
||||
{label && <p className="text-sm text-gray-900 dark:text-gray-100">{label}</p>}
|
||||
{secondaryLabel && (
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">{secondaryLabel}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{rightContent && (
|
||||
<div className="flex items-center text-sm text-gray-500 dark:text-gray-400">
|
||||
{rightContent}
|
||||
</div>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomRadio;
|
||||
Reference in New Issue
Block a user