fix(wizard): prevent duplicate model additions in scan page

- Track added model names in state
- Disable already-added models with 'Added' badge
- Clear selection after successful add
- Add Next button at bottom when models have been added
This commit is contained in:
langbot-dev
2026-08-12 02:34:30 +08:00
parent 72c03276ff
commit 8b0f2f58b1
9 changed files with 64 additions and 23 deletions
+56 -23
View File
@@ -1155,6 +1155,9 @@ function WizardModelScan({
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [manualModelName, setManualModelName] = useState(''); const [manualModelName, setManualModelName] = useState('');
const [manualAdding, setManualAdding] = useState(false); const [manualAdding, setManualAdding] = useState(false);
const [addedModelNames, setAddedModelNames] = useState<Set<string>>(
new Set(initialSelectedModels),
);
// Auto-scan on mount // Auto-scan on mount
useEffect(() => { useEffect(() => {
@@ -1216,7 +1219,13 @@ function WizardModelScan({
toast.success( toast.success(
t('wizard.provider.modelsAdded', { count: selectedModels.size }), t('wizard.provider.modelsAdded', { count: selectedModels.size }),
); );
onModelsAdded(); setAddedModelNames((prev) => {
const next = new Set(prev);
for (const name of selectedModels) next.add(name);
return next;
});
setSelectedModels(new Set());
onSelectedModelsChange(new Set());
} catch { } catch {
toast.error(t('wizard.provider.modelsAddError')); toast.error(t('wizard.provider.modelsAddError'));
} finally { } finally {
@@ -1238,8 +1247,8 @@ function WizardModelScan({
extra_args: {}, extra_args: {},
} as never); } as never);
toast.success(t('wizard.provider.modelsAdded', { count: 1 })); toast.success(t('wizard.provider.modelsAdded', { count: 1 }));
setAddedModelNames((prev) => new Set(prev).add(name));
setManualModelName(''); setManualModelName('');
onModelsAdded();
} catch { } catch {
toast.error(t('wizard.provider.modelsAddError')); toast.error(t('wizard.provider.modelsAddError'));
} finally { } finally {
@@ -1282,29 +1291,43 @@ function WizardModelScan({
{scannedModels.length > 0 ? ( {scannedModels.length > 0 ? (
<> <>
<div className="max-h-48 overflow-y-auto space-y-2"> <div className="max-h-48 overflow-y-auto space-y-2">
{filteredModels.map((model) => ( {filteredModels.map((model) => {
<label const isAdded = addedModelNames.has(model.name);
key={model.name} return (
className="flex items-center gap-3 p-3 rounded-lg border cursor-pointer hover:bg-accent transition-colors" <label
> key={model.name}
<input className={cn(
type="checkbox" 'flex items-center gap-3 p-3 rounded-lg border transition-colors',
checked={selectedModels.has(model.name)} isAdded
onChange={() => toggleModel(model.name)} ? 'opacity-50 cursor-not-allowed bg-muted/50'
className="w-4 h-4 rounded" : 'cursor-pointer hover:bg-accent',
/> )}
<div className="flex-1 min-w-0"> >
<span className="text-sm font-medium truncate block"> <input
{model.name} type="checkbox"
</span> checked={selectedModels.has(model.name) || isAdded}
{model.context_length && ( disabled={isAdded}
<span className="text-xs text-muted-foreground"> onChange={() => toggleModel(model.name)}
ctx: {model.context_length.toLocaleString()} className="w-4 h-4 rounded"
/>
<div className="flex-1 min-w-0">
<span className="text-sm font-medium truncate block">
{model.name}
</span>
{model.context_length && (
<span className="text-xs text-muted-foreground">
ctx: {model.context_length.toLocaleString()}
</span>
)}
</div>
{isAdded && (
<span className="text-xs text-green-600 font-medium shrink-0">
{t('wizard.provider.alreadyAdded')}
</span> </span>
)} )}
</div> </label>
</label> );
))} })}
{filteredModels.length === 0 && ( {filteredModels.length === 0 && (
<p className="text-sm text-muted-foreground text-center py-4"> <p className="text-sm text-muted-foreground text-center py-4">
{t('wizard.provider.noMatch')} {t('wizard.provider.noMatch')}
@@ -1365,6 +1388,16 @@ function WizardModelScan({
</div> </div>
</> </>
)} )}
{/* Next button when models have been added */}
{addedModelNames.size > 0 && (
<div className="flex justify-end pt-2 border-t">
<Button onClick={onModelsAdded}>
{t('wizard.next')}
<ArrowRight className="w-4 h-4 ml-1.5" />
</Button>
</div>
)}
</div> </div>
</div> </div>
); );
+1
View File
@@ -1873,6 +1873,7 @@ const enUS = {
manualModelPlaceholder: 'Enter model name to add manually', manualModelPlaceholder: 'Enter model name to add manually',
addManual: 'Add', addManual: 'Add',
saveAndNext: 'Save & Next', saveAndNext: 'Save & Next',
alreadyAdded: 'Added',
}, },
modelSource: { modelSource: {
title: 'Choose Model Source', title: 'Choose Model Source',
+1
View File
@@ -1731,6 +1731,7 @@ const esES = {
manualModelPlaceholder: 'Ingresa el nombre del modelo para agregar manualmente', manualModelPlaceholder: 'Ingresa el nombre del modelo para agregar manualmente',
addManual: 'Agregar', addManual: 'Agregar',
saveAndNext: 'Guardar y siguiente', saveAndNext: 'Guardar y siguiente',
alreadyAdded: 'Agregado',
}, },
modelSource: { modelSource: {
title: 'Elegir fuente del modelo', title: 'Elegir fuente del modelo',
+1
View File
@@ -1792,6 +1792,7 @@ const jaJP = {
manualModelPlaceholder: 'モデル名を入力して手動追加', manualModelPlaceholder: 'モデル名を入力して手動追加',
addManual: '追加', addManual: '追加',
saveAndNext: '保存して次へ', saveAndNext: '保存して次へ',
alreadyAdded: '追加済み',
}, },
modelSource: { modelSource: {
title: 'モデルソースを選択', title: 'モデルソースを選択',
+1
View File
@@ -1701,6 +1701,7 @@ const ruRU = {
manualModelPlaceholder: 'Введите имя модели для ручного добавления', manualModelPlaceholder: 'Введите имя модели для ручного добавления',
addManual: 'Добавить', addManual: 'Добавить',
saveAndNext: 'Сохранить и далее', saveAndNext: 'Сохранить и далее',
alreadyAdded: 'Добавлено',
}, },
modelSource: { modelSource: {
title: 'Выберите источник модели', title: 'Выберите источник модели',
+1
View File
@@ -1665,6 +1665,7 @@ const thTH = {
manualModelPlaceholder: 'กรอกชื่อโมเดลเพื่อเพิ่มด้วยตนเอง', manualModelPlaceholder: 'กรอกชื่อโมเดลเพื่อเพิ่มด้วยตนเอง',
addManual: 'เพิ่ม', addManual: 'เพิ่ม',
saveAndNext: 'บันทึกและถัดไป', saveAndNext: 'บันทึกและถัดไป',
alreadyAdded: 'เพิ่มแล้ว',
}, },
modelSource: { modelSource: {
title: 'เลือกแหล่งโมเดล', title: 'เลือกแหล่งโมเดล',
+1
View File
@@ -1691,6 +1691,7 @@ const viVN = {
manualModelPlaceholder: 'Nhập tên mô hình để thêm thủ công', manualModelPlaceholder: 'Nhập tên mô hình để thêm thủ công',
addManual: 'Thêm', addManual: 'Thêm',
saveAndNext: 'Lưu & Tiếp theo', saveAndNext: 'Lưu & Tiếp theo',
alreadyAdded: 'Đã thêm',
}, },
modelSource: { modelSource: {
title: 'Chọn nguồn mô hình', title: 'Chọn nguồn mô hình',
+1
View File
@@ -1789,6 +1789,7 @@ const zhHans = {
manualModelPlaceholder: '输入模型名称手动添加', manualModelPlaceholder: '输入模型名称手动添加',
addManual: '添加', addManual: '添加',
saveAndNext: '保存并下一步', saveAndNext: '保存并下一步',
alreadyAdded: '已添加',
}, },
modelSource: { modelSource: {
title: '选择模型来源', title: '选择模型来源',
+1
View File
@@ -1612,6 +1612,7 @@ const zhHant = {
manualModelPlaceholder: '輸入模型名稱手動新增', manualModelPlaceholder: '輸入模型名稱手動新增',
addManual: '新增', addManual: '新增',
saveAndNext: '儲存並下一步', saveAndNext: '儲存並下一步',
alreadyAdded: '已新增',
}, },
modelSource: { modelSource: {
title: '選擇模型來源', title: '選擇模型來源',