mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-22 10:17:13 +00:00
feat(wizard): improve model scan with search and manual add
- Provider form submit button changed to 'Save & Next' - Remove extra Next button from provider form - Add search input to filter scanned models - Add manual model name input for APIs that don't support scanning - Add divider between scanned list and manual add section - Add i18n translations for all locales
This commit is contained in:
@@ -42,6 +42,7 @@ interface ProviderFormProps {
|
|||||||
providerId?: string;
|
providerId?: string;
|
||||||
initialValues?: ProviderFormInitialValues;
|
initialValues?: ProviderFormInitialValues;
|
||||||
onValuesChange?: (values: ProviderFormInitialValues) => void;
|
onValuesChange?: (values: ProviderFormInitialValues) => void;
|
||||||
|
submitButtonText?: string;
|
||||||
onFormSubmit: (providerUuid?: string) => void;
|
onFormSubmit: (providerUuid?: string) => void;
|
||||||
onFormCancel: () => void;
|
onFormCancel: () => void;
|
||||||
}
|
}
|
||||||
@@ -50,6 +51,7 @@ export default function ProviderForm({
|
|||||||
providerId,
|
providerId,
|
||||||
initialValues,
|
initialValues,
|
||||||
onValuesChange,
|
onValuesChange,
|
||||||
|
submitButtonText,
|
||||||
onFormSubmit,
|
onFormSubmit,
|
||||||
onFormCancel,
|
onFormCancel,
|
||||||
}: ProviderFormProps) {
|
}: ProviderFormProps) {
|
||||||
@@ -399,7 +401,7 @@ export default function ProviderForm({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button type="submit">{t('common.save')}</Button>
|
<Button type="submit">{submitButtonText || t('common.save')}</Button>
|
||||||
<Button type="button" variant="outline" onClick={onFormCancel}>
|
<Button type="button" variant="outline" onClick={onFormCancel}>
|
||||||
{t('common.cancel')}
|
{t('common.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
+122
-50
@@ -51,6 +51,7 @@ import { getAdapterDocUrl } from '@/app/infra/entities/adapter-docs';
|
|||||||
import i18n from 'i18next';
|
import i18n from 'i18next';
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -1151,6 +1152,9 @@ function WizardModelScan({
|
|||||||
);
|
);
|
||||||
const [adding, setAdding] = useState(false);
|
const [adding, setAdding] = useState(false);
|
||||||
const [scanDone, setScanDone] = useState(false);
|
const [scanDone, setScanDone] = useState(false);
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [manualModelName, setManualModelName] = useState('');
|
||||||
|
const [manualAdding, setManualAdding] = useState(false);
|
||||||
|
|
||||||
// Auto-scan on mount
|
// Auto-scan on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1178,6 +1182,12 @@ function WizardModelScan({
|
|||||||
};
|
};
|
||||||
}, [providerUuid]);
|
}, [providerUuid]);
|
||||||
|
|
||||||
|
const filteredModels = searchQuery
|
||||||
|
? scannedModels.filter((m) =>
|
||||||
|
m.name.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||||
|
)
|
||||||
|
: scannedModels;
|
||||||
|
|
||||||
const toggleModel = (name: string) => {
|
const toggleModel = (name: string) => {
|
||||||
setSelectedModels((prev) => {
|
setSelectedModels((prev) => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
@@ -1214,6 +1224,29 @@ function WizardModelScan({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleManualAdd = async () => {
|
||||||
|
const name = manualModelName.trim();
|
||||||
|
if (!providerUuid || !name) return;
|
||||||
|
setManualAdding(true);
|
||||||
|
try {
|
||||||
|
await httpClient.createProviderLLMModel({
|
||||||
|
name,
|
||||||
|
provider_uuid: providerUuid,
|
||||||
|
abilities: ['llm'],
|
||||||
|
reasoning_config: { level: 'provider_default' },
|
||||||
|
context_length: null,
|
||||||
|
extra_args: {},
|
||||||
|
} as never);
|
||||||
|
toast.success(t('wizard.provider.modelsAdded', { count: 1 }));
|
||||||
|
setManualModelName('');
|
||||||
|
onModelsAdded();
|
||||||
|
} catch {
|
||||||
|
toast.error(t('wizard.provider.modelsAddError'));
|
||||||
|
} finally {
|
||||||
|
setManualAdding(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-2xl mx-auto w-full animate-in fade-in slide-in-from-bottom-2 duration-300">
|
<div className="max-w-2xl mx-auto w-full animate-in fade-in slide-in-from-bottom-2 duration-300">
|
||||||
<div className="text-center mb-6">
|
<div className="text-center mb-6">
|
||||||
@@ -1233,55 +1266,101 @@ function WizardModelScan({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!scanning && scanDone && scannedModels.length === 0 && (
|
{!scanning && scanDone && (
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
|
||||||
<p>{t('wizard.provider.noModelsFound')}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!scanning && scannedModels.length > 0 && (
|
|
||||||
<>
|
<>
|
||||||
<div className="max-h-60 overflow-y-auto space-y-2">
|
{/* Search input for scanned models */}
|
||||||
{scannedModels.map((model) => (
|
{scannedModels.length > 0 && (
|
||||||
<label
|
<Input
|
||||||
key={model.name}
|
placeholder={t('wizard.provider.searchModels')}
|
||||||
className="flex items-center gap-3 p-3 rounded-lg border cursor-pointer hover:bg-accent transition-colors"
|
value={searchQuery}
|
||||||
>
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
<input
|
className="mb-2"
|
||||||
type="checkbox"
|
/>
|
||||||
checked={selectedModels.has(model.name)}
|
)}
|
||||||
onChange={() => toggleModel(model.name)}
|
|
||||||
className="w-4 h-4 rounded"
|
{/* Scanned models list */}
|
||||||
/>
|
{scannedModels.length > 0 ? (
|
||||||
<div className="flex-1 min-w-0">
|
<>
|
||||||
<span className="text-sm font-medium truncate block">
|
<div className="max-h-48 overflow-y-auto space-y-2">
|
||||||
{model.name}
|
{filteredModels.map((model) => (
|
||||||
</span>
|
<label
|
||||||
{model.context_length && (
|
key={model.name}
|
||||||
<span className="text-xs text-muted-foreground">
|
className="flex items-center gap-3 p-3 rounded-lg border cursor-pointer hover:bg-accent transition-colors"
|
||||||
ctx: {model.context_length.toLocaleString()}
|
>
|
||||||
</span>
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedModels.has(model.name)}
|
||||||
|
onChange={() => toggleModel(model.name)}
|
||||||
|
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>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
{filteredModels.length === 0 && (
|
||||||
|
<p className="text-sm text-muted-foreground text-center py-4">
|
||||||
|
{t('wizard.provider.noMatch')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3 pt-2">
|
||||||
|
<Button
|
||||||
|
onClick={handleAddSelected}
|
||||||
|
disabled={selectedModels.size === 0 || adding}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
{adding && (
|
||||||
|
<Loader2 className="w-4 h-4 mr-1.5 animate-spin" />
|
||||||
)}
|
)}
|
||||||
</div>
|
{t('wizard.provider.addSelected', {
|
||||||
</label>
|
count: selectedModels.size,
|
||||||
))}
|
})}
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" onClick={onModelsAdded}>
|
||||||
|
{t('wizard.provider.skipModelAdd')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-4 text-muted-foreground">
|
||||||
|
<p>{t('wizard.provider.noModelsFound')}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
<div className="flex items-center gap-3 py-2">
|
||||||
|
<div className="flex-1 h-px bg-border" />
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{t('wizard.provider.orDivider')}
|
||||||
|
</span>
|
||||||
|
<div className="flex-1 h-px bg-border" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-3 pt-2">
|
{/* Manual add */}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder={t('wizard.provider.manualModelPlaceholder')}
|
||||||
|
value={manualModelName}
|
||||||
|
onChange={(e) => setManualModelName(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && handleManualAdd()}
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleAddSelected}
|
onClick={handleManualAdd}
|
||||||
disabled={selectedModels.size === 0 || adding}
|
disabled={!manualModelName.trim() || manualAdding}
|
||||||
className="flex-1"
|
|
||||||
>
|
>
|
||||||
{adding && (
|
{manualAdding && (
|
||||||
<Loader2 className="w-4 h-4 mr-1.5 animate-spin" />
|
<Loader2 className="w-4 h-4 mr-1.5 animate-spin" />
|
||||||
)}
|
)}
|
||||||
{t('wizard.provider.addSelected', {
|
{t('wizard.provider.addManual')}
|
||||||
count: selectedModels.size,
|
|
||||||
})}
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" onClick={onModelsAdded}>
|
|
||||||
{t('wizard.provider.skipModelAdd')}
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -1540,23 +1619,16 @@ function StepAIEngine({
|
|||||||
{t('wizard.provider.description')}
|
{t('wizard.provider.description')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="border rounded-lg p-6 bg-card space-y-4">
|
<div className="border rounded-lg p-6 bg-card">
|
||||||
<ProviderForm
|
<ProviderForm
|
||||||
initialValues={savedProviderForm}
|
initialValues={savedProviderForm}
|
||||||
onValuesChange={onSavedProviderFormChange}
|
onValuesChange={onSavedProviderFormChange}
|
||||||
|
submitButtonText={t('wizard.provider.saveAndNext')}
|
||||||
onFormSubmit={(uuid) => {
|
onFormSubmit={(uuid) => {
|
||||||
onProviderCreated(uuid);
|
onProviderCreated(uuid);
|
||||||
}}
|
}}
|
||||||
onFormCancel={onResetModelSource}
|
onFormCancel={onResetModelSource}
|
||||||
/>
|
/>
|
||||||
{providerCreated && (
|
|
||||||
<div className="flex justify-end pt-2 border-t">
|
|
||||||
<Button onClick={onGoToNextLayer}>
|
|
||||||
{t('wizard.next')}
|
|
||||||
<ArrowRight className="w-4 h-4 ml-1.5" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1867,6 +1867,12 @@ const enUS = {
|
|||||||
modelsAdded: 'Added {{count}} model(s)',
|
modelsAdded: 'Added {{count}} model(s)',
|
||||||
modelsAddError: 'Failed to add models',
|
modelsAddError: 'Failed to add models',
|
||||||
skipModelAdd: 'Skip, add later',
|
skipModelAdd: 'Skip, add later',
|
||||||
|
searchModels: 'Search models...',
|
||||||
|
noMatch: 'No matching models',
|
||||||
|
orDivider: 'or',
|
||||||
|
manualModelPlaceholder: 'Enter model name to add manually',
|
||||||
|
addManual: 'Add',
|
||||||
|
saveAndNext: 'Save & Next',
|
||||||
},
|
},
|
||||||
modelSource: {
|
modelSource: {
|
||||||
title: 'Choose Model Source',
|
title: 'Choose Model Source',
|
||||||
|
|||||||
@@ -1725,6 +1725,12 @@ const esES = {
|
|||||||
modelsAdded: '{{count}} modelo(s) agregado(s)',
|
modelsAdded: '{{count}} modelo(s) agregado(s)',
|
||||||
modelsAddError: 'Error al agregar modelos',
|
modelsAddError: 'Error al agregar modelos',
|
||||||
skipModelAdd: 'Omitir, agregar después',
|
skipModelAdd: 'Omitir, agregar después',
|
||||||
|
searchModels: 'Buscar modelos...',
|
||||||
|
noMatch: 'No hay modelos coincidentes',
|
||||||
|
orDivider: 'o',
|
||||||
|
manualModelPlaceholder: 'Ingresa el nombre del modelo para agregar manualmente',
|
||||||
|
addManual: 'Agregar',
|
||||||
|
saveAndNext: 'Guardar y siguiente',
|
||||||
},
|
},
|
||||||
modelSource: {
|
modelSource: {
|
||||||
title: 'Elegir fuente del modelo',
|
title: 'Elegir fuente del modelo',
|
||||||
|
|||||||
@@ -1786,6 +1786,12 @@ const jaJP = {
|
|||||||
modelsAdded: '{{count}}個のモデルを追加しました',
|
modelsAdded: '{{count}}個のモデルを追加しました',
|
||||||
modelsAddError: 'モデルの追加に失敗しました',
|
modelsAddError: 'モデルの追加に失敗しました',
|
||||||
skipModelAdd: 'スキップ(後で追加)',
|
skipModelAdd: 'スキップ(後で追加)',
|
||||||
|
searchModels: 'モデルを検索...',
|
||||||
|
noMatch: '一致するモデルがありません',
|
||||||
|
orDivider: 'または',
|
||||||
|
manualModelPlaceholder: 'モデル名を入力して手動追加',
|
||||||
|
addManual: '追加',
|
||||||
|
saveAndNext: '保存して次へ',
|
||||||
},
|
},
|
||||||
modelSource: {
|
modelSource: {
|
||||||
title: 'モデルソースを選択',
|
title: 'モデルソースを選択',
|
||||||
|
|||||||
@@ -1695,6 +1695,12 @@ const ruRU = {
|
|||||||
modelsAdded: 'Добавлено {{count}} модель(ей)',
|
modelsAdded: 'Добавлено {{count}} модель(ей)',
|
||||||
modelsAddError: 'Ошибка добавления моделей',
|
modelsAddError: 'Ошибка добавления моделей',
|
||||||
skipModelAdd: 'Пропустить, добавить позже',
|
skipModelAdd: 'Пропустить, добавить позже',
|
||||||
|
searchModels: 'Поиск моделей...',
|
||||||
|
noMatch: 'Нет подходящих моделей',
|
||||||
|
orDivider: 'или',
|
||||||
|
manualModelPlaceholder: 'Введите имя модели для ручного добавления',
|
||||||
|
addManual: 'Добавить',
|
||||||
|
saveAndNext: 'Сохранить и далее',
|
||||||
},
|
},
|
||||||
modelSource: {
|
modelSource: {
|
||||||
title: 'Выберите источник модели',
|
title: 'Выберите источник модели',
|
||||||
|
|||||||
@@ -1659,6 +1659,12 @@ const thTH = {
|
|||||||
modelsAdded: 'เพิ่ม {{count}} โมเดลแล้ว',
|
modelsAdded: 'เพิ่ม {{count}} โมเดลแล้ว',
|
||||||
modelsAddError: 'เพิ่มโมเดลไม่สำเร็จ',
|
modelsAddError: 'เพิ่มโมเดลไม่สำเร็จ',
|
||||||
skipModelAdd: 'ข้าม เพิ่มทีหลัง',
|
skipModelAdd: 'ข้าม เพิ่มทีหลัง',
|
||||||
|
searchModels: 'ค้นหาโมเดล...',
|
||||||
|
noMatch: 'ไม่พบโมเดลที่ตรงกัน',
|
||||||
|
orDivider: 'หรือ',
|
||||||
|
manualModelPlaceholder: 'กรอกชื่อโมเดลเพื่อเพิ่มด้วยตนเอง',
|
||||||
|
addManual: 'เพิ่ม',
|
||||||
|
saveAndNext: 'บันทึกและถัดไป',
|
||||||
},
|
},
|
||||||
modelSource: {
|
modelSource: {
|
||||||
title: 'เลือกแหล่งโมเดล',
|
title: 'เลือกแหล่งโมเดล',
|
||||||
|
|||||||
@@ -1685,6 +1685,12 @@ const viVN = {
|
|||||||
modelsAdded: 'Đã thêm {{count}} mô hình',
|
modelsAdded: 'Đã thêm {{count}} mô hình',
|
||||||
modelsAddError: 'Thêm mô hình thất bại',
|
modelsAddError: 'Thêm mô hình thất bại',
|
||||||
skipModelAdd: 'Bỏ qua, thêm sau',
|
skipModelAdd: 'Bỏ qua, thêm sau',
|
||||||
|
searchModels: 'Tìm kiếm mô hình...',
|
||||||
|
noMatch: 'Không tìm thấy mô hình phù hợp',
|
||||||
|
orDivider: 'hoặc',
|
||||||
|
manualModelPlaceholder: 'Nhập tên mô hình để thêm thủ công',
|
||||||
|
addManual: 'Thêm',
|
||||||
|
saveAndNext: 'Lưu & Tiếp theo',
|
||||||
},
|
},
|
||||||
modelSource: {
|
modelSource: {
|
||||||
title: 'Chọn nguồn mô hình',
|
title: 'Chọn nguồn mô hình',
|
||||||
|
|||||||
@@ -1783,6 +1783,12 @@ const zhHans = {
|
|||||||
modelsAdded: '已添加 {{count}} 个模型',
|
modelsAdded: '已添加 {{count}} 个模型',
|
||||||
modelsAddError: '添加模型失败',
|
modelsAddError: '添加模型失败',
|
||||||
skipModelAdd: '跳过,稍后添加',
|
skipModelAdd: '跳过,稍后添加',
|
||||||
|
searchModels: '搜索模型...',
|
||||||
|
noMatch: '没有匹配的模型',
|
||||||
|
orDivider: '或',
|
||||||
|
manualModelPlaceholder: '输入模型名称手动添加',
|
||||||
|
addManual: '添加',
|
||||||
|
saveAndNext: '保存并下一步',
|
||||||
},
|
},
|
||||||
modelSource: {
|
modelSource: {
|
||||||
title: '选择模型来源',
|
title: '选择模型来源',
|
||||||
|
|||||||
@@ -1606,6 +1606,12 @@ const zhHant = {
|
|||||||
modelsAdded: '已新增 {{count}} 個模型',
|
modelsAdded: '已新增 {{count}} 個模型',
|
||||||
modelsAddError: '新增模型失敗',
|
modelsAddError: '新增模型失敗',
|
||||||
skipModelAdd: '跳過,稍後新增',
|
skipModelAdd: '跳過,稍後新增',
|
||||||
|
searchModels: '搜尋模型...',
|
||||||
|
noMatch: '沒有符合的模型',
|
||||||
|
orDivider: '或',
|
||||||
|
manualModelPlaceholder: '輸入模型名稱手動新增',
|
||||||
|
addManual: '新增',
|
||||||
|
saveAndNext: '儲存並下一步',
|
||||||
},
|
},
|
||||||
modelSource: {
|
modelSource: {
|
||||||
title: '選擇模型來源',
|
title: '選擇模型來源',
|
||||||
|
|||||||
Reference in New Issue
Block a user