Custom providers update (#4099)
Co-authored-by: developerayo <shodipovi@gmail.com> Co-authored-by: Douwe Osinga <douwe@squareup.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Zane Staggs <zane@squareup.com>
This commit is contained in:
@@ -170,9 +170,15 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
|
||||
const getProviders = useCallback(
|
||||
async (forceRefresh = false): Promise<ProviderDetails[]> => {
|
||||
if (forceRefresh || providersList.length === 0) {
|
||||
const response = await providers();
|
||||
setProvidersList(response.data || []);
|
||||
return response.data || [];
|
||||
try {
|
||||
const response = await providers();
|
||||
const providersData = response.data || [];
|
||||
setProvidersList(providersData);
|
||||
return providersData;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch providers:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return providersList;
|
||||
},
|
||||
@@ -189,9 +195,11 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
|
||||
// Load providers
|
||||
try {
|
||||
const providersResponse = await providers();
|
||||
setProvidersList(providersResponse.data || []);
|
||||
const providersData = providersResponse.data || [];
|
||||
setProvidersList(providersData);
|
||||
} catch (error) {
|
||||
console.error('Failed to load providers:', error);
|
||||
setProvidersList([]);
|
||||
}
|
||||
|
||||
// Load extensions
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { screen, waitFor } from '@testing-library/dom';
|
||||
import MarkdownContent from './MarkdownContent';
|
||||
|
||||
// Mock the icons to avoid import issues
|
||||
|
||||
@@ -159,11 +159,14 @@ export const AddModelModal = ({ onClose, setView }: AddModelModalProps) => {
|
||||
|
||||
// Add the "Custom model" option to each provider group
|
||||
formattedModelOptions.forEach((group) => {
|
||||
group.options.push({
|
||||
value: 'custom',
|
||||
label: 'Use custom model',
|
||||
provider: group.options[0]?.provider,
|
||||
});
|
||||
const providerName = group.options[0]?.provider;
|
||||
if (providerName && !providerName.startsWith('custom_')) {
|
||||
group.options.push({
|
||||
value: 'custom',
|
||||
label: 'Use custom model',
|
||||
provider: providerName,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
setModelOptions(formattedModelOptions);
|
||||
|
||||
@@ -45,7 +45,7 @@ export default function PermissionModal({ extensionName, onClose }: PermissionMo
|
||||
console.error('Failed to get tools');
|
||||
} else {
|
||||
const filteredTools = (response.data || []).filter(
|
||||
(tool) =>
|
||||
(tool: ToolInfo) =>
|
||||
tool.name !== 'platform__read_resource' && tool.name !== 'platform__list_resources'
|
||||
);
|
||||
setTools(filteredTools);
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import React, { memo, useMemo, useCallback } from 'react';
|
||||
import React, { memo, useMemo, useCallback, useState } from 'react';
|
||||
import { ProviderCard } from './subcomponents/ProviderCard';
|
||||
import CardContainer from './subcomponents/CardContainer';
|
||||
import { ProviderModalProvider, useProviderModal } from './modal/ProviderModalProvider';
|
||||
import ProviderConfigurationModal from './modal/ProviderConfiguationModal';
|
||||
import { ProviderDetails } from '../../../api';
|
||||
import { ProviderDetails, CreateCustomProviderRequest } from '../../../api';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../../ui/dialog';
|
||||
import CustomProviderForm from './modal/subcomponents/forms/CustomProviderForm';
|
||||
|
||||
const GridLayout = memo(function GridLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
@@ -18,6 +22,27 @@ const GridLayout = memo(function GridLayout({ children }: { children: React.Reac
|
||||
);
|
||||
});
|
||||
|
||||
const CustomProviderCard = memo(function CustomProviderCard({ onClick }: { onClick: () => void }) {
|
||||
return (
|
||||
<CardContainer
|
||||
testId="add-custom-provider-card"
|
||||
onClick={onClick}
|
||||
header={null}
|
||||
body={
|
||||
<div className="flex flex-col items-center justify-center min-h-[200px]">
|
||||
<Plus className="w-8 h-8 text-gray-400 mb-2" />
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 text-center">
|
||||
<div>Add</div>
|
||||
<div>Custom Provider</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
grayedOut={false}
|
||||
borderStyle="dashed"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
// Memoize the ProviderCards component
|
||||
const ProviderCards = memo(function ProviderCards({
|
||||
providers,
|
||||
@@ -31,6 +56,7 @@ const ProviderCards = memo(function ProviderCards({
|
||||
onProviderLaunch: (provider: ProviderDetails) => void;
|
||||
}) {
|
||||
const { openModal } = useProviderModal();
|
||||
const [showCustomProviderModal, setShowCustomProviderModal] = useState(false);
|
||||
|
||||
// Memoize these functions so they don't get recreated on every render
|
||||
const configureProviderViaModal = useCallback(
|
||||
@@ -42,7 +68,7 @@ const ProviderCards = memo(function ProviderCards({
|
||||
refreshProviders();
|
||||
}
|
||||
},
|
||||
onDelete: () => {
|
||||
onDelete: (_values: unknown) => {
|
||||
if (refreshProviders) {
|
||||
refreshProviders();
|
||||
}
|
||||
@@ -56,7 +82,7 @@ const ProviderCards = memo(function ProviderCards({
|
||||
const deleteProviderConfigViaModal = useCallback(
|
||||
(provider: ProviderDetails) => {
|
||||
openModal(provider, {
|
||||
onDelete: () => {
|
||||
onDelete: (_values: unknown) => {
|
||||
// Only refresh if the function is provided
|
||||
if (refreshProviders) {
|
||||
refreshProviders();
|
||||
@@ -68,12 +94,27 @@ const ProviderCards = memo(function ProviderCards({
|
||||
[openModal, refreshProviders]
|
||||
);
|
||||
|
||||
// We don't need an intermediate function here
|
||||
// Just pass the onProviderLaunch directly
|
||||
const handleCreateCustomProvider = useCallback(
|
||||
async (data: CreateCustomProviderRequest) => {
|
||||
try {
|
||||
const { createCustomProvider } = await import('../../../api');
|
||||
await createCustomProvider({ body: data });
|
||||
setShowCustomProviderModal(false);
|
||||
if (refreshProviders) {
|
||||
refreshProviders();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to create custom provider:', error);
|
||||
}
|
||||
},
|
||||
[refreshProviders]
|
||||
);
|
||||
|
||||
// Use useMemo to memoize the cards array
|
||||
const providerCards = useMemo(() => {
|
||||
return providers.map((provider) => (
|
||||
// providers needs to be an array
|
||||
const providersArray = Array.isArray(providers) ? providers : [];
|
||||
const cards = providersArray.map((provider) => (
|
||||
<ProviderCard
|
||||
key={provider.name}
|
||||
provider={provider}
|
||||
@@ -83,6 +124,12 @@ const ProviderCards = memo(function ProviderCards({
|
||||
isOnboarding={isOnboarding}
|
||||
/>
|
||||
));
|
||||
|
||||
cards.push(
|
||||
<CustomProviderCard key="add-custom" onClick={() => setShowCustomProviderModal(true)} />
|
||||
);
|
||||
|
||||
return cards;
|
||||
}, [
|
||||
providers,
|
||||
isOnboarding,
|
||||
@@ -91,7 +138,23 @@ const ProviderCards = memo(function ProviderCards({
|
||||
onProviderLaunch,
|
||||
]);
|
||||
|
||||
return <>{providerCards}</>;
|
||||
return (
|
||||
<>
|
||||
{providerCards}
|
||||
|
||||
<Dialog open={showCustomProviderModal} onOpenChange={setShowCustomProviderModal}>
|
||||
<DialogContent className="sm:max-w-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add Custom Provider</DialogTitle>
|
||||
</DialogHeader>
|
||||
<CustomProviderForm
|
||||
onSubmit={handleCreateCustomProvider}
|
||||
onCancel={() => setShowCustomProviderModal(false)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default memo(function ProviderGrid({
|
||||
|
||||
@@ -18,7 +18,7 @@ import OllamaForm from './subcomponents/forms/OllamaForm';
|
||||
import { useConfig } from '../../../ConfigContext';
|
||||
import { useModelAndProvider } from '../../../ModelAndProviderContext';
|
||||
import { AlertTriangle } from 'lucide-react';
|
||||
import { ConfigKey } from '../../../../api';
|
||||
import { ConfigKey, removeCustomProvider } from '../../../../api';
|
||||
|
||||
interface FormValues {
|
||||
[key: string]: string | number | boolean | null;
|
||||
@@ -162,13 +162,21 @@ export default function ProviderConfigurationModal() {
|
||||
}
|
||||
|
||||
try {
|
||||
// Remove the provider configuration
|
||||
// get the keys
|
||||
const params = currentProvider.metadata.config_keys;
|
||||
const isCustomProvider = currentProvider.name.startsWith('custom_');
|
||||
|
||||
// go through the keys are remove them
|
||||
for (const param of params) {
|
||||
await remove(param.name, param.secret);
|
||||
if (isCustomProvider) {
|
||||
await removeCustomProvider({
|
||||
path: { id: currentProvider.name },
|
||||
});
|
||||
} else {
|
||||
// Remove the provider configuration
|
||||
// get the keys
|
||||
const params = currentProvider.metadata.config_keys;
|
||||
|
||||
// go through the keys are remove them
|
||||
for (const param of params) {
|
||||
await remove(param.name, param.secret);
|
||||
}
|
||||
}
|
||||
|
||||
// Call onDelete callback if provided
|
||||
@@ -247,9 +255,9 @@ export default function ProviderConfigurationModal() {
|
||||
setShowDeleteConfirmation(false);
|
||||
setIsActiveProvider(false);
|
||||
}}
|
||||
canDelete={isConfigured && !isActiveProvider} // Disable delete button for active provider
|
||||
canDelete={isConfigured && !isActiveProvider}
|
||||
providerName={currentProvider.metadata.display_name}
|
||||
isActiveProvider={isActiveProvider} // Pass this to actions for button state
|
||||
isActiveProvider={isActiveProvider}
|
||||
/>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Input } from '../../../../../ui/input';
|
||||
import { Select } from '../../../../../ui/Select';
|
||||
import { Button } from '../../../../../ui/button';
|
||||
import { SecureStorageNotice } from '../SecureStorageNotice';
|
||||
import { Checkbox } from '@radix-ui/themes';
|
||||
|
||||
interface CustomProviderFormProps {
|
||||
onSubmit: (data: {
|
||||
provider_type: string;
|
||||
display_name: string;
|
||||
api_url: string;
|
||||
api_key: string;
|
||||
models: string[];
|
||||
supports_streaming: boolean;
|
||||
}) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export default function CustomProviderForm({ onSubmit, onCancel }: CustomProviderFormProps) {
|
||||
const [providerType, setProviderType] = useState('openai_compatible');
|
||||
const [displayName, setDisplayName] = useState('');
|
||||
const [apiUrl, setApiUrl] = useState('');
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
const [models, setModels] = useState('');
|
||||
const [isLocalModel, setIsLocalModel] = useState(false);
|
||||
const [supportsStreaming, setSupportsStreaming] = useState(true);
|
||||
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
|
||||
|
||||
const handleLocalModels = (checked: boolean) => {
|
||||
setIsLocalModel(checked);
|
||||
if (checked) {
|
||||
setApiKey('notrequired');
|
||||
} else {
|
||||
setApiKey('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const errors: Record<string, string> = {};
|
||||
if (!displayName) errors.displayName = 'Display name is required';
|
||||
if (!apiUrl) errors.apiUrl = 'API URL is required';
|
||||
if (!isLocalModel && !apiKey) errors.apiKey = 'API key is required';
|
||||
if (!models) errors.models = 'At least one model is required';
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
setValidationErrors(errors);
|
||||
return;
|
||||
}
|
||||
|
||||
const modelList = models
|
||||
.split(',')
|
||||
.map((m) => m.trim())
|
||||
.filter((m) => m);
|
||||
|
||||
onSubmit({
|
||||
provider_type: providerType,
|
||||
display_name: displayName,
|
||||
api_url: apiUrl,
|
||||
api_key: apiKey,
|
||||
models: modelList,
|
||||
supports_streaming: supportsStreaming,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="mt-4 space-y-4">
|
||||
<div>
|
||||
<label className="flex items-center text-sm font-medium text-white mb-1">
|
||||
Provider Type
|
||||
<span className="text-red-500 ml-1">*</span>
|
||||
</label>
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'openai_compatible', label: 'OpenAI Compatible' },
|
||||
{ value: 'anthropic_compatible', label: 'Anthropic Compatible' },
|
||||
{ value: 'ollama_compatible', label: 'Ollama Compatible' },
|
||||
]}
|
||||
value={{
|
||||
value: providerType,
|
||||
label:
|
||||
providerType === 'openai_compatible'
|
||||
? 'OpenAI Compatible'
|
||||
: providerType === 'anthropic_compatible'
|
||||
? 'Anthropic Compatible'
|
||||
: 'Ollama Compatible',
|
||||
}}
|
||||
onChange={(option: unknown) => {
|
||||
const selectedOption = option as { value: string; label: string } | null;
|
||||
if (selectedOption) setProviderType(selectedOption.value);
|
||||
}}
|
||||
isSearchable={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="flex items-center text-sm font-medium text-white mb-1">
|
||||
Display Name
|
||||
<span className="text-red-500 ml-1">*</span>
|
||||
</label>
|
||||
<Input
|
||||
value={displayName}
|
||||
onChange={(e) => setDisplayName(e.target.value)}
|
||||
placeholder="Your Provider Name"
|
||||
className={validationErrors.displayName ? 'border-red-500' : ''}
|
||||
/>
|
||||
{validationErrors.displayName && (
|
||||
<p className="text-red-500 text-sm mt-1">{validationErrors.displayName}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="flex items-center text-sm font-medium text-white mb-1">
|
||||
API URL
|
||||
<span className="text-red-500 ml-1">*</span>
|
||||
</label>
|
||||
<Input
|
||||
value={apiUrl}
|
||||
onChange={(e) => setApiUrl(e.target.value)}
|
||||
placeholder="https://api.example.com/v1/messages"
|
||||
className={validationErrors.apiUrl ? 'border-red-500' : ''}
|
||||
/>
|
||||
{validationErrors.apiUrl && (
|
||||
<p className="text-red-500 text-sm mt-1">{validationErrors.apiUrl}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="flex items-center text-sm font-medium text-white mb-1">
|
||||
API Key
|
||||
{!isLocalModel && <span className="text-red-500 ml-1">*</span>}
|
||||
</label>
|
||||
<Input
|
||||
type="password"
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
placeholder="Your API key"
|
||||
className={validationErrors.apiKey ? 'border-red-500' : ''}
|
||||
disabled={isLocalModel}
|
||||
/>
|
||||
{validationErrors.apiKey && (
|
||||
<p className="text-red-500 text-sm mt-1">{validationErrors.apiKey}</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center space-x-2 mt-2">
|
||||
<Checkbox id="local-model" checked={isLocalModel} onCheckedChange={handleLocalModels} />
|
||||
<label
|
||||
htmlFor="local-model"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-gray-400"
|
||||
>
|
||||
This is a local model (no auth required)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="flex items-center text-sm font-medium text-white mb-1">
|
||||
Available Models (comma-separated)
|
||||
<span className="text-red-500 ml-1">*</span>
|
||||
</label>
|
||||
<Input
|
||||
value={models}
|
||||
onChange={(e) => setModels(e.target.value)}
|
||||
placeholder="model-a, model-b, model-c"
|
||||
className={validationErrors.models ? 'border-red-500' : ''}
|
||||
/>
|
||||
{validationErrors.models && (
|
||||
<p className="text-red-500 text-sm mt-1">{validationErrors.models}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 mb-10">
|
||||
<Checkbox
|
||||
id="supports-streaming"
|
||||
checked={supportsStreaming}
|
||||
onCheckedChange={(checked) => setSupportsStreaming(checked as boolean)}
|
||||
/>
|
||||
<label
|
||||
htmlFor="supports-streaming"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-gray-400"
|
||||
>
|
||||
Provider supports streaming responses
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<SecureStorageNotice />
|
||||
|
||||
<div className="flex justify-end space-x-2 pt-4">
|
||||
<Button type="button" variant="outline" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit">Create Provider</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
+63
-36
@@ -3,9 +3,7 @@ import { Input } from '../../../../../ui/input';
|
||||
import { useConfig } from '../../../../../ConfigContext'; // Adjust this import path as needed
|
||||
import { ProviderDetails, ConfigKey } from '../../../../../../api';
|
||||
|
||||
interface ValidationErrors {
|
||||
[key: string]: string;
|
||||
}
|
||||
type ValidationErrors = Record<string, string>;
|
||||
|
||||
interface DefaultProviderSetupFormProps {
|
||||
configValues: Record<string, string>;
|
||||
@@ -36,33 +34,30 @@ export default function DefaultProviderSetupForm({
|
||||
|
||||
// Try to load actual values from config for each parameter that is not secret
|
||||
for (const parameter of parameters) {
|
||||
if (parameter.required) {
|
||||
try {
|
||||
// Check if there's a stored value in the config system
|
||||
const configKey = `${parameter.name}`;
|
||||
const configResponse = await read(configKey, parameter.secret || false);
|
||||
try {
|
||||
// Check if there's a stored value in the config system
|
||||
const configKey = `${parameter.name}`;
|
||||
const configResponse = await read(configKey, parameter.secret || false);
|
||||
|
||||
if (configResponse) {
|
||||
// Use the value from the config provider
|
||||
newValues[parameter.name] = String(configResponse);
|
||||
} else if (
|
||||
parameter.default !== undefined &&
|
||||
parameter.default !== null &&
|
||||
!configValues[parameter.name]
|
||||
) {
|
||||
// Fall back to default value if no config value exists
|
||||
newValues[parameter.name] = String(parameter.default);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to load config for ${parameter.name}:`, error);
|
||||
// Fall back to default if read operation fails
|
||||
if (
|
||||
parameter.default !== undefined &&
|
||||
parameter.default !== null &&
|
||||
!configValues[parameter.name]
|
||||
) {
|
||||
newValues[parameter.name] = String(parameter.default);
|
||||
}
|
||||
if (configResponse) {
|
||||
newValues[parameter.name] = parameter.secret ? 'true' : String(configResponse);
|
||||
} else if (
|
||||
parameter.default !== undefined &&
|
||||
parameter.default !== null &&
|
||||
!configValues[parameter.name]
|
||||
) {
|
||||
// Fall back to default value if no config value exists
|
||||
newValues[parameter.name] = String(parameter.default);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to load config for ${parameter.name}:`, error);
|
||||
// Fall back to default if read operation fails
|
||||
if (
|
||||
parameter.default !== undefined &&
|
||||
parameter.default !== null &&
|
||||
!configValues[parameter.name]
|
||||
) {
|
||||
newValues[parameter.name] = String(parameter.default);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,6 +80,11 @@ export default function DefaultProviderSetupForm({
|
||||
return parameters.filter((param) => param.required === true);
|
||||
}, [parameters]);
|
||||
|
||||
// TODO: show all params, not just required ones
|
||||
// const allParameters = useMemo(() => {
|
||||
// return parameters;
|
||||
// }, [parameters]);
|
||||
|
||||
// Helper function to generate appropriate placeholder text
|
||||
const getPlaceholder = (parameter: ConfigKey): string => {
|
||||
// If default is defined and not null, show it
|
||||
@@ -92,8 +92,30 @@ export default function DefaultProviderSetupForm({
|
||||
return `Default: ${parameter.default}`;
|
||||
}
|
||||
|
||||
// Otherwise, use the parameter name as a hint
|
||||
return parameter.name.toUpperCase();
|
||||
const name = parameter.name.toLowerCase();
|
||||
if (name.includes('api_key')) return 'Your API key';
|
||||
if (name.includes('api_url') || name.includes('host')) return 'https://api.example.com';
|
||||
if (name.includes('models')) return 'model-a, model-b';
|
||||
|
||||
return parameter.name
|
||||
.replace(/_/g, ' ')
|
||||
.replace(/([A-Z])/g, ' $1')
|
||||
.replace(/^./, (str) => str.toUpperCase())
|
||||
.trim();
|
||||
};
|
||||
|
||||
// helper for custom labels
|
||||
const getFieldLabel = (parameter: ConfigKey): string => {
|
||||
const name = parameter.name.toLowerCase();
|
||||
if (name.includes('api_key')) return 'API Key';
|
||||
if (name.includes('api_url') || name.includes('host')) return 'API Host';
|
||||
if (name.includes('models')) return 'Models';
|
||||
|
||||
return parameter.name
|
||||
.replace(/_/g, ' ')
|
||||
.replace(/([A-Z])/g, ' $1')
|
||||
.replace(/^./, (str) => str.toUpperCase())
|
||||
.trim();
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
@@ -111,25 +133,30 @@ export default function DefaultProviderSetupForm({
|
||||
requiredParameters.map((parameter) => (
|
||||
<div key={parameter.name}>
|
||||
<label className="block text-sm font-medium text-textStandard mb-1">
|
||||
{parameter.name}
|
||||
{getFieldLabel(parameter)}
|
||||
{parameter.required && <span className="text-red-500 ml-1">*</span>}
|
||||
</label>
|
||||
<Input
|
||||
type={parameter.secret ? 'password' : 'text'}
|
||||
value={configValues[parameter.name] || ''}
|
||||
onChange={(e) =>
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
console.log(`Setting ${parameter.name} to:`, e.target.value);
|
||||
setConfigValues((prev) => ({
|
||||
...prev,
|
||||
[parameter.name]: e.target.value,
|
||||
}))
|
||||
}
|
||||
}));
|
||||
}}
|
||||
placeholder={getPlaceholder(parameter)}
|
||||
className={`w-full h-14 px-4 font-regular rounded-lg shadow-none ${
|
||||
validationErrors[parameter.name]
|
||||
? 'border-2 border-red-500'
|
||||
: 'border border-borderSubtle hover:border-borderStandard'
|
||||
} bg-background-default text-lg placeholder:text-textSubtle font-regular text-textStandard`}
|
||||
required={true}
|
||||
required={parameter.required}
|
||||
/>
|
||||
{validationErrors[parameter.name] && (
|
||||
<p className="text-red-500 text-sm mt-1">{validationErrors[parameter.name]}</p>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -6,6 +6,7 @@ interface CardContainerProps {
|
||||
onClick: () => void;
|
||||
grayedOut: boolean;
|
||||
testId?: string;
|
||||
borderStyle?: 'solid' | 'dashed';
|
||||
}
|
||||
|
||||
function GlowingRing() {
|
||||
@@ -33,6 +34,7 @@ export default function CardContainer({
|
||||
onClick,
|
||||
grayedOut = false,
|
||||
testId,
|
||||
borderStyle = 'solid',
|
||||
}: CardContainerProps) {
|
||||
return (
|
||||
<div
|
||||
@@ -50,19 +52,21 @@ export default function CardContainer({
|
||||
>
|
||||
{!grayedOut && <GlowingRing />}
|
||||
<div
|
||||
className={`relative bg-background-default rounded-lg p-3 transition-all duration-200 h-[160px] flex flex-col justify-between
|
||||
className={`relative bg-background-default rounded-lg p-3 transition-all duration-200 h-[160px] flex flex-col
|
||||
${header ? 'justify-between' : 'justify-center'}
|
||||
${borderStyle === 'dashed' ? 'border-2 border-dashed' : 'border'}
|
||||
${
|
||||
grayedOut
|
||||
? 'border border-borderSubtle'
|
||||
: 'border border-borderSubtle hover:border-borderStandard'
|
||||
? 'border-borderSubtle'
|
||||
: 'border-borderSubtle hover:border-borderStandard'
|
||||
}`}
|
||||
>
|
||||
{/* Apply opacity only to the header when grayed out */}
|
||||
<div style={{ opacity: grayedOut ? '0.5' : '1' }}>
|
||||
<HeaderContainer>{header}</HeaderContainer>
|
||||
</div>
|
||||
{header && (
|
||||
<div style={{ opacity: grayedOut ? '0.5' : '1' }}>
|
||||
<HeaderContainer>{header}</HeaderContainer>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Body always at full opacity */}
|
||||
<div>{body}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { cn } from '../../utils';
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[1px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm transition-all cursor-pointer disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[1px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
Reference in New Issue
Block a user