fix: improve error handling and config validation
- Add fallback mechanism for service startup with default config - Implement config file backup before saving - Add robust validation for config data in UI components - Improve error handling and user feedback in UI - Fix potential null/undefined access in provider and router components 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,6 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>CCR UI</title>
|
||||
</head>
|
||||
|
||||
139
ui/src/App.tsx
139
ui/src/App.tsx
@@ -75,87 +75,108 @@ function App() {
|
||||
}, [config, navigate]);
|
||||
|
||||
const saveConfig = async () => {
|
||||
if (config) {
|
||||
try {
|
||||
// Save to API
|
||||
const response = await api.updateConfig(config);
|
||||
// Handle case where config might be null or undefined
|
||||
if (!config) {
|
||||
setToast({ message: t('app.config_missing'), type: 'error' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Save to API
|
||||
const response = await api.updateConfig(config);
|
||||
// Show success message or handle as needed
|
||||
console.log('Config saved successfully');
|
||||
|
||||
// 根据响应信息进行提示
|
||||
if (response && typeof response === 'object' && 'success' in response) {
|
||||
const apiResponse = response as { success: boolean; message?: string };
|
||||
if (apiResponse.success) {
|
||||
setToast({ message: apiResponse.message || t('app.config_saved_success'), type: 'success' });
|
||||
} else {
|
||||
setToast({ message: apiResponse.message || t('app.config_saved_failed'), type: 'error' });
|
||||
}
|
||||
} else {
|
||||
// 默认成功提示
|
||||
setToast({ message: t('app.config_saved_success'), type: 'success' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save config:', error);
|
||||
// Handle error appropriately
|
||||
setToast({ message: t('app.config_saved_failed') + ': ' + (error as Error).message, type: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
const saveConfigAndRestart = async () => {
|
||||
// Handle case where config might be null or undefined
|
||||
if (!config) {
|
||||
setToast({ message: t('app.config_missing'), type: 'error' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Save to API
|
||||
const response = await api.updateConfig(config);
|
||||
|
||||
// Check if save was successful before restarting
|
||||
let saveSuccessful = true;
|
||||
if (response && typeof response === 'object' && 'success' in response) {
|
||||
const apiResponse = response as { success: boolean; message?: string };
|
||||
if (!apiResponse.success) {
|
||||
saveSuccessful = false;
|
||||
setToast({ message: apiResponse.message || t('app.config_saved_failed'), type: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
// Only restart if save was successful
|
||||
if (saveSuccessful) {
|
||||
// Restart service
|
||||
const response = await api.restartService();
|
||||
|
||||
// Show success message or handle as needed
|
||||
console.log('Config saved successfully');
|
||||
console.log('Config saved and service restarted successfully');
|
||||
|
||||
// 根据响应信息进行提示
|
||||
if (response && typeof response === 'object' && 'success' in response) {
|
||||
const apiResponse = response as { success: boolean; message?: string };
|
||||
if (apiResponse.success) {
|
||||
setToast({ message: apiResponse.message || t('app.config_saved_success'), type: 'success' });
|
||||
} else {
|
||||
setToast({ message: apiResponse.message || t('app.config_saved_failed'), type: 'error' });
|
||||
setToast({ message: apiResponse.message || t('app.config_saved_restart_success'), type: 'success' });
|
||||
}
|
||||
} else {
|
||||
// 默认成功提示
|
||||
setToast({ message: t('app.config_saved_success'), type: 'success' });
|
||||
setToast({ message: t('app.config_saved_restart_success'), type: 'success' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save config:', error);
|
||||
// Handle error appropriately
|
||||
setToast({ message: t('app.config_saved_failed') + ': ' + (error as Error).message, type: 'error' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const saveConfigAndRestart = async () => {
|
||||
if (config) {
|
||||
try {
|
||||
// Save to API
|
||||
const response = await api.updateConfig(config);
|
||||
|
||||
// Check if save was successful before restarting
|
||||
let saveSuccessful = true;
|
||||
if (response && typeof response === 'object' && 'success' in response) {
|
||||
const apiResponse = response as { success: boolean; message?: string };
|
||||
if (!apiResponse.success) {
|
||||
saveSuccessful = false;
|
||||
setToast({ message: apiResponse.message || t('app.config_saved_failed'), type: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
// Only restart if save was successful
|
||||
if (saveSuccessful) {
|
||||
// Restart service
|
||||
const response = await api.restartService();
|
||||
|
||||
// Show success message or handle as needed
|
||||
console.log('Config saved and service restarted successfully');
|
||||
|
||||
// 根据响应信息进行提示
|
||||
if (response && typeof response === 'object' && 'success' in response) {
|
||||
const apiResponse = response as { success: boolean; message?: string };
|
||||
if (apiResponse.success) {
|
||||
setToast({ message: apiResponse.message || t('app.config_saved_restart_success'), type: 'success' });
|
||||
}
|
||||
} else {
|
||||
// 默认成功提示
|
||||
setToast({ message: t('app.config_saved_restart_success'), type: 'success' });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save config and restart:', error);
|
||||
// Handle error appropriately
|
||||
setToast({ message: t('app.config_saved_restart_failed') + ': ' + (error as Error).message, type: 'error' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save config and restart:', error);
|
||||
// Handle error appropriately
|
||||
setToast({ message: t('app.config_saved_restart_failed') + ': ' + (error as Error).message, type: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (isCheckingAuth) {
|
||||
return <div>Loading...</div>;
|
||||
return (
|
||||
<div className="h-screen bg-gray-50 font-sans flex items-center justify-center">
|
||||
<div className="text-gray-500">Loading application...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div>Error: {error.message}</div>;
|
||||
return (
|
||||
<div className="h-screen bg-gray-50 font-sans flex items-center justify-center">
|
||||
<div className="text-red-500">Error: {error.message}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Handle case where config is null or undefined
|
||||
if (!config) {
|
||||
return <div>Loading...</div>;
|
||||
return (
|
||||
<div className="h-screen bg-gray-50 font-sans flex items-center justify-center">
|
||||
<div className="text-gray-500">Loading configuration...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -108,7 +108,32 @@ export function ConfigProvider({ children }: ConfigProviderProps) {
|
||||
try {
|
||||
// Try to fetch config regardless of API key presence
|
||||
const data = await api.getConfig();
|
||||
setConfig(data);
|
||||
|
||||
// Validate the received data to ensure it has the expected structure
|
||||
const validConfig = {
|
||||
LOG: typeof data.LOG === 'boolean' ? data.LOG : false,
|
||||
CLAUDE_PATH: typeof data.CLAUDE_PATH === 'string' ? data.CLAUDE_PATH : '',
|
||||
HOST: typeof data.HOST === 'string' ? data.HOST : '127.0.0.1',
|
||||
PORT: typeof data.PORT === 'number' ? data.PORT : 3456,
|
||||
APIKEY: typeof data.APIKEY === 'string' ? data.APIKEY : '',
|
||||
transformers: Array.isArray(data.transformers) ? data.transformers : [],
|
||||
Providers: Array.isArray(data.Providers) ? data.Providers : [],
|
||||
Router: data.Router && typeof data.Router === 'object' ? {
|
||||
default: typeof data.Router.default === 'string' ? data.Router.default : '',
|
||||
background: typeof data.Router.background === 'string' ? data.Router.background : '',
|
||||
think: typeof data.Router.think === 'string' ? data.Router.think : '',
|
||||
longContext: typeof data.Router.longContext === 'string' ? data.Router.longContext : '',
|
||||
webSearch: typeof data.Router.webSearch === 'string' ? data.Router.webSearch : ''
|
||||
} : {
|
||||
default: '',
|
||||
background: '',
|
||||
think: '',
|
||||
longContext: '',
|
||||
webSearch: ''
|
||||
}
|
||||
};
|
||||
|
||||
setConfig(validConfig);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch config:', err);
|
||||
// If we get a 401, the API client will redirect to login
|
||||
|
||||
@@ -10,29 +10,74 @@ interface ProviderListProps {
|
||||
}
|
||||
|
||||
export function ProviderList({ providers, onEdit, onRemove }: ProviderListProps) {
|
||||
// Handle case where providers might be null or undefined
|
||||
if (!providers || !Array.isArray(providers)) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-center rounded-md border bg-white p-8 text-gray-500">
|
||||
No providers configured
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{providers.map((provider, index) => (
|
||||
<div key={index} className="flex items-start justify-between rounded-md border bg-white p-4 transition-all hover:shadow-md animate-slide-in hover:scale-[1.01]">
|
||||
<div className="flex-1 space-y-1.5">
|
||||
<p className="text-md font-semibold text-gray-800">{provider.name}</p>
|
||||
<p className="text-sm text-gray-500">{provider.api_base_url}</p>
|
||||
<div className="flex flex-wrap gap-2 pt-2">
|
||||
{provider.models.map((model) => (
|
||||
<Badge key={model} variant="outline" className="font-normal transition-all-ease hover:scale-105">{model}</Badge>
|
||||
))}
|
||||
{providers.map((provider, index) => {
|
||||
// Handle case where individual provider might be null or undefined
|
||||
if (!provider) {
|
||||
return (
|
||||
<div key={index} className="flex items-start justify-between rounded-md border bg-white p-4 transition-all hover:shadow-md animate-slide-in hover:scale-[1.01]">
|
||||
<div className="flex-1 space-y-1.5">
|
||||
<p className="text-md font-semibold text-gray-800">Invalid Provider</p>
|
||||
<p className="text-sm text-gray-500">Provider data is missing</p>
|
||||
</div>
|
||||
<div className="ml-4 flex flex-shrink-0 items-center gap-2">
|
||||
<Button variant="ghost" size="icon" onClick={() => onEdit(index)} className="transition-all-ease hover:scale-110" disabled>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="destructive" size="icon" onClick={() => onRemove(index)} className="transition-all duration-200 hover:scale-110">
|
||||
<Trash2 className="h-4 w-4 text-current transition-colors duration-200" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Handle case where provider.name might be null or undefined
|
||||
const providerName = provider.name || "Unnamed Provider";
|
||||
|
||||
// Handle case where provider.api_base_url might be null or undefined
|
||||
const apiBaseUrl = provider.api_base_url || "No API URL";
|
||||
|
||||
// Handle case where provider.models might be null or undefined
|
||||
const models = Array.isArray(provider.models) ? provider.models : [];
|
||||
|
||||
return (
|
||||
<div key={index} className="flex items-start justify-between rounded-md border bg-white p-4 transition-all hover:shadow-md animate-slide-in hover:scale-[1.01]">
|
||||
<div className="flex-1 space-y-1.5">
|
||||
<p className="text-md font-semibold text-gray-800">{providerName}</p>
|
||||
<p className="text-sm text-gray-500">{apiBaseUrl}</p>
|
||||
<div className="flex flex-wrap gap-2 pt-2">
|
||||
{models.map((model, modelIndex) => (
|
||||
// Handle case where model might be null or undefined
|
||||
<Badge key={modelIndex} variant="outline" className="font-normal transition-all-ease hover:scale-105">
|
||||
{model || "Unnamed Model"}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-4 flex flex-shrink-0 items-center gap-2">
|
||||
<Button variant="ghost" size="icon" onClick={() => onEdit(index)} className="transition-all-ease hover:scale-110">
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="destructive" size="icon" onClick={() => onRemove(index)} className="transition-all duration-200 hover:scale-110">
|
||||
<Trash2 className="h-4 w-4 text-current transition-colors duration-200" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-4 flex flex-shrink-0 items-center gap-2">
|
||||
<Button variant="ghost" size="icon" onClick={() => onEdit(index)} className="transition-all-ease hover:scale-110">
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="destructive" size="icon" onClick={() => onRemove(index)} className="transition-all duration-200 hover:scale-110">
|
||||
<Trash2 className="h-4 w-4 text-current transition-colors duration-200" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -46,10 +46,23 @@ export function Providers() {
|
||||
fetchTransformers();
|
||||
}, []);
|
||||
|
||||
// Handle case where config is null or undefined
|
||||
if (!config) {
|
||||
return null;
|
||||
return (
|
||||
<Card className="flex h-full flex-col rounded-lg border shadow-sm">
|
||||
<CardHeader className="flex flex-row items-center justify-between border-b p-4">
|
||||
<CardTitle className="text-lg">{t("providers.title")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-grow flex items-center justify-center p-4">
|
||||
<div className="text-gray-500">Loading providers configuration...</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Validate config.Providers to ensure it's an array
|
||||
const validProviders = Array.isArray(config.Providers) ? config.Providers : [];
|
||||
|
||||
|
||||
const handleAddProvider = () => {
|
||||
const newProviders = [...config.Providers, { name: "", api_base_url: "", api_key: "", models: [] }];
|
||||
@@ -307,8 +320,16 @@ export function Providers() {
|
||||
const handleAddModel = (index: number, model: string) => {
|
||||
if (!model.trim()) return;
|
||||
|
||||
// Handle case where config.Providers might be null or undefined
|
||||
if (!config || !Array.isArray(config.Providers)) return;
|
||||
|
||||
// Handle case where the provider at the given index might be null or undefined
|
||||
if (!config.Providers[index]) return;
|
||||
|
||||
const newProviders = [...config.Providers];
|
||||
const models = [...newProviders[index].models];
|
||||
|
||||
// Handle case where provider.models might be null or undefined
|
||||
const models = Array.isArray(newProviders[index].models) ? [...newProviders[index].models] : [];
|
||||
|
||||
// Check if model already exists
|
||||
if (!models.includes(model.trim())) {
|
||||
@@ -319,24 +340,36 @@ export function Providers() {
|
||||
};
|
||||
|
||||
const handleRemoveModel = (providerIndex: number, modelIndex: number) => {
|
||||
// Handle case where config.Providers might be null or undefined
|
||||
if (!config || !Array.isArray(config.Providers)) return;
|
||||
|
||||
// Handle case where the provider at the given index might be null or undefined
|
||||
if (!config.Providers[providerIndex]) return;
|
||||
|
||||
const newProviders = [...config.Providers];
|
||||
const models = [...newProviders[providerIndex].models];
|
||||
models.splice(modelIndex, 1);
|
||||
newProviders[providerIndex].models = models;
|
||||
setConfig({ ...config, Providers: newProviders });
|
||||
|
||||
// Handle case where provider.models might be null or undefined
|
||||
const models = Array.isArray(newProviders[providerIndex].models) ? [...newProviders[providerIndex].models] : [];
|
||||
|
||||
// Handle case where modelIndex might be out of bounds
|
||||
if (modelIndex >= 0 && modelIndex < models.length) {
|
||||
models.splice(modelIndex, 1);
|
||||
newProviders[providerIndex].models = models;
|
||||
setConfig({ ...config, Providers: newProviders });
|
||||
}
|
||||
};
|
||||
|
||||
const editingProvider = editingProviderIndex !== null ? config.Providers[editingProviderIndex] : null;
|
||||
const editingProvider = editingProviderIndex !== null ? validProviders[editingProviderIndex] : null;
|
||||
|
||||
return (
|
||||
<Card className="flex h-full flex-col rounded-lg border shadow-sm">
|
||||
<CardHeader className="flex flex-row items-center justify-between border-b p-4">
|
||||
<CardTitle className="text-lg">{t("providers.title")} <span className="text-sm font-normal text-gray-500">({config.Providers.length})</span></CardTitle>
|
||||
<CardTitle className="text-lg">{t("providers.title")} <span className="text-sm font-normal text-gray-500">({validProviders.length})</span></CardTitle>
|
||||
<Button onClick={handleAddProvider}>{t("providers.add")}</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-grow overflow-y-auto p-4">
|
||||
<ProviderList
|
||||
providers={config.Providers}
|
||||
providers={validProviders}
|
||||
onEdit={setEditingProviderIndex}
|
||||
onRemove={setDeletingProviderIndex}
|
||||
/>
|
||||
@@ -356,15 +389,15 @@ export function Providers() {
|
||||
<div className="space-y-4 p-4 overflow-y-auto flex-grow">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">{t("providers.name")}</Label>
|
||||
<Input id="name" value={editingProvider.name} onChange={(e) => handleProviderChange(editingProviderIndex, 'name', e.target.value)} />
|
||||
<Input id="name" value={editingProvider.name || ''} onChange={(e) => handleProviderChange(editingProviderIndex, 'name', e.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="api_base_url">{t("providers.api_base_url")}</Label>
|
||||
<Input id="api_base_url" value={editingProvider.api_base_url} onChange={(e) => handleProviderChange(editingProviderIndex, 'api_base_url', e.target.value)} />
|
||||
<Input id="api_base_url" value={editingProvider.api_base_url || ''} onChange={(e) => handleProviderChange(editingProviderIndex, 'api_base_url', e.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="api_key">{t("providers.api_key")}</Label>
|
||||
<Input id="api_key" type="password" value={editingProvider.api_key} onChange={(e) => handleProviderChange(editingProviderIndex, 'api_key', e.target.value)} />
|
||||
<Input id="api_key" type="password" value={editingProvider.api_key || ''} onChange={(e) => handleProviderChange(editingProviderIndex, 'api_key', e.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="models">{t("providers.models")}</Label>
|
||||
@@ -374,7 +407,7 @@ export function Providers() {
|
||||
{hasFetchedModels[editingProviderIndex] ? (
|
||||
<ComboInput
|
||||
ref={comboInputRef}
|
||||
options={editingProvider.models.map(model => ({ label: model, value: model }))}
|
||||
options={(editingProvider.models || []).map(model => ({ label: model, value: model }))}
|
||||
value=""
|
||||
onChange={(_) => {
|
||||
// 只更新输入值,不添加模型
|
||||
@@ -431,7 +464,7 @@ export function Providers() {
|
||||
</Button> */}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 pt-2">
|
||||
{editingProvider.models.map((model, modelIndex) => (
|
||||
{(editingProvider.models || []).map((model, modelIndex) => (
|
||||
<Badge key={modelIndex} variant="outline" className="font-normal flex items-center gap-1">
|
||||
{model}
|
||||
<button
|
||||
@@ -596,11 +629,11 @@ export function Providers() {
|
||||
</div>
|
||||
|
||||
{/* Model-specific Transformers */}
|
||||
{editingProvider.models.length > 0 && (
|
||||
{editingProvider.models && editingProvider.models.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<Label>{t("providers.model_transformers")}</Label>
|
||||
<div className="space-y-3">
|
||||
{editingProvider.models.map((model, modelIndex) => (
|
||||
{(editingProvider.models || []).map((model, modelIndex) => (
|
||||
<div key={modelIndex} className="border rounded-md p-3">
|
||||
<div className="font-medium text-sm mb-2">{model}</div>
|
||||
{/* Add new transformer */}
|
||||
|
||||
@@ -8,21 +8,54 @@ export function Router() {
|
||||
const { t } = useTranslation();
|
||||
const { config, setConfig } = useConfig();
|
||||
|
||||
// Handle case where config is null or undefined
|
||||
if (!config) {
|
||||
return null;
|
||||
return (
|
||||
<Card className="flex h-full flex-col rounded-lg border shadow-sm">
|
||||
<CardHeader className="border-b p-4">
|
||||
<CardTitle className="text-lg">{t("router.title")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-grow flex items-center justify-center p-4">
|
||||
<div className="text-gray-500">Loading router configuration...</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Handle case where config.Router is null or undefined
|
||||
const routerConfig = config.Router || {
|
||||
default: "",
|
||||
background: "",
|
||||
think: "",
|
||||
longContext: "",
|
||||
webSearch: ""
|
||||
};
|
||||
|
||||
const handleRouterChange = (field: string, value: string) => {
|
||||
const newRouter = { ...config.Router, [field]: value };
|
||||
// Handle case where config.Router might be null or undefined
|
||||
const currentRouter = config.Router || {};
|
||||
const newRouter = { ...currentRouter, [field]: value };
|
||||
setConfig({ ...config, Router: newRouter });
|
||||
};
|
||||
|
||||
const modelOptions = config.Providers.flatMap((provider) =>
|
||||
provider.models.map((model) => ({
|
||||
value: `${provider.name},${model}`,
|
||||
label: `${provider.name}, ${model}`,
|
||||
}))
|
||||
);
|
||||
// Handle case where config.Providers might be null or undefined
|
||||
const providers = Array.isArray(config.Providers) ? config.Providers : [];
|
||||
|
||||
const modelOptions = providers.flatMap((provider) => {
|
||||
// Handle case where individual provider might be null or undefined
|
||||
if (!provider) return [];
|
||||
|
||||
// Handle case where provider.models might be null or undefined
|
||||
const models = Array.isArray(provider.models) ? provider.models : [];
|
||||
|
||||
// Handle case where provider.name might be null or undefined
|
||||
const providerName = provider.name || "Unknown Provider";
|
||||
|
||||
return models.map((model) => ({
|
||||
value: `${providerName},${model || "Unknown Model"}`,
|
||||
label: `${providerName}, ${model || "Unknown Model"}`,
|
||||
}));
|
||||
});
|
||||
|
||||
return (
|
||||
<Card className="flex h-full flex-col rounded-lg border shadow-sm">
|
||||
@@ -34,7 +67,7 @@ export function Router() {
|
||||
<Label>{t("router.default")}</Label>
|
||||
<Combobox
|
||||
options={modelOptions}
|
||||
value={config.Router.default}
|
||||
value={routerConfig.default || ""}
|
||||
onChange={(value) => handleRouterChange("default", value)}
|
||||
placeholder={t("router.selectModel")}
|
||||
searchPlaceholder={t("router.searchModel")}
|
||||
@@ -45,7 +78,7 @@ export function Router() {
|
||||
<Label>{t("router.background")}</Label>
|
||||
<Combobox
|
||||
options={modelOptions}
|
||||
value={config.Router.background}
|
||||
value={routerConfig.background || ""}
|
||||
onChange={(value) => handleRouterChange("background", value)}
|
||||
placeholder={t("router.selectModel")}
|
||||
searchPlaceholder={t("router.searchModel")}
|
||||
@@ -56,7 +89,7 @@ export function Router() {
|
||||
<Label>{t("router.think")}</Label>
|
||||
<Combobox
|
||||
options={modelOptions}
|
||||
value={config.Router.think}
|
||||
value={routerConfig.think || ""}
|
||||
onChange={(value) => handleRouterChange("think", value)}
|
||||
placeholder={t("router.selectModel")}
|
||||
searchPlaceholder={t("router.searchModel")}
|
||||
@@ -67,7 +100,7 @@ export function Router() {
|
||||
<Label>{t("router.longContext")}</Label>
|
||||
<Combobox
|
||||
options={modelOptions}
|
||||
value={config.Router.longContext}
|
||||
value={routerConfig.longContext || ""}
|
||||
onChange={(value) => handleRouterChange("longContext", value)}
|
||||
placeholder={t("router.selectModel")}
|
||||
searchPlaceholder={t("router.searchModel")}
|
||||
@@ -78,7 +111,7 @@ export function Router() {
|
||||
<Label>{t("router.webSearch")}</Label>
|
||||
<Combobox
|
||||
options={modelOptions}
|
||||
value={config.Router.webSearch}
|
||||
value={routerConfig.webSearch || ""}
|
||||
onChange={(value) => handleRouterChange("webSearch", value)}
|
||||
placeholder={t("router.selectModel")}
|
||||
searchPlaceholder={t("router.searchModel")}
|
||||
|
||||
@@ -9,24 +9,66 @@ interface TransformerListProps {
|
||||
}
|
||||
|
||||
export function TransformerList({ transformers, onEdit, onRemove }: TransformerListProps) {
|
||||
// Handle case where transformers might be null or undefined
|
||||
if (!transformers || !Array.isArray(transformers)) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-center rounded-md border bg-white p-8 text-gray-500">
|
||||
No transformers configured
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{transformers.map((transformer, index) => (
|
||||
<div key={index} className="flex items-start justify-between rounded-md border bg-white p-4 transition-all hover:shadow-md animate-slide-in hover:scale-[1.01]">
|
||||
<div className="flex-1 space-y-1.5">
|
||||
<p className="text-md font-semibold text-gray-800">{transformer.path}</p>
|
||||
<p className="text-sm text-gray-500">{transformer.options.project}</p>
|
||||
{transformers.map((transformer, index) => {
|
||||
// Handle case where individual transformer might be null or undefined
|
||||
if (!transformer) {
|
||||
return (
|
||||
<div key={index} className="flex items-start justify-between rounded-md border bg-white p-4 transition-all hover:shadow-md animate-slide-in hover:scale-[1.01]">
|
||||
<div className="flex-1 space-y-1.5">
|
||||
<p className="text-md font-semibold text-gray-800">Invalid Transformer</p>
|
||||
<p className="text-sm text-gray-500">Transformer data is missing</p>
|
||||
</div>
|
||||
<div className="ml-4 flex flex-shrink-0 items-center gap-2">
|
||||
<Button variant="ghost" size="icon" onClick={() => onEdit(index)} className="transition-all-ease hover:scale-110" disabled>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="destructive" size="icon" onClick={() => onRemove(index)} className="transition-all duration-200 hover:scale-110">
|
||||
<Trash2 className="h-4 w-4 text-current transition-colors duration-200" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Handle case where transformer.path might be null or undefined
|
||||
const transformerPath = transformer.path || "Unnamed Transformer";
|
||||
|
||||
// Handle case where transformer.options might be null or undefined
|
||||
const options = transformer.options || {};
|
||||
|
||||
// Handle case where options.project might be null or undefined
|
||||
const project = options.project || "No Project";
|
||||
|
||||
return (
|
||||
<div key={index} className="flex items-start justify-between rounded-md border bg-white p-4 transition-all hover:shadow-md animate-slide-in hover:scale-[1.01]">
|
||||
<div className="flex-1 space-y-1.5">
|
||||
<p className="text-md font-semibold text-gray-800">{transformerPath}</p>
|
||||
<p className="text-sm text-gray-500">{project}</p>
|
||||
</div>
|
||||
<div className="ml-4 flex flex-shrink-0 items-center gap-2">
|
||||
<Button variant="ghost" size="icon" onClick={() => onEdit(index)} className="transition-all-ease hover:scale-110">
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="destructive" size="icon" onClick={() => onRemove(index)} className="transition-all duration-200 hover:scale-110">
|
||||
<Trash2 className="h-4 w-4 text-current transition-colors duration-200" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-4 flex flex-shrink-0 items-center gap-2">
|
||||
<Button variant="ghost" size="icon" onClick={() => onEdit(index)} className="transition-all-ease hover:scale-110">
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="destructive" size="icon" onClick={() => onRemove(index)} className="transition-all duration-200 hover:scale-110">
|
||||
<Trash2 className="h-4 w-4 text-current transition-colors duration-200" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,27 +23,40 @@ export function Transformers() {
|
||||
const [deletingTransformerIndex, setDeletingTransformerIndex] = useState<number | null>(null);
|
||||
const [newTransformer, setNewTransformer] = useState<{ path: string; options: { [key: string]: string } } | null>(null);
|
||||
|
||||
// Handle case where config is null or undefined
|
||||
if (!config) {
|
||||
return null;
|
||||
return (
|
||||
<Card className="flex h-full flex-col rounded-lg border shadow-sm">
|
||||
<CardHeader className="flex flex-row items-center justify-between border-b p-4">
|
||||
<CardTitle className="text-lg">{t("transformers.title")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-grow flex items-center justify-center p-4">
|
||||
<div className="text-gray-500">Loading transformers configuration...</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Validate config.transformers to ensure it's an array
|
||||
const validTransformers = Array.isArray(config.transformers) ? config.transformers : [];
|
||||
|
||||
const handleAddTransformer = () => {
|
||||
const newTransformer = { path: "", options: {} };
|
||||
setNewTransformer(newTransformer);
|
||||
setEditingTransformerIndex(config.transformers.length); // Use the length as index for the new item
|
||||
setEditingTransformerIndex(validTransformers.length); // Use the length as index for the new item
|
||||
};
|
||||
|
||||
const handleRemoveTransformer = (index: number) => {
|
||||
const newTransformers = [...config.transformers];
|
||||
const newTransformers = [...validTransformers];
|
||||
newTransformers.splice(index, 1);
|
||||
setConfig({ ...config, transformers: newTransformers });
|
||||
setDeletingTransformerIndex(null);
|
||||
};
|
||||
|
||||
const handleTransformerChange = (index: number, field: string, value: string, optionKey?: string) => {
|
||||
if (index < config.transformers.length) {
|
||||
if (index < validTransformers.length) {
|
||||
// Editing an existing transformer
|
||||
const newTransformers = [...config.transformers];
|
||||
const newTransformers = [...validTransformers];
|
||||
if (optionKey !== undefined) {
|
||||
newTransformers[index].options[optionKey] = value;
|
||||
} else {
|
||||
@@ -65,15 +78,15 @@ export function Transformers() {
|
||||
};
|
||||
|
||||
const editingTransformer = editingTransformerIndex !== null ?
|
||||
(editingTransformerIndex < config.transformers.length ?
|
||||
config.transformers[editingTransformerIndex] :
|
||||
(editingTransformerIndex < validTransformers.length ?
|
||||
validTransformers[editingTransformerIndex] :
|
||||
newTransformer) :
|
||||
null;
|
||||
|
||||
const handleSaveTransformer = () => {
|
||||
if (newTransformer && editingTransformerIndex === config.transformers.length) {
|
||||
if (newTransformer && editingTransformerIndex === validTransformers.length) {
|
||||
// Saving a new transformer
|
||||
const newTransformers = [...config.transformers, newTransformer];
|
||||
const newTransformers = [...validTransformers, newTransformer];
|
||||
setConfig({ ...config, transformers: newTransformers });
|
||||
}
|
||||
// Close the dialog
|
||||
@@ -90,12 +103,12 @@ export function Transformers() {
|
||||
return (
|
||||
<Card className="flex h-full flex-col rounded-lg border shadow-sm">
|
||||
<CardHeader className="flex flex-row items-center justify-between border-b p-4">
|
||||
<CardTitle className="text-lg">{t("transformers.title")} <span className="text-sm font-normal text-gray-500">({config.transformers.length})</span></CardTitle>
|
||||
<CardTitle className="text-lg">{t("transformers.title")} <span className="text-sm font-normal text-gray-500">({validTransformers.length})</span></CardTitle>
|
||||
<Button onClick={handleAddTransformer}>{t("transformers.add")}</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-grow overflow-y-auto p-4">
|
||||
<TransformerList
|
||||
transformers={config.transformers}
|
||||
transformers={validTransformers}
|
||||
onEdit={setEditingTransformerIndex}
|
||||
onRemove={setDeletingTransformerIndex}
|
||||
/>
|
||||
@@ -113,7 +126,7 @@ export function Transformers() {
|
||||
<Label htmlFor="transformer-path">{t("transformers.path")}</Label>
|
||||
<Input
|
||||
id="transformer-path"
|
||||
value={editingTransformer.path}
|
||||
value={editingTransformer.path || ''}
|
||||
onChange={(e) => handleTransformerChange(editingTransformerIndex, "path", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
@@ -124,11 +137,12 @@ export function Transformers() {
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const newKey = `param${Object.keys(editingTransformer.options).length + 1}`;
|
||||
const options = editingTransformer.options || {};
|
||||
const newKey = `param${Object.keys(options).length + 1}`;
|
||||
if (editingTransformerIndex !== null) {
|
||||
const newOptions = { ...editingTransformer.options, [newKey]: "" };
|
||||
if (editingTransformerIndex < config.transformers.length) {
|
||||
const newTransformers = [...config.transformers];
|
||||
const newOptions = { ...options, [newKey]: "" };
|
||||
if (editingTransformerIndex < validTransformers.length) {
|
||||
const newTransformers = [...validTransformers];
|
||||
newTransformers[editingTransformerIndex].options = newOptions;
|
||||
setConfig({ ...config, transformers: newTransformers });
|
||||
} else if (newTransformer) {
|
||||
@@ -140,17 +154,18 @@ export function Transformers() {
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{Object.entries(editingTransformer.options).map(([key, value]) => (
|
||||
{Object.entries(editingTransformer.options || {}).map(([key, value]) => (
|
||||
<div key={key} className="flex items-center gap-2">
|
||||
<Input
|
||||
value={key}
|
||||
onChange={(e) => {
|
||||
const newOptions = { ...editingTransformer.options };
|
||||
const options = editingTransformer.options || {};
|
||||
const newOptions = { ...options };
|
||||
delete newOptions[key];
|
||||
newOptions[e.target.value] = value;
|
||||
if (editingTransformerIndex !== null) {
|
||||
if (editingTransformerIndex < config.transformers.length) {
|
||||
const newTransformers = [...config.transformers];
|
||||
if (editingTransformerIndex < validTransformers.length) {
|
||||
const newTransformers = [...validTransformers];
|
||||
newTransformers[editingTransformerIndex].options = newOptions;
|
||||
setConfig({ ...config, transformers: newTransformers });
|
||||
} else if (newTransformer) {
|
||||
@@ -174,10 +189,11 @@ export function Transformers() {
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
if (editingTransformerIndex !== null) {
|
||||
const newOptions = { ...editingTransformer.options };
|
||||
const options = editingTransformer.options || {};
|
||||
const newOptions = { ...options };
|
||||
delete newOptions[key];
|
||||
if (editingTransformerIndex < config.transformers.length) {
|
||||
const newTransformers = [...config.transformers];
|
||||
if (editingTransformerIndex < validTransformers.length) {
|
||||
const newTransformers = [...validTransformers];
|
||||
newTransformers[editingTransformerIndex].options = newOptions;
|
||||
setConfig({ ...config, transformers: newTransformers });
|
||||
} else if (newTransformer) {
|
||||
|
||||
Reference in New Issue
Block a user