arena-learning / studyArena /components /settings /model-edit-dialog.tsx
Nitish kumar
Upload folder using huggingface_hub
c20f20c verified
'use client';
import { useState, useCallback, useEffect } from 'react';
import { Dialog, DialogContent, DialogTitle, DialogDescription } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
import { Sparkles, Wrench, Zap, Loader2, CheckCircle, XCircle } from 'lucide-react';
import { useI18n } from '@/lib/hooks/use-i18n';
import type { EditingModel } from '@/lib/types/settings';
import type { ProviderId } from '@/lib/ai/providers';
import { cn } from '@/lib/utils';
interface ModelEditDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
editingModel: EditingModel | null;
setEditingModel: (model: EditingModel | null) => void;
onSave: () => void;
onAutoSave?: () => void; // Auto-save on blur
providerId: ProviderId;
apiKey: string;
baseUrl?: string;
providerType?: string;
requiresApiKey?: boolean;
}
export function ModelEditDialog({
open,
onOpenChange,
editingModel,
setEditingModel,
onSave,
onAutoSave,
providerId,
apiKey,
baseUrl,
providerType,
requiresApiKey,
}: ModelEditDialogProps) {
const { t } = useI18n();
const [testStatus, setTestStatus] = useState<'idle' | 'testing' | 'success' | 'error'>('idle');
const [testMessage, setTestMessage] = useState('');
// Reset test status when dialog closes
useEffect(() => {
if (!open) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- Reset state when dialog closes
setTestStatus('idle');
setTestMessage('');
}
}, [open]);
const handleClose = () => {
onOpenChange(false);
setEditingModel(null);
};
const handleTestModel = useCallback(async () => {
if (!editingModel || !apiKey) {
setTestStatus('error');
setTestMessage(t('settings.apiKeyRequired') || 'API Key is required');
return;
}
setTestStatus('testing');
setTestMessage('');
try {
const response = await fetch('/api/verify-model', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
apiKey,
baseUrl,
model: `${providerId}:${editingModel.model.id}`,
providerType,
requiresApiKey,
}),
});
const data = await response.json();
if (data.success) {
setTestStatus('success');
setTestMessage(t('settings.connectionSuccess'));
} else {
setTestStatus('error');
setTestMessage(data.error || t('settings.connectionFailed'));
}
} catch (_error) {
setTestStatus('error');
setTestMessage(t('settings.connectionFailed'));
}
}, [editingModel, apiKey, baseUrl, providerId, providerType, requiresApiKey, t]);
if (!editingModel) return null;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[500px]">
<DialogTitle className="sr-only">
{editingModel.modelIndex === null ? t('settings.addNewModel') : t('settings.editModel')}
</DialogTitle>
<DialogDescription className="sr-only">
{editingModel.modelIndex === null
? t('settings.addNewModelDescription')
: t('settings.editModelDescription')}
</DialogDescription>
<div className="space-y-4">
<div className="pb-3 border-b">
<h2 className="text-lg font-semibold">
{editingModel.modelIndex === null
? t('settings.addNewModel')
: t('settings.editModel')}
</h2>
</div>
{/* Model ID */}
<div className="space-y-2">
<Label>{t('settings.modelId')}</Label>
<Input
placeholder={t('settings.modelIdPlaceholder')}
value={editingModel.model.id}
onChange={(e) => {
const newId = e.target.value;
const currentName = editingModel.model.name;
const currentId = editingModel.model.id;
// Auto-sync name if it's empty or matches the old ID
const shouldSyncName = !currentName || currentName === currentId;
setEditingModel({
...editingModel,
model: {
...editingModel.model,
id: newId,
name: shouldSyncName ? newId : currentName,
},
});
// Reset test status when model ID changes
setTestStatus('idle');
setTestMessage('');
}}
onBlur={() => onAutoSave?.()}
/>
</div>
{/* Display Name */}
<div className="space-y-2">
<Label>{t('settings.modelName')}</Label>
<Input
placeholder={t('settings.modelNamePlaceholder')}
value={editingModel.model.name}
onChange={(e) =>
setEditingModel({
...editingModel,
model: { ...editingModel.model, name: e.target.value },
})
}
onBlur={() => onAutoSave?.()}
/>
</div>
{/* Capabilities */}
<div className="space-y-2">
<Label>{t('settings.modelCapabilities')}</Label>
<div className="flex gap-4">
<div className="flex items-center space-x-2">
<Checkbox
id="cap-vision"
checked={editingModel.model.capabilities?.vision || false}
onCheckedChange={(checked) => {
setEditingModel({
...editingModel,
model: {
...editingModel.model,
capabilities: {
...editingModel.model.capabilities,
vision: checked as boolean,
},
},
});
onAutoSave?.();
}}
/>
<label
htmlFor="cap-vision"
className="text-sm flex items-center gap-1.5 cursor-pointer"
>
<Sparkles className="h-3.5 w-3.5" />
{t('settings.capabilities.vision')}
</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="cap-tools"
checked={editingModel.model.capabilities?.tools || false}
onCheckedChange={(checked) => {
setEditingModel({
...editingModel,
model: {
...editingModel.model,
capabilities: {
...editingModel.model.capabilities,
tools: checked as boolean,
},
},
});
onAutoSave?.();
}}
/>
<label
htmlFor="cap-tools"
className="text-sm flex items-center gap-1.5 cursor-pointer"
>
<Wrench className="h-3.5 w-3.5" />
{t('settings.capabilities.tools')}
</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="cap-streaming"
checked={editingModel.model.capabilities?.streaming || false}
onCheckedChange={(checked) => {
setEditingModel({
...editingModel,
model: {
...editingModel.model,
capabilities: {
...editingModel.model.capabilities,
streaming: checked as boolean,
},
},
});
onAutoSave?.();
}}
/>
<label
htmlFor="cap-streaming"
className="text-sm flex items-center gap-1.5 cursor-pointer"
>
<Zap className="h-3.5 w-3.5" />
{t('settings.capabilities.streaming')}
</label>
</div>
</div>
</div>
{/* Advanced Settings */}
<div className="space-y-3 pt-3 border-t">
<Label className="text-base">{t('settings.advancedSettings')}</Label>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-2">
<Label className="text-sm">{t('settings.contextWindowLabel')}</Label>
<Input
type="number"
placeholder={t('settings.contextWindowPlaceholder')}
value={editingModel.model.contextWindow || ''}
onChange={(e) =>
setEditingModel({
...editingModel,
model: {
...editingModel.model,
contextWindow: e.target.value ? parseInt(e.target.value) : undefined,
},
})
}
onBlur={() => onAutoSave?.()}
/>
</div>
<div className="space-y-2">
<Label className="text-sm">{t('settings.outputWindowLabel')}</Label>
<Input
type="number"
placeholder={t('settings.outputWindowPlaceholder')}
value={editingModel.model.outputWindow || ''}
onChange={(e) =>
setEditingModel({
...editingModel,
model: {
...editingModel.model,
outputWindow: e.target.value ? parseInt(e.target.value) : undefined,
},
})
}
onBlur={() => onAutoSave?.()}
/>
</div>
</div>
</div>
{/* Test Model */}
<div className="space-y-3 pt-3 border-t">
<div className="flex items-center justify-between">
<Label className="text-base">{t('settings.testModel')}</Label>
<Button
variant="outline"
size="sm"
onClick={handleTestModel}
disabled={!editingModel.model.id || testStatus === 'testing'}
className={cn(
testStatus === 'success' && 'border-green-600 text-green-600 hover:bg-green-50',
testStatus === 'error' && 'border-red-600 text-red-600 hover:bg-red-50',
)}
>
{testStatus === 'testing' && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{testStatus === 'success' && <CheckCircle className="mr-2 h-4 w-4" />}
{testStatus === 'error' && <XCircle className="mr-2 h-4 w-4" />}
{testStatus === 'testing' ? t('settings.testing') : t('settings.testConnection')}
</Button>
</div>
{testMessage && (
<div
className={cn(
'rounded-lg p-3 text-sm',
testStatus === 'success' && 'bg-green-50 text-green-700 border border-green-200',
testStatus === 'error' && 'bg-red-50 text-red-700 border border-red-200',
)}
>
<div className="flex items-start gap-2 flex-wrap">
{testStatus === 'success' && <CheckCircle className="h-4 w-4 mt-0.5 shrink-0" />}
{testStatus === 'error' && <XCircle className="h-4 w-4 mt-0.5 shrink-0" />}
<p className="flex-1 break-words">{testMessage}</p>
</div>
</div>
)}
</div>
{/* Footer */}
<div className="flex items-center justify-end gap-2 pt-3 border-t">
<Button variant="outline" size="sm" onClick={handleClose}>
{t('settings.cancelEdit')}
</Button>
<Button size="sm" onClick={onSave}>
{t('settings.saveModel')}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
}