mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-21 17:57:14 +00:00
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.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
+214
-5
@@ -103,6 +103,10 @@ export default function WizardPage() {
|
||||
null,
|
||||
);
|
||||
const [providerCreated, setProviderCreated] = useState(false);
|
||||
const [createdProviderUuid, setCreatedProviderUuid] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [modelsAdded, setModelsAdded] = useState(false);
|
||||
const [createdBotUuid, setCreatedBotUuid] = useState<string | null>(null);
|
||||
const [webhookUrl, setWebhookUrl] = useState<string>('');
|
||||
const [extraWebhookUrl, setExtraWebhookUrl] = useState<string>('');
|
||||
@@ -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<Set<string>>(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 (
|
||||
<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">
|
||||
<h2 className="text-xl font-semibold">
|
||||
{t('wizard.provider.scanTitle')}
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t('wizard.provider.scanDescription')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="border rounded-lg p-6 bg-card space-y-4">
|
||||
{scanning && (
|
||||
<div className="flex items-center justify-center gap-2 py-8 text-muted-foreground">
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
<span>{t('wizard.provider.scanning')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!scanning && scanDone && scannedModels.length === 0 && (
|
||||
<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">
|
||||
{scannedModels.map((model) => (
|
||||
<label
|
||||
key={model.name}
|
||||
className="flex items-center gap-3 p-3 rounded-lg border cursor-pointer hover:bg-accent transition-colors"
|
||||
>
|
||||
<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>
|
||||
))}
|
||||
</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" />
|
||||
)}
|
||||
{t('wizard.provider.addSelected', {
|
||||
count: selectedModels.size,
|
||||
})}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={onModelsAdded}>
|
||||
{t('wizard.provider.skipModelAdd')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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<string, unknown>;
|
||||
@@ -1325,6 +1519,21 @@ function StepAIEngine({
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Layer 2.5: Model scan & add after provider creation ----
|
||||
if (
|
||||
aiEngineMode === 'llm' &&
|
||||
modelSource === 'custom' &&
|
||||
providerCreated &&
|
||||
!modelsAdded
|
||||
) {
|
||||
return (
|
||||
<WizardModelScan
|
||||
providerUuid={createdProviderUuid}
|
||||
onModelsAdded={onModelsAdded}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Layer 3: Runner configuration form ----
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-1 lg:min-h-0 max-w-6xl mx-auto w-full">
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1777,6 +1777,15 @@ const jaJP = {
|
||||
title: 'モデルプロバイダーを追加',
|
||||
description:
|
||||
'独自のモデルプロバイダーを追加します。APIキーを設定すると使用可能になります。',
|
||||
scanTitle: '利用可能なモデルをスキャン',
|
||||
scanDescription: 'プロバイダーから利用可能なLLMモデルをスキャン中です。',
|
||||
scanning: 'モデルをスキャン中...',
|
||||
noModelsFound:
|
||||
'モデルが見つかりません。プロバイダー設定を確認してください。',
|
||||
addSelected: '選択した{{count}}個のモデルを追加',
|
||||
modelsAdded: '{{count}}個のモデルを追加しました',
|
||||
modelsAddError: 'モデルの追加に失敗しました',
|
||||
skipModelAdd: 'スキップ(後で追加)',
|
||||
},
|
||||
modelSource: {
|
||||
title: 'モデルソースを選択',
|
||||
|
||||
@@ -1686,6 +1686,15 @@ const ruRU = {
|
||||
title: 'Добавить провайдера моделей',
|
||||
description:
|
||||
'Добавьте своего провайдера моделей. Настройте API-ключ для начала работы.',
|
||||
scanTitle: 'Сканирование доступных моделей',
|
||||
scanDescription: 'Сканирование LLM-моделей у вашего провайдера.',
|
||||
scanning: 'Сканирование моделей...',
|
||||
noModelsFound:
|
||||
'Модели не найдены. Проверьте настройки провайдера.',
|
||||
addSelected: 'Добавить {{count}} выбранную(ых)',
|
||||
modelsAdded: 'Добавлено {{count}} модель(ей)',
|
||||
modelsAddError: 'Ошибка добавления моделей',
|
||||
skipModelAdd: 'Пропустить, добавить позже',
|
||||
},
|
||||
modelSource: {
|
||||
title: 'Выберите источник модели',
|
||||
|
||||
@@ -1650,6 +1650,15 @@ const thTH = {
|
||||
title: 'เพิ่มผู้ให้บริการโมเดล',
|
||||
description:
|
||||
'เพิ่มผู้ให้บริการโมเดลของคุณเอง กำหนดค่า API key เพื่อเริ่มต้น',
|
||||
scanTitle: 'สแกนโมเดลที่ใช้ได้',
|
||||
scanDescription: 'กำลังสแกนโมเดล LLM จากผู้ให้บริการของคุณ',
|
||||
scanning: 'กำลังสแกนโมเดล...',
|
||||
noModelsFound:
|
||||
'ไม่พบโมเดล กรุณาตรวจสอบการตั้งค่าผู้ให้บริการ',
|
||||
addSelected: 'เพิ่ม {{count}} โมเดลที่เลือก',
|
||||
modelsAdded: 'เพิ่ม {{count}} โมเดลแล้ว',
|
||||
modelsAddError: 'เพิ่มโมเดลไม่สำเร็จ',
|
||||
skipModelAdd: 'ข้าม เพิ่มทีหลัง',
|
||||
},
|
||||
modelSource: {
|
||||
title: 'เลือกแหล่งโมเดล',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1775,6 +1775,14 @@ const zhHans = {
|
||||
provider: {
|
||||
title: '添加模型供应商',
|
||||
description: '添加你自己的模型供应商,配置 API Key 后即可使用。',
|
||||
scanTitle: '扫描可用模型',
|
||||
scanDescription: '正在从你的供应商中扫描可用的 LLM 模型。',
|
||||
scanning: '正在扫描模型...',
|
||||
noModelsFound: '未发现可用模型,请检查供应商配置。',
|
||||
addSelected: '添加选中的 {{count}} 个模型',
|
||||
modelsAdded: '已添加 {{count}} 个模型',
|
||||
modelsAddError: '添加模型失败',
|
||||
skipModelAdd: '跳过,稍后添加',
|
||||
},
|
||||
modelSource: {
|
||||
title: '选择模型来源',
|
||||
|
||||
@@ -1598,6 +1598,14 @@ const zhHant = {
|
||||
provider: {
|
||||
title: '新增模型供應商',
|
||||
description: '新增你自己的模型供應商,配置 API Key 後即可使用。',
|
||||
scanTitle: '掃描可用模型',
|
||||
scanDescription: '正在從你的供應商中掃描可用的 LLM 模型。',
|
||||
scanning: '正在掃描模型...',
|
||||
noModelsFound: '未發現可用模型,請檢查供應商配置。',
|
||||
addSelected: '新增選取的 {{count}} 個模型',
|
||||
modelsAdded: '已新增 {{count}} 個模型',
|
||||
modelsAddError: '新增模型失敗',
|
||||
skipModelAdd: '跳過,稍後新增',
|
||||
},
|
||||
modelSource: {
|
||||
title: '選擇模型來源',
|
||||
|
||||
Reference in New Issue
Block a user