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:
musistudio
2025-07-30 15:39:44 +08:00
parent e560db85f4
commit 1d7374067e
11 changed files with 429 additions and 151 deletions

View File

@@ -137,10 +137,52 @@ async function main() {
startProcess.unref(); startProcess.unref();
if (!(await waitForService())) { if (!(await waitForService())) {
console.error( // If service startup fails, try to start with default config
"Service startup timeout, please manually run `ccr start` to start the service" console.log("Service startup timeout, trying to start with default configuration...");
); const { initDir, writeConfigFile, backupConfigFile } = require("./utils");
process.exit(1);
try {
// Initialize directories
await initDir();
// Backup existing config file if it exists
const backupPath = await backupConfigFile();
if (backupPath) {
console.log(`Backed up existing configuration file to ${backupPath}`);
}
// Create a minimal default config file
await writeConfigFile({
"PORT": 3456,
"Providers": [],
"Router": {}
});
console.log("Created minimal default configuration file at ~/.claude-code-router/config.json");
console.log("Please edit this file with your actual configuration.");
// Try starting the service again
const restartProcess = spawn("node", [cliPath, "start"], {
detached: true,
stdio: "ignore",
});
restartProcess.on("error", (error) => {
console.error("Failed to start service with default config:", error.message);
process.exit(1);
});
restartProcess.unref();
if (!(await waitForService(15000))) { // Wait a bit longer for the first start
console.error(
"Service startup still failing. Please manually run `ccr start` to start the service and check the logs."
);
process.exit(1);
}
} catch (error: any) {
console.error("Failed to create default configuration:", error.message);
process.exit(1);
}
} }
} }

View File

@@ -28,6 +28,14 @@ export const createServer = (config: any): Server => {
// Add endpoint to save config.json // Add endpoint to save config.json
server.app.post("/api/config", async (req) => { server.app.post("/api/config", async (req) => {
const newConfig = req.body; const newConfig = req.body;
// Backup existing config file if it exists
const { backupConfigFile } = await import("./utils");
const backupPath = await backupConfigFile();
if (backupPath) {
console.log(`Backed up existing configuration file to ${backupPath}`);
}
await writeConfigFile(newConfig); await writeConfigFile(newConfig);
return { success: true, message: "Config saved successfully" }; return { success: true, message: "Config saved successfully" };
}); });

View File

@@ -85,6 +85,20 @@ export const readConfigFile = async () => {
} }
}; };
export const backupConfigFile = async () => {
try {
if (await fs.access(CONFIG_FILE).then(() => true).catch(() => false)) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupPath = `${CONFIG_FILE}.${timestamp}.bak`;
await fs.copyFile(CONFIG_FILE, backupPath);
return backupPath;
}
} catch (error) {
console.error("Failed to backup config file:", error);
}
return null;
};
export const writeConfigFile = async (config: any) => { export const writeConfigFile = async (config: any) => {
await ensureDir(HOME_DIR); await ensureDir(HOME_DIR);
const configWithComment = `${JSON.stringify(config, null, 2)}`; const configWithComment = `${JSON.stringify(config, null, 2)}`;

View File

@@ -2,7 +2,6 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <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" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CCR UI</title> <title>CCR UI</title>
</head> </head>

View File

@@ -75,87 +75,108 @@ function App() {
}, [config, navigate]); }, [config, navigate]);
const saveConfig = async () => { const saveConfig = async () => {
if (config) { // Handle case where config might be null or undefined
try { if (!config) {
// Save to API setToast({ message: t('app.config_missing'), type: 'error' });
const response = await api.updateConfig(config); 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 // 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) { if (response && typeof response === 'object' && 'success' in response) {
const apiResponse = response as { success: boolean; message?: string }; const apiResponse = response as { success: boolean; message?: string };
if (apiResponse.success) { if (apiResponse.success) {
setToast({ message: apiResponse.message || t('app.config_saved_success'), type: 'success' }); setToast({ message: apiResponse.message || t('app.config_saved_restart_success'), type: 'success' });
} else {
setToast({ message: apiResponse.message || t('app.config_saved_failed'), type: 'error' });
} }
} else { } 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) { 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) { 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) { 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 ( return (

View File

@@ -108,7 +108,32 @@ export function ConfigProvider({ children }: ConfigProviderProps) {
try { try {
// Try to fetch config regardless of API key presence // Try to fetch config regardless of API key presence
const data = await api.getConfig(); 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) { } catch (err) {
console.error('Failed to fetch config:', err); console.error('Failed to fetch config:', err);
// If we get a 401, the API client will redirect to login // If we get a 401, the API client will redirect to login

View File

@@ -10,29 +10,74 @@ interface ProviderListProps {
} }
export function ProviderList({ providers, onEdit, onRemove }: 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 ( return (
<div className="space-y-3"> <div className="space-y-3">
{providers.map((provider, index) => ( {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]"> // Handle case where individual provider might be null or undefined
<div className="flex-1 space-y-1.5"> if (!provider) {
<p className="text-md font-semibold text-gray-800">{provider.name}</p> return (
<p className="text-sm text-gray-500">{provider.api_base_url}</p> <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 flex-wrap gap-2 pt-2"> <div className="flex-1 space-y-1.5">
{provider.models.map((model) => ( <p className="text-md font-semibold text-gray-800">Invalid Provider</p>
<Badge key={model} variant="outline" className="font-normal transition-all-ease hover:scale-105">{model}</Badge> <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> </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> </div>
); );
} }

View File

@@ -46,10 +46,23 @@ export function Providers() {
fetchTransformers(); fetchTransformers();
}, []); }, []);
// Handle case where config is null or undefined
if (!config) { 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 handleAddProvider = () => {
const newProviders = [...config.Providers, { name: "", api_base_url: "", api_key: "", models: [] }]; const newProviders = [...config.Providers, { name: "", api_base_url: "", api_key: "", models: [] }];
@@ -307,8 +320,16 @@ export function Providers() {
const handleAddModel = (index: number, model: string) => { const handleAddModel = (index: number, model: string) => {
if (!model.trim()) return; 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 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 // Check if model already exists
if (!models.includes(model.trim())) { if (!models.includes(model.trim())) {
@@ -319,24 +340,36 @@ export function Providers() {
}; };
const handleRemoveModel = (providerIndex: number, modelIndex: number) => { 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 newProviders = [...config.Providers];
const models = [...newProviders[providerIndex].models];
models.splice(modelIndex, 1); // Handle case where provider.models might be null or undefined
newProviders[providerIndex].models = models; const models = Array.isArray(newProviders[providerIndex].models) ? [...newProviders[providerIndex].models] : [];
setConfig({ ...config, Providers: newProviders });
// 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 ( return (
<Card className="flex h-full flex-col rounded-lg border shadow-sm"> <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"> <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> <Button onClick={handleAddProvider}>{t("providers.add")}</Button>
</CardHeader> </CardHeader>
<CardContent className="flex-grow overflow-y-auto p-4"> <CardContent className="flex-grow overflow-y-auto p-4">
<ProviderList <ProviderList
providers={config.Providers} providers={validProviders}
onEdit={setEditingProviderIndex} onEdit={setEditingProviderIndex}
onRemove={setDeletingProviderIndex} 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-4 p-4 overflow-y-auto flex-grow">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="name">{t("providers.name")}</Label> <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>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="api_base_url">{t("providers.api_base_url")}</Label> <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>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="api_key">{t("providers.api_key")}</Label> <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>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="models">{t("providers.models")}</Label> <Label htmlFor="models">{t("providers.models")}</Label>
@@ -374,7 +407,7 @@ export function Providers() {
{hasFetchedModels[editingProviderIndex] ? ( {hasFetchedModels[editingProviderIndex] ? (
<ComboInput <ComboInput
ref={comboInputRef} ref={comboInputRef}
options={editingProvider.models.map(model => ({ label: model, value: model }))} options={(editingProvider.models || []).map(model => ({ label: model, value: model }))}
value="" value=""
onChange={(_) => { onChange={(_) => {
// 只更新输入值,不添加模型 // 只更新输入值,不添加模型
@@ -431,7 +464,7 @@ export function Providers() {
</Button> */} </Button> */}
</div> </div>
<div className="flex flex-wrap gap-2 pt-2"> <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"> <Badge key={modelIndex} variant="outline" className="font-normal flex items-center gap-1">
{model} {model}
<button <button
@@ -596,11 +629,11 @@ export function Providers() {
</div> </div>
{/* Model-specific Transformers */} {/* Model-specific Transformers */}
{editingProvider.models.length > 0 && ( {editingProvider.models && editingProvider.models.length > 0 && (
<div className="space-y-2"> <div className="space-y-2">
<Label>{t("providers.model_transformers")}</Label> <Label>{t("providers.model_transformers")}</Label>
<div className="space-y-3"> <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 key={modelIndex} className="border rounded-md p-3">
<div className="font-medium text-sm mb-2">{model}</div> <div className="font-medium text-sm mb-2">{model}</div>
{/* Add new transformer */} {/* Add new transformer */}

View File

@@ -8,21 +8,54 @@ export function Router() {
const { t } = useTranslation(); const { t } = useTranslation();
const { config, setConfig } = useConfig(); const { config, setConfig } = useConfig();
// Handle case where config is null or undefined
if (!config) { 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 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 }); setConfig({ ...config, Router: newRouter });
}; };
const modelOptions = config.Providers.flatMap((provider) => // Handle case where config.Providers might be null or undefined
provider.models.map((model) => ({ const providers = Array.isArray(config.Providers) ? config.Providers : [];
value: `${provider.name},${model}`,
label: `${provider.name}, ${model}`, 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 ( return (
<Card className="flex h-full flex-col rounded-lg border shadow-sm"> <Card className="flex h-full flex-col rounded-lg border shadow-sm">
@@ -34,7 +67,7 @@ export function Router() {
<Label>{t("router.default")}</Label> <Label>{t("router.default")}</Label>
<Combobox <Combobox
options={modelOptions} options={modelOptions}
value={config.Router.default} value={routerConfig.default || ""}
onChange={(value) => handleRouterChange("default", value)} onChange={(value) => handleRouterChange("default", value)}
placeholder={t("router.selectModel")} placeholder={t("router.selectModel")}
searchPlaceholder={t("router.searchModel")} searchPlaceholder={t("router.searchModel")}
@@ -45,7 +78,7 @@ export function Router() {
<Label>{t("router.background")}</Label> <Label>{t("router.background")}</Label>
<Combobox <Combobox
options={modelOptions} options={modelOptions}
value={config.Router.background} value={routerConfig.background || ""}
onChange={(value) => handleRouterChange("background", value)} onChange={(value) => handleRouterChange("background", value)}
placeholder={t("router.selectModel")} placeholder={t("router.selectModel")}
searchPlaceholder={t("router.searchModel")} searchPlaceholder={t("router.searchModel")}
@@ -56,7 +89,7 @@ export function Router() {
<Label>{t("router.think")}</Label> <Label>{t("router.think")}</Label>
<Combobox <Combobox
options={modelOptions} options={modelOptions}
value={config.Router.think} value={routerConfig.think || ""}
onChange={(value) => handleRouterChange("think", value)} onChange={(value) => handleRouterChange("think", value)}
placeholder={t("router.selectModel")} placeholder={t("router.selectModel")}
searchPlaceholder={t("router.searchModel")} searchPlaceholder={t("router.searchModel")}
@@ -67,7 +100,7 @@ export function Router() {
<Label>{t("router.longContext")}</Label> <Label>{t("router.longContext")}</Label>
<Combobox <Combobox
options={modelOptions} options={modelOptions}
value={config.Router.longContext} value={routerConfig.longContext || ""}
onChange={(value) => handleRouterChange("longContext", value)} onChange={(value) => handleRouterChange("longContext", value)}
placeholder={t("router.selectModel")} placeholder={t("router.selectModel")}
searchPlaceholder={t("router.searchModel")} searchPlaceholder={t("router.searchModel")}
@@ -78,7 +111,7 @@ export function Router() {
<Label>{t("router.webSearch")}</Label> <Label>{t("router.webSearch")}</Label>
<Combobox <Combobox
options={modelOptions} options={modelOptions}
value={config.Router.webSearch} value={routerConfig.webSearch || ""}
onChange={(value) => handleRouterChange("webSearch", value)} onChange={(value) => handleRouterChange("webSearch", value)}
placeholder={t("router.selectModel")} placeholder={t("router.selectModel")}
searchPlaceholder={t("router.searchModel")} searchPlaceholder={t("router.searchModel")}

View File

@@ -9,24 +9,66 @@ interface TransformerListProps {
} }
export function TransformerList({ transformers, onEdit, onRemove }: 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 ( return (
<div className="space-y-3"> <div className="space-y-3">
{transformers.map((transformer, index) => ( {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]"> // Handle case where individual transformer might be null or undefined
<div className="flex-1 space-y-1.5"> if (!transformer) {
<p className="text-md font-semibold text-gray-800">{transformer.path}</p> return (
<p className="text-sm text-gray-500">{transformer.options.project}</p> <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>
<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> </div>
); );
} }

View File

@@ -23,27 +23,40 @@ export function Transformers() {
const [deletingTransformerIndex, setDeletingTransformerIndex] = useState<number | null>(null); const [deletingTransformerIndex, setDeletingTransformerIndex] = useState<number | null>(null);
const [newTransformer, setNewTransformer] = useState<{ path: string; options: { [key: string]: string } } | null>(null); const [newTransformer, setNewTransformer] = useState<{ path: string; options: { [key: string]: string } } | null>(null);
// Handle case where config is null or undefined
if (!config) { 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 handleAddTransformer = () => {
const newTransformer = { path: "", options: {} }; const newTransformer = { path: "", options: {} };
setNewTransformer(newTransformer); 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 handleRemoveTransformer = (index: number) => {
const newTransformers = [...config.transformers]; const newTransformers = [...validTransformers];
newTransformers.splice(index, 1); newTransformers.splice(index, 1);
setConfig({ ...config, transformers: newTransformers }); setConfig({ ...config, transformers: newTransformers });
setDeletingTransformerIndex(null); setDeletingTransformerIndex(null);
}; };
const handleTransformerChange = (index: number, field: string, value: string, optionKey?: string) => { const handleTransformerChange = (index: number, field: string, value: string, optionKey?: string) => {
if (index < config.transformers.length) { if (index < validTransformers.length) {
// Editing an existing transformer // Editing an existing transformer
const newTransformers = [...config.transformers]; const newTransformers = [...validTransformers];
if (optionKey !== undefined) { if (optionKey !== undefined) {
newTransformers[index].options[optionKey] = value; newTransformers[index].options[optionKey] = value;
} else { } else {
@@ -65,15 +78,15 @@ export function Transformers() {
}; };
const editingTransformer = editingTransformerIndex !== null ? const editingTransformer = editingTransformerIndex !== null ?
(editingTransformerIndex < config.transformers.length ? (editingTransformerIndex < validTransformers.length ?
config.transformers[editingTransformerIndex] : validTransformers[editingTransformerIndex] :
newTransformer) : newTransformer) :
null; null;
const handleSaveTransformer = () => { const handleSaveTransformer = () => {
if (newTransformer && editingTransformerIndex === config.transformers.length) { if (newTransformer && editingTransformerIndex === validTransformers.length) {
// Saving a new transformer // Saving a new transformer
const newTransformers = [...config.transformers, newTransformer]; const newTransformers = [...validTransformers, newTransformer];
setConfig({ ...config, transformers: newTransformers }); setConfig({ ...config, transformers: newTransformers });
} }
// Close the dialog // Close the dialog
@@ -90,12 +103,12 @@ export function Transformers() {
return ( return (
<Card className="flex h-full flex-col rounded-lg border shadow-sm"> <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"> <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> <Button onClick={handleAddTransformer}>{t("transformers.add")}</Button>
</CardHeader> </CardHeader>
<CardContent className="flex-grow overflow-y-auto p-4"> <CardContent className="flex-grow overflow-y-auto p-4">
<TransformerList <TransformerList
transformers={config.transformers} transformers={validTransformers}
onEdit={setEditingTransformerIndex} onEdit={setEditingTransformerIndex}
onRemove={setDeletingTransformerIndex} onRemove={setDeletingTransformerIndex}
/> />
@@ -113,7 +126,7 @@ export function Transformers() {
<Label htmlFor="transformer-path">{t("transformers.path")}</Label> <Label htmlFor="transformer-path">{t("transformers.path")}</Label>
<Input <Input
id="transformer-path" id="transformer-path"
value={editingTransformer.path} value={editingTransformer.path || ''}
onChange={(e) => handleTransformerChange(editingTransformerIndex, "path", e.target.value)} onChange={(e) => handleTransformerChange(editingTransformerIndex, "path", e.target.value)}
/> />
</div> </div>
@@ -124,11 +137,12 @@ export function Transformers() {
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => { 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) { if (editingTransformerIndex !== null) {
const newOptions = { ...editingTransformer.options, [newKey]: "" }; const newOptions = { ...options, [newKey]: "" };
if (editingTransformerIndex < config.transformers.length) { if (editingTransformerIndex < validTransformers.length) {
const newTransformers = [...config.transformers]; const newTransformers = [...validTransformers];
newTransformers[editingTransformerIndex].options = newOptions; newTransformers[editingTransformerIndex].options = newOptions;
setConfig({ ...config, transformers: newTransformers }); setConfig({ ...config, transformers: newTransformers });
} else if (newTransformer) { } else if (newTransformer) {
@@ -140,17 +154,18 @@ export function Transformers() {
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
</Button> </Button>
</div> </div>
{Object.entries(editingTransformer.options).map(([key, value]) => ( {Object.entries(editingTransformer.options || {}).map(([key, value]) => (
<div key={key} className="flex items-center gap-2"> <div key={key} className="flex items-center gap-2">
<Input <Input
value={key} value={key}
onChange={(e) => { onChange={(e) => {
const newOptions = { ...editingTransformer.options }; const options = editingTransformer.options || {};
const newOptions = { ...options };
delete newOptions[key]; delete newOptions[key];
newOptions[e.target.value] = value; newOptions[e.target.value] = value;
if (editingTransformerIndex !== null) { if (editingTransformerIndex !== null) {
if (editingTransformerIndex < config.transformers.length) { if (editingTransformerIndex < validTransformers.length) {
const newTransformers = [...config.transformers]; const newTransformers = [...validTransformers];
newTransformers[editingTransformerIndex].options = newOptions; newTransformers[editingTransformerIndex].options = newOptions;
setConfig({ ...config, transformers: newTransformers }); setConfig({ ...config, transformers: newTransformers });
} else if (newTransformer) { } else if (newTransformer) {
@@ -174,10 +189,11 @@ export function Transformers() {
size="icon" size="icon"
onClick={() => { onClick={() => {
if (editingTransformerIndex !== null) { if (editingTransformerIndex !== null) {
const newOptions = { ...editingTransformer.options }; const options = editingTransformer.options || {};
const newOptions = { ...options };
delete newOptions[key]; delete newOptions[key];
if (editingTransformerIndex < config.transformers.length) { if (editingTransformerIndex < validTransformers.length) {
const newTransformers = [...config.transformers]; const newTransformers = [...validTransformers];
newTransformers[editingTransformerIndex].options = newOptions; newTransformers[editingTransformerIndex].options = newOptions;
setConfig({ ...config, transformers: newTransformers }); setConfig({ ...config, transformers: newTransformers });
} else if (newTransformer) { } else if (newTransformer) {