mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-22 02:07: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;
|
||||
initialValues?: ProviderFormInitialValues;
|
||||
onValuesChange?: (values: ProviderFormInitialValues) => void;
|
||||
submitButtonText?: string;
|
||||
onFormSubmit: (providerUuid?: string) => void;
|
||||
onFormCancel: () => void;
|
||||
}
|
||||
@@ -50,6 +51,7 @@ export default function ProviderForm({
|
||||
providerId,
|
||||
initialValues,
|
||||
onValuesChange,
|
||||
submitButtonText,
|
||||
onFormSubmit,
|
||||
onFormCancel,
|
||||
}: ProviderFormProps) {
|
||||
@@ -399,7 +401,7 @@ export default function ProviderForm({
|
||||
/>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="submit">{t('common.save')}</Button>
|
||||
<Button type="submit">{submitButtonText || t('common.save')}</Button>
|
||||
<Button type="button" variant="outline" onClick={onFormCancel}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
|
||||
+122
-50
@@ -51,6 +51,7 @@ import { getAdapterDocUrl } from '@/app/infra/entities/adapter-docs';
|
||||
import i18n from 'i18next';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -1151,6 +1152,9 @@ function WizardModelScan({
|
||||
);
|
||||
const [adding, setAdding] = useState(false);
|
||||
const [scanDone, setScanDone] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [manualModelName, setManualModelName] = useState('');
|
||||
const [manualAdding, setManualAdding] = useState(false);
|
||||
|
||||
// Auto-scan on mount
|
||||
useEffect(() => {
|
||||
@@ -1178,6 +1182,12 @@ function WizardModelScan({
|
||||
};
|
||||
}, [providerUuid]);
|
||||
|
||||
const filteredModels = searchQuery
|
||||
? scannedModels.filter((m) =>
|
||||
m.name.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
)
|
||||
: scannedModels;
|
||||
|
||||
const toggleModel = (name: string) => {
|
||||
setSelectedModels((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 (
|
||||
<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">
|
||||
@@ -1233,55 +1266,101 @@ function WizardModelScan({
|
||||
</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 && (
|
||||
{!scanning && scanDone && (
|
||||
<>
|
||||
<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>
|
||||
{/* Search input for scanned models */}
|
||||
{scannedModels.length > 0 && (
|
||||
<Input
|
||||
placeholder={t('wizard.provider.searchModels')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="mb-2"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Scanned models list */}
|
||||
{scannedModels.length > 0 ? (
|
||||
<>
|
||||
<div className="max-h-48 overflow-y-auto space-y-2">
|
||||
{filteredModels.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>
|
||||
))}
|
||||
{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>
|
||||
</label>
|
||||
))}
|
||||
{t('wizard.provider.addSelected', {
|
||||
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 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
|
||||
onClick={handleAddSelected}
|
||||
disabled={selectedModels.size === 0 || adding}
|
||||
className="flex-1"
|
||||
onClick={handleManualAdd}
|
||||
disabled={!manualModelName.trim() || manualAdding}
|
||||
>
|
||||
{adding && (
|
||||
{manualAdding && (
|
||||
<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')}
|
||||
{t('wizard.provider.addManual')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
@@ -1540,23 +1619,16 @@ function StepAIEngine({
|
||||
{t('wizard.provider.description')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="border rounded-lg p-6 bg-card space-y-4">
|
||||
<div className="border rounded-lg p-6 bg-card">
|
||||
<ProviderForm
|
||||
initialValues={savedProviderForm}
|
||||
onValuesChange={onSavedProviderFormChange}
|
||||
submitButtonText={t('wizard.provider.saveAndNext')}
|
||||
onFormSubmit={(uuid) => {
|
||||
onProviderCreated(uuid);
|
||||
}}
|
||||
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>
|
||||
);
|
||||
|
||||
@@ -1867,6 +1867,12 @@ const enUS = {
|
||||
modelsAdded: 'Added {{count}} model(s)',
|
||||
modelsAddError: 'Failed to add models',
|
||||
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: {
|
||||
title: 'Choose Model Source',
|
||||
|
||||
@@ -1725,6 +1725,12 @@ const esES = {
|
||||
modelsAdded: '{{count}} modelo(s) agregado(s)',
|
||||
modelsAddError: 'Error al agregar modelos',
|
||||
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: {
|
||||
title: 'Elegir fuente del modelo',
|
||||
|
||||
@@ -1786,6 +1786,12 @@ const jaJP = {
|
||||
modelsAdded: '{{count}}個のモデルを追加しました',
|
||||
modelsAddError: 'モデルの追加に失敗しました',
|
||||
skipModelAdd: 'スキップ(後で追加)',
|
||||
searchModels: 'モデルを検索...',
|
||||
noMatch: '一致するモデルがありません',
|
||||
orDivider: 'または',
|
||||
manualModelPlaceholder: 'モデル名を入力して手動追加',
|
||||
addManual: '追加',
|
||||
saveAndNext: '保存して次へ',
|
||||
},
|
||||
modelSource: {
|
||||
title: 'モデルソースを選択',
|
||||
|
||||
@@ -1695,6 +1695,12 @@ const ruRU = {
|
||||
modelsAdded: 'Добавлено {{count}} модель(ей)',
|
||||
modelsAddError: 'Ошибка добавления моделей',
|
||||
skipModelAdd: 'Пропустить, добавить позже',
|
||||
searchModels: 'Поиск моделей...',
|
||||
noMatch: 'Нет подходящих моделей',
|
||||
orDivider: 'или',
|
||||
manualModelPlaceholder: 'Введите имя модели для ручного добавления',
|
||||
addManual: 'Добавить',
|
||||
saveAndNext: 'Сохранить и далее',
|
||||
},
|
||||
modelSource: {
|
||||
title: 'Выберите источник модели',
|
||||
|
||||
@@ -1659,6 +1659,12 @@ const thTH = {
|
||||
modelsAdded: 'เพิ่ม {{count}} โมเดลแล้ว',
|
||||
modelsAddError: 'เพิ่มโมเดลไม่สำเร็จ',
|
||||
skipModelAdd: 'ข้าม เพิ่มทีหลัง',
|
||||
searchModels: 'ค้นหาโมเดล...',
|
||||
noMatch: 'ไม่พบโมเดลที่ตรงกัน',
|
||||
orDivider: 'หรือ',
|
||||
manualModelPlaceholder: 'กรอกชื่อโมเดลเพื่อเพิ่มด้วยตนเอง',
|
||||
addManual: 'เพิ่ม',
|
||||
saveAndNext: 'บันทึกและถัดไป',
|
||||
},
|
||||
modelSource: {
|
||||
title: 'เลือกแหล่งโมเดล',
|
||||
|
||||
@@ -1685,6 +1685,12 @@ const viVN = {
|
||||
modelsAdded: 'Đã thêm {{count}} mô hình',
|
||||
modelsAddError: 'Thêm mô hình thất bại',
|
||||
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: {
|
||||
title: 'Chọn nguồn mô hình',
|
||||
|
||||
@@ -1783,6 +1783,12 @@ const zhHans = {
|
||||
modelsAdded: '已添加 {{count}} 个模型',
|
||||
modelsAddError: '添加模型失败',
|
||||
skipModelAdd: '跳过,稍后添加',
|
||||
searchModels: '搜索模型...',
|
||||
noMatch: '没有匹配的模型',
|
||||
orDivider: '或',
|
||||
manualModelPlaceholder: '输入模型名称手动添加',
|
||||
addManual: '添加',
|
||||
saveAndNext: '保存并下一步',
|
||||
},
|
||||
modelSource: {
|
||||
title: '选择模型来源',
|
||||
|
||||
@@ -1606,6 +1606,12 @@ const zhHant = {
|
||||
modelsAdded: '已新增 {{count}} 個模型',
|
||||
modelsAddError: '新增模型失敗',
|
||||
skipModelAdd: '跳過,稍後新增',
|
||||
searchModels: '搜尋模型...',
|
||||
noMatch: '沒有符合的模型',
|
||||
orDivider: '或',
|
||||
manualModelPlaceholder: '輸入模型名稱手動新增',
|
||||
addManual: '新增',
|
||||
saveAndNext: '儲存並下一步',
|
||||
},
|
||||
modelSource: {
|
||||
title: '選擇模型來源',
|
||||
|
||||
Reference in New Issue
Block a user