From d06c4287bfe673226274325d4e251127963bedfe Mon Sep 17 00:00:00 2001 From: langbot-dev Date: Wed, 12 Aug 2026 01:51:28 +0800 Subject: [PATCH] feat(wizard): add model scan step after provider creation After creating a custom provider, scan for available LLM models and let the user select which ones to add before proceeding. Support back navigation from config form to model scan to provider form. Pass provider UUID from ProviderForm callback. --- .../component/provider-form/ProviderForm.tsx | 7 +- web/src/app/wizard/page.tsx | 219 +++++++++++++++++- web/src/i18n/locales/en-US.ts | 8 + web/src/i18n/locales/es-ES.ts | 9 + web/src/i18n/locales/ja-JP.ts | 9 + web/src/i18n/locales/ru-RU.ts | 9 + web/src/i18n/locales/th-TH.ts | 9 + web/src/i18n/locales/vi-VN.ts | 9 + web/src/i18n/locales/zh-Hans.ts | 8 + web/src/i18n/locales/zh-Hant.ts | 8 + 10 files changed, 287 insertions(+), 8 deletions(-) diff --git a/web/src/app/home/components/models-dialog/component/provider-form/ProviderForm.tsx b/web/src/app/home/components/models-dialog/component/provider-form/ProviderForm.tsx index ae20bb254..b59127cee 100644 --- a/web/src/app/home/components/models-dialog/component/provider-form/ProviderForm.tsx +++ b/web/src/app/home/components/models-dialog/component/provider-form/ProviderForm.tsx @@ -33,7 +33,7 @@ const getFormSchema = (t: (key: string) => string) => interface ProviderFormProps { providerId?: string; - onFormSubmit: () => void; + onFormSubmit: (providerUuid?: string) => void; onFormCancel: () => void; } @@ -174,11 +174,12 @@ export default function ProviderForm({ if (providerId) { await httpClient.updateModelProvider(providerId, data); toast.success(t('models.providerSaved')); + onFormSubmit(); } else { - await httpClient.createModelProvider(data); + const resp = await httpClient.createModelProvider(data); toast.success(t('models.providerCreated')); + onFormSubmit(resp.uuid); } - onFormSubmit(); } catch (err) { toast.error(t('models.providerSaveError') + (err as CustomApiError).msg); } diff --git a/web/src/app/wizard/page.tsx b/web/src/app/wizard/page.tsx index 3b5d3b7fb..375f2df34 100644 --- a/web/src/app/wizard/page.tsx +++ b/web/src/app/wizard/page.tsx @@ -103,6 +103,10 @@ export default function WizardPage() { null, ); const [providerCreated, setProviderCreated] = useState(false); + const [createdProviderUuid, setCreatedProviderUuid] = useState( + null, + ); + const [modelsAdded, setModelsAdded] = useState(false); const [createdBotUuid, setCreatedBotUuid] = useState(null); const [webhookUrl, setWebhookUrl] = useState(''); const [extraWebhookUrl, setExtraWebhookUrl] = useState(''); @@ -314,7 +318,7 @@ export default function WizardPage() { if (aiEngineMode === 'llm') { return ( modelSource === 'space' || - (modelSource === 'custom' && providerCreated && selectedRunner !== null) + (modelSource === 'custom' && modelsAdded) ); } return false; @@ -343,10 +347,28 @@ export default function WizardPage() { const goPrev = useCallback(() => { if (currentStep === 2) { // Intra-step back navigation for AI Engine step - if (selectedRunner && aiEngineMode === 'orchestrated') { + if (aiEngineMode === 'orchestrated' && selectedRunner) { setSelectedRunner(null); return; } + // LLM + custom: config form → model scan + if (aiEngineMode === 'llm' && modelSource === 'custom' && modelsAdded) { + setModelsAdded(false); + setSelectedRunner(null); + return; + } + // LLM + custom: model scan → provider form + if ( + aiEngineMode === 'llm' && + modelSource === 'custom' && + providerCreated && + !modelsAdded + ) { + setProviderCreated(false); + setCreatedProviderUuid(null); + return; + } + // LLM + custom: provider form → model source if ( aiEngineMode === 'llm' && modelSource === 'custom' && @@ -355,10 +377,12 @@ export default function WizardPage() { setModelSource(null); return; } + // LLM: model source → main choice if (modelSource) { setModelSource(null); return; } + // Main choice → previous step if (aiEngineMode) { setAiEngineMode(null); return; @@ -369,7 +393,7 @@ export default function WizardPage() { setCurrentStep(prevStep); saveProgress({ step: prevStep }); } - }, [currentStep, saveProgress, aiEngineMode, selectedRunner, modelSource, providerCreated]); + }, [currentStep, saveProgress, aiEngineMode, selectedRunner, modelSource, providerCreated, modelsAdded]); // ---- Create Bot (Step 0) ---- // Creates a disabled bot using the adapter label as name. @@ -741,8 +765,14 @@ export default function WizardPage() { modelSource={modelSource} onModelSourceSelect={handleModelSourceSelect} providerCreated={providerCreated} - onProviderCreated={() => { + createdProviderUuid={createdProviderUuid} + modelsAdded={modelsAdded} + onProviderCreated={(uuid) => { setProviderCreated(true); + setCreatedProviderUuid(uuid ?? null); + }} + onModelsAdded={() => { + setModelsAdded(true); handleSelectRunner('local-agent'); }} onResetModelSource={() => setModelSource(null)} @@ -1080,6 +1110,164 @@ function StepBotConfig({ ); } +// --------------------------------------------------------------------------- +// Model scan sub-component (used within Step 2) +// --------------------------------------------------------------------------- + +function WizardModelScan({ + providerUuid, + onModelsAdded, +}: { + providerUuid: string | null; + onModelsAdded: () => void; +}) { + const { t } = useTranslation(); + const [scanning, setScanning] = useState(false); + const [scannedModels, setScannedModels] = useState< + { name: string; context_length?: number | null }[] + >([]); + const [selectedModels, setSelectedModels] = useState>(new Set()); + const [adding, setAdding] = useState(false); + const [scanDone, setScanDone] = useState(false); + + // Auto-scan on mount + useEffect(() => { + if (!providerUuid) return; + let cancelled = false; + (async () => { + setScanning(true); + try { + const resp = await httpClient.scanProviderModels( + providerUuid, + 'llm', + ); + if (!cancelled) { + setScannedModels(resp.models ?? []); + setScanDone(true); + } + } catch { + if (!cancelled) setScanDone(true); + } finally { + if (!cancelled) setScanning(false); + } + })(); + return () => { + cancelled = true; + }; + }, [providerUuid]); + + const toggleModel = (name: string) => { + setSelectedModels((prev) => { + const next = new Set(prev); + if (next.has(name)) next.delete(name); + else next.add(name); + return next; + }); + }; + + const handleAddSelected = async () => { + if (!providerUuid || selectedModels.size === 0) return; + setAdding(true); + try { + for (const name of selectedModels) { + const model = scannedModels.find((m) => m.name === name); + await httpClient.createProviderLLMModel({ + name, + provider_uuid: providerUuid, + abilities: ['llm'], + reasoning_config: { enabled: false } as never, + context_length: model?.context_length ?? null, + extra_args: {}, + } as never); + } + toast.success( + t('wizard.provider.modelsAdded', { count: selectedModels.size }), + ); + onModelsAdded(); + } catch { + toast.error(t('wizard.provider.modelsAddError')); + } finally { + setAdding(false); + } + }; + + return ( +
+
+

+ {t('wizard.provider.scanTitle')} +

+

+ {t('wizard.provider.scanDescription')} +

+
+ +
+ {scanning && ( +
+ + {t('wizard.provider.scanning')} +
+ )} + + {!scanning && scanDone && scannedModels.length === 0 && ( +
+

{t('wizard.provider.noModelsFound')}

+
+ )} + + {!scanning && scannedModels.length > 0 && ( + <> +
+ {scannedModels.map((model) => ( + + ))} +
+ +
+ + +
+ + )} +
+
+ ); +} + // --------------------------------------------------------------------------- // Step 2: Select & Configure AI Engine // --------------------------------------------------------------------------- @@ -1095,7 +1283,10 @@ function StepAIEngine({ modelSource, onModelSourceSelect, providerCreated, + createdProviderUuid, + modelsAdded, onProviderCreated, + onModelsAdded, onResetModelSource, runnerConfigItems, runnerConfigValues, @@ -1111,7 +1302,10 @@ function StepAIEngine({ modelSource: 'space' | 'custom' | null; onModelSourceSelect: (source: 'space' | 'custom') => void; providerCreated: boolean; - onProviderCreated: () => void; + createdProviderUuid: string | null; + modelsAdded: boolean; + onProviderCreated: (uuid?: string) => void; + onModelsAdded: () => void; onResetModelSource: () => void; runnerConfigItems: IDynamicFormItemSchema[]; runnerConfigValues: Record; @@ -1325,6 +1519,21 @@ function StepAIEngine({ ); } + // ---- Layer 2.5: Model scan & add after provider creation ---- + if ( + aiEngineMode === 'llm' && + modelSource === 'custom' && + providerCreated && + !modelsAdded + ) { + return ( + + ); + } + // ---- Layer 3: Runner configuration form ---- return (
diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index f3b0b3b56..e32037e87 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -1859,6 +1859,14 @@ const enUS = { title: 'Add Model Provider', description: 'Add your own model provider. Configure the API key to get started.', + scanTitle: 'Scan Available Models', + scanDescription: 'Scanning your provider for available LLM models.', + scanning: 'Scanning models...', + noModelsFound: 'No models found. Please check your provider config.', + addSelected: 'Add {{count}} selected model(s)', + modelsAdded: 'Added {{count}} model(s)', + modelsAddError: 'Failed to add models', + skipModelAdd: 'Skip, add later', }, modelSource: { title: 'Choose Model Source', diff --git a/web/src/i18n/locales/es-ES.ts b/web/src/i18n/locales/es-ES.ts index 02e467cbc..36c92a74a 100644 --- a/web/src/i18n/locales/es-ES.ts +++ b/web/src/i18n/locales/es-ES.ts @@ -1716,6 +1716,15 @@ const esES = { title: 'Agregar proveedor de modelos', description: 'Agrega tu propio proveedor de modelos. Configura la API key para comenzar.', + scanTitle: 'Escanear modelos disponibles', + scanDescription: 'Escaneando modelos LLM de tu proveedor.', + scanning: 'Escaneando modelos...', + noModelsFound: + 'No se encontraron modelos. Verifica la configuración del proveedor.', + addSelected: 'Agregar {{count}} modelo(s) seleccionado(s)', + modelsAdded: '{{count}} modelo(s) agregado(s)', + modelsAddError: 'Error al agregar modelos', + skipModelAdd: 'Omitir, agregar después', }, modelSource: { title: 'Elegir fuente del modelo', diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index 5143a262d..0296d8a5b 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -1777,6 +1777,15 @@ const jaJP = { title: 'モデルプロバイダーを追加', description: '独自のモデルプロバイダーを追加します。APIキーを設定すると使用可能になります。', + scanTitle: '利用可能なモデルをスキャン', + scanDescription: 'プロバイダーから利用可能なLLMモデルをスキャン中です。', + scanning: 'モデルをスキャン中...', + noModelsFound: + 'モデルが見つかりません。プロバイダー設定を確認してください。', + addSelected: '選択した{{count}}個のモデルを追加', + modelsAdded: '{{count}}個のモデルを追加しました', + modelsAddError: 'モデルの追加に失敗しました', + skipModelAdd: 'スキップ(後で追加)', }, modelSource: { title: 'モデルソースを選択', diff --git a/web/src/i18n/locales/ru-RU.ts b/web/src/i18n/locales/ru-RU.ts index d94675809..c9b949166 100644 --- a/web/src/i18n/locales/ru-RU.ts +++ b/web/src/i18n/locales/ru-RU.ts @@ -1686,6 +1686,15 @@ const ruRU = { title: 'Добавить провайдера моделей', description: 'Добавьте своего провайдера моделей. Настройте API-ключ для начала работы.', + scanTitle: 'Сканирование доступных моделей', + scanDescription: 'Сканирование LLM-моделей у вашего провайдера.', + scanning: 'Сканирование моделей...', + noModelsFound: + 'Модели не найдены. Проверьте настройки провайдера.', + addSelected: 'Добавить {{count}} выбранную(ых)', + modelsAdded: 'Добавлено {{count}} модель(ей)', + modelsAddError: 'Ошибка добавления моделей', + skipModelAdd: 'Пропустить, добавить позже', }, modelSource: { title: 'Выберите источник модели', diff --git a/web/src/i18n/locales/th-TH.ts b/web/src/i18n/locales/th-TH.ts index 3b036ed37..6d6ff9692 100644 --- a/web/src/i18n/locales/th-TH.ts +++ b/web/src/i18n/locales/th-TH.ts @@ -1650,6 +1650,15 @@ const thTH = { title: 'เพิ่มผู้ให้บริการโมเดล', description: 'เพิ่มผู้ให้บริการโมเดลของคุณเอง กำหนดค่า API key เพื่อเริ่มต้น', + scanTitle: 'สแกนโมเดลที่ใช้ได้', + scanDescription: 'กำลังสแกนโมเดล LLM จากผู้ให้บริการของคุณ', + scanning: 'กำลังสแกนโมเดล...', + noModelsFound: + 'ไม่พบโมเดล กรุณาตรวจสอบการตั้งค่าผู้ให้บริการ', + addSelected: 'เพิ่ม {{count}} โมเดลที่เลือก', + modelsAdded: 'เพิ่ม {{count}} โมเดลแล้ว', + modelsAddError: 'เพิ่มโมเดลไม่สำเร็จ', + skipModelAdd: 'ข้าม เพิ่มทีหลัง', }, modelSource: { title: 'เลือกแหล่งโมเดล', diff --git a/web/src/i18n/locales/vi-VN.ts b/web/src/i18n/locales/vi-VN.ts index e0281d52a..1de57a816 100644 --- a/web/src/i18n/locales/vi-VN.ts +++ b/web/src/i18n/locales/vi-VN.ts @@ -1676,6 +1676,15 @@ const viVN = { title: 'Thêm nhà cung cấp mô hình', description: 'Thêm nhà cung cấp mô hình của bạn. Cấu hình API key để bắt đầu.', + scanTitle: 'Quét mô hình khả dụng', + scanDescription: 'Đang quét mô hình LLM từ nhà cung cấp của bạn.', + scanning: 'Đang quét mô hình...', + noModelsFound: + 'Không tìm thấy mô hình. Vui lòng kiểm tra cấu hình nhà cung cấp.', + addSelected: 'Thêm {{count}} mô hình đã chọn', + modelsAdded: 'Đã thêm {{count}} mô hình', + modelsAddError: 'Thêm mô hình thất bại', + skipModelAdd: 'Bỏ qua, thêm sau', }, modelSource: { title: 'Chọn nguồn mô hình', diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index 0a743d4df..66d70aa7a 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -1775,6 +1775,14 @@ const zhHans = { provider: { title: '添加模型供应商', description: '添加你自己的模型供应商,配置 API Key 后即可使用。', + scanTitle: '扫描可用模型', + scanDescription: '正在从你的供应商中扫描可用的 LLM 模型。', + scanning: '正在扫描模型...', + noModelsFound: '未发现可用模型,请检查供应商配置。', + addSelected: '添加选中的 {{count}} 个模型', + modelsAdded: '已添加 {{count}} 个模型', + modelsAddError: '添加模型失败', + skipModelAdd: '跳过,稍后添加', }, modelSource: { title: '选择模型来源', diff --git a/web/src/i18n/locales/zh-Hant.ts b/web/src/i18n/locales/zh-Hant.ts index b1d65ad65..f9e631efd 100644 --- a/web/src/i18n/locales/zh-Hant.ts +++ b/web/src/i18n/locales/zh-Hant.ts @@ -1598,6 +1598,14 @@ const zhHant = { provider: { title: '新增模型供應商', description: '新增你自己的模型供應商,配置 API Key 後即可使用。', + scanTitle: '掃描可用模型', + scanDescription: '正在從你的供應商中掃描可用的 LLM 模型。', + scanning: '正在掃描模型...', + noModelsFound: '未發現可用模型,請檢查供應商配置。', + addSelected: '新增選取的 {{count}} 個模型', + modelsAdded: '已新增 {{count}} 個模型', + modelsAddError: '新增模型失敗', + skipModelAdd: '跳過,稍後新增', }, modelSource: { title: '選擇模型來源',