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: '選擇模型來源',