Compare commits

...

11 Commits

Author SHA1 Message Date
langbot-dev e82918faef fix(wizard): prevent duplicate models at API level
Before adding models, fetch existing provider models and skip
names that already exist. Show error toast for manual add duplicates.
2026-08-12 02:38:28 +08:00
langbot-dev 8b0f2f58b1 fix(wizard): prevent duplicate model additions in scan page
- Track added model names in state
- Disable already-added models with 'Added' badge
- Clear selection after successful add
- Add Next button at bottom when models have been added
2026-08-12 02:34:30 +08:00
langbot-dev 72c03276ff 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
2026-08-12 02:29:36 +08:00
langbot-dev d7a034a562 feat(wizard): add next-step button on provider form when returning
When navigating back to the provider form after already creating a
provider, show a Next button that skips to the model scan or config
layer without requiring re-submission.
2026-08-12 02:22:28 +08:00
langbot-dev c4de3c5168 feat(wizard): add next-step navigation to jump back to deepest layer
Track maxReachedLayer so clicking Next from an earlier layer jumps
directly to the deepest previously visited layer, preserving state.
Reset tracking when the user changes their AI engine mode choice.
2026-08-12 02:18:27 +08:00
langbot-dev 259e127696 fix(wizard): preserve form data and model selection on back nav
- Add initialValues and onValuesChange props to ProviderForm
- Save provider form data and selected models in wizard state
- Fix canProceed missing modelsAdded dependency
- Restore form data and selections when navigating back
2026-08-12 02:11:59 +08:00
langbot-dev 1f0371e177 fix(wizard): preserve state when navigating back between layers
Use a dedicated step2Layer state for sub-layer navigation within
Step 2, instead of resetting data flags. This preserves provider
form data, scan results, and model selections when going back.
2026-08-12 02:05:39 +08:00
langbot-dev 62d352ce03 fix(wizard): use correct reasoning_config format for model creation
Use { level: 'provider_default' } instead of { enabled: false }.
Also use model's scanned abilities instead of hardcoded ['llm'].
2026-08-12 01:58:43 +08:00
langbot-dev d06c4287bf 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.
2026-08-12 01:51:28 +08:00
langbot-dev af80425627 fix(wizard): auto-select local-agent after provider creation
After creating a custom provider, automatically select local-agent as
the runner so the config form (with model selector) is displayed.
Require runner selection for canProceed in LLM+custom path.
2026-08-12 01:42:43 +08:00
langbot-dev 1f3aad6baa feat(wizard): restructure AI engine selection flow
Restructure the wizard's Step 2 (AI Engine) into a two-layer choice:
- Orchestrated Agent apps (Dify, n8n, Coze, etc.)
- Direct LLM usage (Space service or custom provider)

For direct LLM mode, users choose between LangBot Space (OAuth)
or adding their own model provider via a dedicated ProviderForm page.

Support intra-step back navigation across all layers.
Add Vite dev server proxy config for API requests.
Add i18n translations for all 8 locales.
2026-08-12 01:29:07 +08:00
11 changed files with 1150 additions and 97 deletions
@@ -31,14 +31,27 @@ const getFormSchema = (t: (key: string) => string) =>
api_key: z.string().optional(), api_key: z.string().optional(),
}); });
export interface ProviderFormInitialValues {
name?: string;
requester?: string;
base_url?: string;
api_key?: string;
}
interface ProviderFormProps { interface ProviderFormProps {
providerId?: string; providerId?: string;
onFormSubmit: () => void; initialValues?: ProviderFormInitialValues;
onValuesChange?: (values: ProviderFormInitialValues) => void;
submitButtonText?: string;
onFormSubmit: (providerUuid?: string) => void;
onFormCancel: () => void; onFormCancel: () => void;
} }
export default function ProviderForm({ export default function ProviderForm({
providerId, providerId,
initialValues,
onValuesChange,
submitButtonText,
onFormSubmit, onFormSubmit,
onFormCancel, onFormCancel,
}: ProviderFormProps) { }: ProviderFormProps) {
@@ -48,13 +61,22 @@ export default function ProviderForm({
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: { defaultValues: {
name: '', name: initialValues?.name ?? '',
requester: '', requester: initialValues?.requester ?? '',
base_url: '', base_url: initialValues?.base_url ?? '',
api_key: '', api_key: initialValues?.api_key ?? '',
}, },
}); });
const { setValue } = form; const { setValue, watch } = form;
// Report form values changes to parent
useEffect(() => {
if (!onValuesChange) return;
const subscription = watch((values) => {
onValuesChange(values as ProviderFormInitialValues);
});
return () => subscription.unsubscribe();
}, [watch, onValuesChange]);
const [requesterList, setRequesterList] = useState< const [requesterList, setRequesterList] = useState<
{ {
@@ -174,11 +196,12 @@ export default function ProviderForm({
if (providerId) { if (providerId) {
await httpClient.updateModelProvider(providerId, data); await httpClient.updateModelProvider(providerId, data);
toast.success(t('models.providerSaved')); toast.success(t('models.providerSaved'));
onFormSubmit();
} else { } else {
await httpClient.createModelProvider(data); const resp = await httpClient.createModelProvider(data);
toast.success(t('models.providerCreated')); toast.success(t('models.providerCreated'));
onFormSubmit(resp.uuid);
} }
onFormSubmit();
} catch (err) { } catch (err) {
toast.error(t('models.providerSaveError') + (err as CustomApiError).msg); toast.error(t('models.providerSaveError') + (err as CustomApiError).msg);
} }
@@ -378,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>
+700 -88
View File
@@ -12,6 +12,8 @@ import {
Loader2, Loader2,
X, X,
ExternalLink, ExternalLink,
Rocket,
Wrench,
} from 'lucide-react'; } from 'lucide-react';
import { httpClient } from '@/app/infra/http/HttpClient'; import { httpClient } from '@/app/infra/http/HttpClient';
@@ -38,6 +40,7 @@ import {
parseDynamicFormItemType, parseDynamicFormItemType,
} from '@/app/home/components/dynamic-form/DynamicFormItemConfig'; } from '@/app/home/components/dynamic-form/DynamicFormItemConfig';
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent'; import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
import ProviderForm from '@/app/home/components/models-dialog/component/provider-form/ProviderForm';
import { BotLogListComponent } from '@/app/home/bots/components/bot-log/view/BotLogListComponent'; import { BotLogListComponent } from '@/app/home/bots/components/bot-log/view/BotLogListComponent';
import { extractI18nObject } from '@/i18n/I18nProvider'; import { extractI18nObject } from '@/i18n/I18nProvider';
import { import {
@@ -48,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,
@@ -67,6 +71,7 @@ import {
DialogTitle, DialogTitle,
} from '@/components/ui/dialog'; } from '@/components/ui/dialog';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Types // Types
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -92,6 +97,40 @@ export default function WizardPage() {
{}, {},
); );
const [runnerConfig, setRunnerConfig] = useState<Record<string, unknown>>({}); const [runnerConfig, setRunnerConfig] = useState<Record<string, unknown>>({});
const [aiEngineMode, setAiEngineMode] = useState<
'orchestrated' | 'llm' | null
>(null);
const [modelSource, setModelSource] = useState<'space' | 'custom' | null>(
null,
);
const [providerCreated, setProviderCreated] = useState(false);
const [createdProviderUuid, setCreatedProviderUuid] = useState<string | null>(
null,
);
const [modelsAdded, setModelsAdded] = useState(false);
// Sub-layer within Step 2: 0=main, 1=sub-choice, 2=provider, 3=scan, 4=config
const [step2Layer, setStep2Layer] = useState(0);
const [maxReachedLayer, setMaxReachedLayer] = useState(0);
const goToStep2Layer = useCallback(
(layer: number) => {
setStep2Layer(layer);
setMaxReachedLayer((prev) => Math.max(prev, layer));
},
[],
);
// Saved provider form data for back navigation
const [savedProviderForm, setSavedProviderForm] = useState<{
name?: string;
requester?: string;
base_url?: string;
api_key?: string;
}>({});
// Saved selected scanned models for back navigation
const [savedSelectedModels, setSavedSelectedModels] = useState<Set<string>>(
new Set(),
);
const [createdBotUuid, setCreatedBotUuid] = useState<string | null>(null); const [createdBotUuid, setCreatedBotUuid] = useState<string | null>(null);
const [webhookUrl, setWebhookUrl] = useState<string>(''); const [webhookUrl, setWebhookUrl] = useState<string>('');
const [extraWebhookUrl, setExtraWebhookUrl] = useState<string>(''); const [extraWebhookUrl, setExtraWebhookUrl] = useState<string>('');
@@ -273,11 +312,28 @@ export default function WizardPage() {
const handleSelectRunner = useCallback( const handleSelectRunner = useCallback(
(runner: string) => { (runner: string) => {
setSelectedRunner(runner); setSelectedRunner(runner);
goToStep2Layer(4);
saveProgress({ step: 2, selected_runner: runner }); saveProgress({ step: 2, selected_runner: runner });
}, },
[saveProgress], [saveProgress],
); );
const handleAiEngineModeSelect = useCallback(
(mode: 'orchestrated' | 'llm') => {
setAiEngineMode(mode);
setSelectedRunner(null);
setModelSource(null);
setProviderCreated(false);
setCreatedProviderUuid(null);
setModelsAdded(false);
setSavedProviderForm({});
setSavedSelectedModels(new Set());
setMaxReachedLayer(0);
goToStep2Layer(1);
},
[goToStep2Layer],
);
// ---- Navigation helpers ---- // ---- Navigation helpers ----
const canProceed = useCallback((): boolean => { const canProceed = useCallback((): boolean => {
@@ -287,27 +343,53 @@ export default function WizardPage() {
case 1: case 1:
return createdBotUuid !== null && botSaved; return createdBotUuid !== null && botSaved;
case 2: case 2:
return selectedRunner !== null; if (aiEngineMode === 'orchestrated') {
return selectedRunner !== null;
}
if (aiEngineMode === 'llm') {
return (
modelSource === 'space' ||
(modelSource === 'custom' && modelsAdded)
);
}
return false;
default: default:
return false; return false;
} }
}, [currentStep, selectedAdapter, createdBotUuid, botSaved, selectedRunner]); }, [
currentStep,
selectedAdapter,
createdBotUuid,
botSaved,
aiEngineMode,
selectedRunner,
modelSource,
modelsAdded,
]);
const goNext = useCallback(() => { const goNext = useCallback(() => {
if (currentStep === 2 && step2Layer < maxReachedLayer) {
setStep2Layer(maxReachedLayer);
return;
}
if (currentStep < TOTAL_STEPS - 1 && canProceed()) { if (currentStep < TOTAL_STEPS - 1 && canProceed()) {
const nextStep = currentStep + 1; const nextStep = currentStep + 1;
setCurrentStep(nextStep); setCurrentStep(nextStep);
saveProgress({ step: nextStep }); saveProgress({ step: nextStep });
} }
}, [currentStep, canProceed, saveProgress]); }, [currentStep, step2Layer, maxReachedLayer, canProceed, saveProgress]);
const goPrev = useCallback(() => { const goPrev = useCallback(() => {
if (currentStep === 2 && step2Layer > 0) {
setStep2Layer(step2Layer - 1);
return;
}
if (currentStep > 0) { if (currentStep > 0) {
const prevStep = currentStep - 1; const prevStep = currentStep - 1;
setCurrentStep(prevStep); setCurrentStep(prevStep);
saveProgress({ step: prevStep }); saveProgress({ step: prevStep });
} }
}, [currentStep, saveProgress]); }, [currentStep, step2Layer, saveProgress]);
// ---- Create Bot (Step 0) ---- // ---- Create Bot (Step 0) ----
// Creates a disabled bot using the adapter label as name. // Creates a disabled bot using the adapter label as name.
@@ -503,6 +585,22 @@ export default function WizardPage() {
} }
}, [t]); }, [t]);
const handleModelSourceSelect = useCallback(
(source: 'space' | 'custom') => {
setModelSource(source);
if (source === 'space') {
handleSpaceAuth();
} else if (providerCreated && modelsAdded) {
goToStep2Layer(4);
} else if (providerCreated) {
goToStep2Layer(3);
} else {
goToStep2Layer(2);
}
},
[handleSpaceAuth, providerCreated, modelsAdded],
);
// ---- Check if local account ---- // ---- Check if local account ----
// Re-evaluated after remote data fetch (when userInfo is populated) // Re-evaluated after remote data fetch (when userInfo is populated)
const isLocalAccount = const isLocalAccount =
@@ -660,10 +758,41 @@ export default function WizardPage() {
{currentStep === 2 && ( {currentStep === 2 && (
<StepAIEngine <StepAIEngine
runnerOptions={runnerOptions} runnerOptions={runnerOptions}
selected={selectedRunner} step2Layer={step2Layer}
onSelect={handleSelectRunner} aiEngineMode={aiEngineMode}
onAiEngineModeSelect={handleAiEngineModeSelect}
selectedRunner={selectedRunner}
onSelectRunner={handleSelectRunner}
isLocalAccount={isLocalAccount} isLocalAccount={isLocalAccount}
onSpaceAuth={handleSpaceAuth} onSpaceAuth={handleSpaceAuth}
modelSource={modelSource}
onModelSourceSelect={handleModelSourceSelect}
providerCreated={providerCreated}
createdProviderUuid={createdProviderUuid}
modelsAdded={modelsAdded}
onProviderCreated={(uuid) => {
setProviderCreated(true);
setCreatedProviderUuid(uuid ?? null);
goToStep2Layer(3);
}}
onModelsAdded={() => {
setModelsAdded(true);
setSelectedRunner('local-agent');
goToStep2Layer(4);
saveProgress({ step: 2, selected_runner: 'local-agent' });
}}
onResetModelSource={() => setModelSource(null)}
savedProviderForm={savedProviderForm}
onSavedProviderFormChange={setSavedProviderForm}
savedSelectedModels={savedSelectedModels}
onSavedSelectedModelsChange={setSavedSelectedModels}
onGoToNextLayer={() => {
if (modelsAdded) {
goToStep2Layer(4);
} else {
goToStep2Layer(3);
}
}}
runnerConfigItems={selectedRunnerConfigItems} runnerConfigItems={selectedRunnerConfigItems}
runnerConfigValues={runnerConfig} runnerConfigValues={runnerConfig}
onRunnerConfigChange={setRunnerConfig} onRunnerConfigChange={setRunnerConfig}
@@ -998,25 +1127,354 @@ function StepBotConfig({
); );
} }
// ---------------------------------------------------------------------------
// Model scan sub-component (used within Step 2)
// ---------------------------------------------------------------------------
function WizardModelScan({
providerUuid,
initialSelectedModels,
onSelectedModelsChange,
onModelsAdded,
}: {
providerUuid: string | null;
initialSelectedModels: Set<string>;
onSelectedModelsChange: (v: Set<string>) => void;
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>>(
initialSelectedModels,
);
const [adding, setAdding] = useState(false);
const [scanDone, setScanDone] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [manualModelName, setManualModelName] = useState('');
const [manualAdding, setManualAdding] = useState(false);
const [addedModelNames, setAddedModelNames] = useState<Set<string>>(
new Set(initialSelectedModels),
);
// 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 filteredModels = searchQuery
? scannedModels.filter((m) =>
m.name.toLowerCase().includes(searchQuery.toLowerCase()),
)
: scannedModels;
const toggleModel = (name: string) => {
setSelectedModels((prev) => {
const next = new Set(prev);
if (next.has(name)) next.delete(name);
else next.add(name);
onSelectedModelsChange(next);
return next;
});
};
const getExistingModelNames = async (): Promise<Set<string>> => {
if (!providerUuid) return new Set();
try {
const resp = await httpClient.getProviderLLMModels(providerUuid);
return new Set((resp.models ?? []).map((m: { name: string }) => m.name));
} catch {
return new Set();
}
};
const handleAddSelected = async () => {
if (!providerUuid || selectedModels.size === 0) return;
setAdding(true);
try {
const existing = await getExistingModelNames();
let added = 0;
for (const name of selectedModels) {
if (existing.has(name)) continue;
const model = scannedModels.find((m) => m.name === name);
await httpClient.createProviderLLMModel({
name,
provider_uuid: providerUuid,
abilities: (model as { abilities?: string[] })?.abilities || ['llm'],
reasoning_config: { level: 'provider_default' },
context_length: model?.context_length ?? null,
extra_args: {},
} as never);
existing.add(name);
added++;
}
if (added > 0) {
toast.success(t('wizard.provider.modelsAdded', { count: added }));
}
setAddedModelNames((prev) => {
const next = new Set(prev);
for (const name of selectedModels) next.add(name);
return next;
});
setSelectedModels(new Set());
onSelectedModelsChange(new Set());
} catch {
toast.error(t('wizard.provider.modelsAddError'));
} finally {
setAdding(false);
}
};
const handleManualAdd = async () => {
const name = manualModelName.trim();
if (!providerUuid || !name) return;
setManualAdding(true);
try {
const existing = await getExistingModelNames();
if (existing.has(name)) {
toast.error(t('wizard.provider.modelAlreadyExists', { name }));
setManualAdding(false);
return;
}
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 }));
setAddedModelNames((prev) => new Set(prev).add(name));
setManualModelName('');
} 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">
<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 && (
<>
{/* 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) => {
const isAdded = addedModelNames.has(model.name);
return (
<label
key={model.name}
className={cn(
'flex items-center gap-3 p-3 rounded-lg border transition-colors',
isAdded
? 'opacity-50 cursor-not-allowed bg-muted/50'
: 'cursor-pointer hover:bg-accent',
)}
>
<input
type="checkbox"
checked={selectedModels.has(model.name) || isAdded}
disabled={isAdded}
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>
{isAdded && (
<span className="text-xs text-green-600 font-medium shrink-0">
{t('wizard.provider.alreadyAdded')}
</span>
)}
</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" />
)}
{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>
{/* 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={handleManualAdd}
disabled={!manualModelName.trim() || manualAdding}
>
{manualAdding && (
<Loader2 className="w-4 h-4 mr-1.5 animate-spin" />
)}
{t('wizard.provider.addManual')}
</Button>
</div>
</>
)}
{/* Next button when models have been added */}
{addedModelNames.size > 0 && (
<div className="flex justify-end pt-2 border-t">
<Button onClick={onModelsAdded}>
{t('wizard.next')}
<ArrowRight className="w-4 h-4 ml-1.5" />
</Button>
</div>
)}
</div>
</div>
);
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Step 2: Select & Configure AI Engine // Step 2: Select & Configure AI Engine
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function StepAIEngine({ function StepAIEngine({
runnerOptions, runnerOptions,
selected, step2Layer,
onSelect, aiEngineMode,
onAiEngineModeSelect,
selectedRunner,
onSelectRunner,
isLocalAccount, isLocalAccount,
onSpaceAuth, onSpaceAuth,
modelSource,
onModelSourceSelect,
providerCreated,
createdProviderUuid,
modelsAdded,
onProviderCreated,
onModelsAdded,
onResetModelSource,
savedProviderForm,
onSavedProviderFormChange,
savedSelectedModels,
onSavedSelectedModelsChange,
onGoToNextLayer,
runnerConfigItems, runnerConfigItems,
runnerConfigValues, runnerConfigValues,
onRunnerConfigChange, onRunnerConfigChange,
}: { }: {
runnerOptions: { name: string; label: { en_US: string; zh_Hans: string } }[]; runnerOptions: { name: string; label: { en_US: string; zh_Hans: string } }[];
selected: string | null; step2Layer: number;
onSelect: (name: string) => void; aiEngineMode: 'orchestrated' | 'llm' | null;
onAiEngineModeSelect: (mode: 'orchestrated' | 'llm') => void;
selectedRunner: string | null;
onSelectRunner: (name: string) => void;
isLocalAccount: boolean; isLocalAccount: boolean;
onSpaceAuth: () => void; onSpaceAuth: () => void;
modelSource: 'space' | 'custom' | null;
onModelSourceSelect: (source: 'space' | 'custom') => void;
providerCreated: boolean;
createdProviderUuid: string | null;
modelsAdded: boolean;
onProviderCreated: (uuid?: string) => void;
onModelsAdded: () => void;
onResetModelSource: () => void;
savedProviderForm: { name?: string; requester?: string; base_url?: string; api_key?: string };
onSavedProviderFormChange: (v: { name?: string; requester?: string; base_url?: string; api_key?: string }) => void;
savedSelectedModels: Set<string>;
onSavedSelectedModelsChange: (v: Set<string>) => void;
onGoToNextLayer: () => void;
runnerConfigItems: IDynamicFormItemSchema[]; runnerConfigItems: IDynamicFormItemSchema[];
runnerConfigValues: Record<string, unknown>; runnerConfigValues: Record<string, unknown>;
onRunnerConfigChange: (v: Record<string, unknown>) => void; onRunnerConfigChange: (v: Record<string, unknown>) => void;
@@ -1032,14 +1490,19 @@ function StepAIEngine({
); );
const runnerLabel = useMemo(() => { const runnerLabel = useMemo(() => {
const r = runnerOptions.find((o) => o.name === selected); const r = runnerOptions.find((o) => o.name === selectedRunner);
return r ? extractI18nObject(r.label) : (selected ?? ''); return r ? extractI18nObject(r.label) : (selectedRunner ?? '');
}, [runnerOptions, selected]); }, [runnerOptions, selectedRunner]);
// Before any runner is selected: centered grid layout // Orchestrated runners: everything except local-agent
if (!selected) { const orchestratedRunners = runnerOptions.filter(
(o) => o.name !== 'local-agent',
);
// ---- Layer 0: Main choice ----
if (step2Layer === 0) {
return ( return (
<div className="space-y-6 max-w-4xl mx-auto"> <div className="space-y-6 max-w-3xl mx-auto">
<div className="text-center"> <div className="text-center">
<h2 className="text-xl font-semibold"> <h2 className="text-xl font-semibold">
{t('wizard.aiEngine.title')} {t('wizard.aiEngine.title')}
@@ -1048,12 +1511,75 @@ function StepAIEngine({
{t('wizard.aiEngine.description')} {t('wizard.aiEngine.description')}
</p> </p>
</div> </div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
{/* Orchestrated Agent Apps */}
<div
className="relative rounded-xl border-2 border-border cursor-pointer transition-all hover:shadow-lg hover:border-primary/50 hover:scale-[1.02]"
onClick={() => onAiEngineModeSelect('orchestrated')}
>
<div className="p-6 flex flex-col items-center gap-4 text-center h-full">
<div className="w-14 h-14 rounded-full bg-muted flex items-center justify-center">
<Wrench className="w-7 h-7 text-muted-foreground" />
</div>
<div>
<h3 className="text-lg font-semibold">
{t('wizard.aiEngine.orchestrated.title')}
</h3>
<p className="text-sm text-muted-foreground mt-1">
{t('wizard.aiEngine.orchestrated.description')}
</p>
</div>
<Button variant="outline" size="lg" className="w-full">
{t('wizard.aiEngine.orchestrated.action')}
</Button>
</div>
</div>
{/* Direct LLM */}
<div
className="relative rounded-xl border-2 border-border cursor-pointer transition-all hover:shadow-lg hover:border-primary/50 hover:scale-[1.02]"
onClick={() => onAiEngineModeSelect('llm')}
>
<div className="p-6 flex flex-col items-center gap-4 text-center h-full">
<div className="w-14 h-14 rounded-full bg-muted flex items-center justify-center">
<Rocket className="w-7 h-7 text-muted-foreground" />
</div>
<div>
<h3 className="text-lg font-semibold">
{t('wizard.aiEngine.llm.title')}
</h3>
<p className="text-sm text-muted-foreground mt-1">
{t('wizard.aiEngine.llm.description')}
</p>
</div>
<Button variant="outline" size="lg" className="w-full">
{t('wizard.aiEngine.llm.action')}
</Button>
</div>
</div>
</div>
</div>
);
}
// ---- Layer 1a: Orchestrated runners ----
if (step2Layer === 1 && aiEngineMode === 'orchestrated') {
return (
<div className="space-y-6 max-w-4xl mx-auto">
<div className="text-center">
<h2 className="text-xl font-semibold">
{t('wizard.aiEngine.orchestrated.selectTitle')}
</h2>
<p className="text-sm text-muted-foreground mt-1">
{t('wizard.aiEngine.orchestrated.selectDescription')}
</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{runnerOptions.map((opt) => ( {orchestratedRunners.map((opt) => (
<Card <Card
key={opt.name} key={opt.name}
className="cursor-pointer transition-all hover:shadow-md hover:border-primary/50" className="cursor-pointer transition-all hover:shadow-md hover:border-primary/50"
onClick={() => onSelect(opt.name)} onClick={() => onSelectRunner(opt.name)}
> >
<CardHeader className="flex flex-row items-center gap-3"> <CardHeader className="flex flex-row items-center gap-3">
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
@@ -1072,9 +1598,109 @@ function StepAIEngine({
); );
} }
// After a runner is selected: left-right split layout // ---- Layer 1b: LLM model source selection ----
// On mobile (< lg): single column, normal scroll from parent if (step2Layer === 1 && aiEngineMode === 'llm') {
// On desktop (>= lg): side-by-side with independent scroll per column return (
<div className="space-y-6 max-w-3xl mx-auto">
<div className="text-center">
<h2 className="text-xl font-semibold">
{t('wizard.modelSource.title')}
</h2>
<p className="text-sm text-muted-foreground mt-1">
{t('wizard.modelSource.description')}
</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
{/* LangBot Service */}
<div
className="relative rounded-xl border-2 border-border cursor-pointer transition-all hover:shadow-lg hover:border-primary/50 hover:scale-[1.02]"
onClick={() => onModelSourceSelect('space')}
>
<div className="p-6 flex flex-col items-center gap-4 text-center h-full">
<div className="w-14 h-14 rounded-full bg-muted flex items-center justify-center">
<Rocket className="w-7 h-7 text-muted-foreground" />
</div>
<div>
<h3 className="text-lg font-semibold">
{t('wizard.modelSource.space.title')}
</h3>
<p className="text-sm text-muted-foreground mt-1">
{t('wizard.modelSource.space.description')}
</p>
</div>
<Button variant="outline" size="lg" className="w-full">
{t('wizard.modelSource.space.action')}
</Button>
</div>
</div>
{/* Custom provider */}
<div
className="relative rounded-xl border-2 border-border cursor-pointer transition-all hover:shadow-lg hover:border-primary/50 hover:scale-[1.02]"
onClick={() => onModelSourceSelect('custom')}
>
<div className="p-6 flex flex-col items-center gap-4 text-center h-full">
<div className="w-14 h-14 rounded-full bg-muted flex items-center justify-center">
<Wrench className="w-7 h-7 text-muted-foreground" />
</div>
<div>
<h3 className="text-lg font-semibold">
{t('wizard.modelSource.custom.title')}
</h3>
<p className="text-sm text-muted-foreground mt-1">
{t('wizard.modelSource.custom.description')}
</p>
</div>
<Button variant="outline" size="lg" className="w-full">
{t('wizard.modelSource.custom.action')}
</Button>
</div>
</div>
</div>
</div>
);
}
// ---- Layer 2: Custom provider creation form ----
if (step2Layer === 2) {
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.title')}
</h2>
<p className="text-sm text-muted-foreground mt-1">
{t('wizard.provider.description')}
</p>
</div>
<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}
/>
</div>
</div>
);
}
// ---- Layer 2.5: Model scan & add after provider creation ----
if (step2Layer === 3) {
return (
<WizardModelScan
providerUuid={createdProviderUuid}
initialSelectedModels={savedSelectedModels}
onSelectedModelsChange={onSavedSelectedModelsChange}
onModelsAdded={onModelsAdded}
/>
);
}
// ---- Layer 3: Runner configuration form ----
return ( return (
<div className="flex flex-col lg:flex-1 lg:min-h-0 max-w-6xl mx-auto w-full"> <div className="flex flex-col lg:flex-1 lg:min-h-0 max-w-6xl mx-auto w-full">
<div className="text-center shrink-0 mb-4"> <div className="text-center shrink-0 mb-4">
@@ -1085,75 +1711,61 @@ function StepAIEngine({
</div> </div>
<div className="flex flex-col lg:flex-row lg:justify-center gap-6 lg:flex-1 lg:min-h-0 animate-in fade-in slide-in-from-bottom-2 duration-300"> <div className="flex flex-col lg:flex-row lg:justify-center gap-6 lg:flex-1 lg:min-h-0 animate-in fade-in slide-in-from-bottom-2 duration-300">
{/* Left: runner list */} {/* Left: runner list (only for orchestrated mode) */}
<div className="w-full lg:w-[280px] shrink-0 lg:overflow-y-auto lg:pr-3"> {aiEngineMode === 'orchestrated' && (
{/* p-1 provides space for ring-2 (4px) to render without clipping */} <div className="w-full lg:w-[280px] shrink-0 lg:overflow-y-auto lg:pr-3">
<div className="space-y-3 p-1"> <div className="space-y-3 p-1">
{runnerOptions.map((opt) => { {orchestratedRunners.map((opt) => {
const isSelected = selected === opt.name; const isSelected = selectedRunner === opt.name;
return ( return (
<Card <Card
key={opt.name} key={opt.name}
className={cn( className={cn(
'cursor-pointer transition-all', 'cursor-pointer transition-all',
isSelected isSelected
? 'ring-2 ring-primary shadow-md' ? 'ring-2 ring-primary shadow-md'
: 'opacity-50 hover:opacity-80 hover:border-primary/50', : 'opacity-50 hover:opacity-80 hover:border-primary/50',
)}
onClick={() => onSelect(opt.name)}
>
<CardHeader className="flex flex-row items-center gap-3 py-3 px-4">
<div className="min-w-0 flex-1">
<CardTitle
className={cn(
'text-sm',
!isSelected && 'text-muted-foreground',
)}
>
{extractI18nObject(opt.label)}
</CardTitle>
<CardDescription className="text-xs font-mono text-muted-foreground">
{opt.name}
</CardDescription>
</div>
{isSelected && (
<div className="shrink-0">
<div className="w-5 h-5 rounded-full bg-primary flex items-center justify-center">
<Check className="w-3 h-3 text-primary-foreground" />
</div>
</div>
)} )}
</CardHeader> onClick={() => onSelectRunner(opt.name)}
</Card> >
); <CardHeader className="flex flex-row items-center gap-3 py-3 px-4">
})} <div className="min-w-0 flex-1">
<CardTitle
{/* Space promotion banner */} className={cn(
{selected === 'local-agent' && isLocalAccount && ( 'text-sm',
<div className="animate-in fade-in slide-in-from-left-2 duration-300"> !isSelected && 'text-muted-foreground',
<div className="relative rounded-lg p-[2px] bg-gradient-to-r from-purple-500 via-pink-500 to-orange-500"> )}
<div className="rounded-[calc(0.5rem-2px)] bg-background p-3 flex flex-col items-center gap-2 text-center"> >
<Sparkles className="w-6 h-6 text-purple-500 shrink-0" /> {extractI18nObject(opt.label)}
<p className="text-xs font-medium"> </CardTitle>
{t('wizard.spaceBanner.message')} <CardDescription className="text-xs font-mono text-muted-foreground">
</p> {opt.name}
<Button </CardDescription>
variant="outline" </div>
size="sm" {isSelected && (
onClick={onSpaceAuth} <div className="shrink-0">
className="w-full" <div className="w-5 h-5 rounded-full bg-primary flex items-center justify-center">
> <Check className="w-3 h-3 text-primary-foreground" />
{t('wizard.spaceBanner.action')} </div>
</Button> </div>
</div> )}
</div> </CardHeader>
</div> </Card>
)} );
})}
</div>
</div> </div>
</div> )}
{/* Right: runner configuration — fixed width on desktop */} {/* Right: runner configuration */}
<div className="w-full lg:w-[560px] shrink-0 lg:overflow-y-auto lg:pr-3 animate-in fade-in slide-in-from-right-2 duration-300"> <div
className={cn(
'shrink-0 lg:overflow-y-auto lg:pr-3 animate-in fade-in slide-in-from-right-2 duration-300',
aiEngineMode === 'orchestrated'
? 'w-full lg:w-[560px]'
: 'w-full max-w-2xl mx-auto',
)}
>
<div className="p-1"> <div className="p-1">
{runnerConfigItems.length > 0 && ( {runnerConfigItems.length > 0 && (
<Card> <Card>
@@ -1164,7 +1776,7 @@ function StepAIEngine({
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<DynamicFormComponent <DynamicFormComponent
key={selected} key={selectedRunner ?? 'llm'}
itemConfigList={runnerConfigItems} itemConfigList={runnerConfigItems}
initialValues={runnerConfigValues as Record<string, object>} initialValues={runnerConfigValues as Record<string, object>}
onSubmit={stableRunnerConfigCb} onSubmit={stableRunnerConfigCb}
+51
View File
@@ -1835,12 +1835,63 @@ const enUS = {
title: 'Select an AI Engine', title: 'Select an AI Engine',
description: description:
"Choose the AI engine that will power your bot's intelligence.", "Choose the AI engine that will power your bot's intelligence.",
orchestrated: {
title: 'Use Orchestrated Agent Apps',
description:
'Use pre-built Agent apps from Dify, n8n, Coze, and more.',
action: 'Select Agent App',
selectTitle: 'Select Agent App',
selectDescription: 'Choose the Agent app platform you want to use.',
},
llm: {
title: 'Use LLM Directly',
description:
'Configure a model provider and use LLM to drive your bot directly.',
action: 'Configure LLM',
},
}, },
spaceBanner: { spaceBanner: {
message: message:
'Connect to LangBot Space for free trial model credits and zero-config instant setup!', 'Connect to LangBot Space for free trial model credits and zero-config instant setup!',
action: 'Authorize with Space', action: 'Authorize with Space',
}, },
provider: {
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',
searchModels: 'Search models...',
noMatch: 'No matching models',
orDivider: 'or',
manualModelPlaceholder: 'Enter model name to add manually',
addManual: 'Add',
saveAndNext: 'Save & Next',
alreadyAdded: 'Added',
modelAlreadyExists: 'Model "{{name}}" already exists',
},
modelSource: {
title: 'Choose Model Source',
description: 'Select how to provide model capabilities for your AI engine.',
space: {
title: 'LangBot Service',
description:
'Zero-config ready with free trial model credits. No API key needed — just plug and play.',
action: 'Use LangBot Service',
},
custom: {
title: 'Custom Model',
description:
'Bring your own model API key. Supports OpenAI, Claude, Gemini, and more.',
action: 'Add My Own Model',
},
},
config: { config: {
botInfo: 'Bot Information', botInfo: 'Bot Information',
botNamePlaceholder: 'Enter bot name', botNamePlaceholder: 'Enter bot name',
+54
View File
@@ -1691,12 +1691,66 @@ const esES = {
title: 'Selecciona un motor de IA', title: 'Selecciona un motor de IA',
description: description:
'Elige el motor de IA que impulsará la inteligencia de tu Bot.', 'Elige el motor de IA que impulsará la inteligencia de tu Bot.',
orchestrated: {
title: 'Usar aplicaciones Agent orquestadas',
description:
'Usar aplicaciones Agent predefinidas de Dify, n8n, Coze y más.',
action: 'Seleccionar aplicación Agent',
selectTitle: 'Seleccionar aplicación Agent',
selectDescription:
'Elige la plataforma de aplicación Agent que deseas usar.',
},
llm: {
title: 'Usar modelo directamente',
description:
'Configurar un proveedor de modelos y usar el modelo para impulsar tu Bot.',
action: 'Configurar modelo',
},
}, },
spaceBanner: { spaceBanner: {
message: message:
'¡Conéctate a LangBot Space para obtener créditos de prueba gratuitos y configuración instantánea sin esfuerzo!', '¡Conéctate a LangBot Space para obtener créditos de prueba gratuitos y configuración instantánea sin esfuerzo!',
action: 'Autorizar con Space', action: 'Autorizar con Space',
}, },
provider: {
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',
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',
alreadyAdded: 'Agregado',
modelAlreadyExists: 'El modelo "{{name}}" ya existe',
},
modelSource: {
title: 'Elegir fuente del modelo',
description:
'Selecciona cómo proporcionar capacidades de modelo para tu motor de IA.',
space: {
title: 'Servicio LangBot',
description:
'Listo para usar sin configuración, con créditos de prueba gratuitos. No se necesita API key.',
action: 'Usar servicio LangBot',
},
custom: {
title: 'Modelo personalizado',
description:
'Usa tu propia API key. Compatible con OpenAI, Claude, Gemini y más.',
action: 'Agregar mi propio modelo',
},
},
config: { config: {
botInfo: 'Información del Bot', botInfo: 'Información del Bot',
botNamePlaceholder: 'Introduce el nombre del Bot', botNamePlaceholder: 'Introduce el nombre del Bot',
+53
View File
@@ -1752,12 +1752,65 @@ const jaJP = {
title: 'AIエンジンを選択', title: 'AIエンジンを選択',
description: description:
'ボットのインテリジェンスを駆動するAIエンジンを選択してください。', 'ボットのインテリジェンスを駆動するAIエンジンを選択してください。',
orchestrated: {
title: 'オーケストレーション済みAgentアプリを使用',
description:
'Dify、n8n、Cozeなどのプラットフォームで構築済みのAgentアプリを使用。',
action: 'Agentアプリを選択',
selectTitle: 'Agentアプリを選択',
selectDescription:
'使用するAgentアプリプラットフォームを選択してください。',
},
llm: {
title: '大規模モデルを直接使用',
description:
'モデルプロバイダーを設定し、大規模モデルでボットを直接駆動。',
action: '大規模モデルを設定',
},
}, },
spaceBanner: { spaceBanner: {
message: message:
'LangBot Spaceに接続して、無料トライアルモデルクレジットとゼロ設定の即時セットアップを入手!', 'LangBot Spaceに接続して、無料トライアルモデルクレジットとゼロ設定の即時セットアップを入手!',
action: 'Spaceで認証', action: 'Spaceで認証',
}, },
provider: {
title: 'モデルプロバイダーを追加',
description:
'独自のモデルプロバイダーを追加します。APIキーを設定すると使用可能になります。',
scanTitle: '利用可能なモデルをスキャン',
scanDescription: 'プロバイダーから利用可能なLLMモデルをスキャン中です。',
scanning: 'モデルをスキャン中...',
noModelsFound:
'モデルが見つかりません。プロバイダー設定を確認してください。',
addSelected: '選択した{{count}}個のモデルを追加',
modelsAdded: '{{count}}個のモデルを追加しました',
modelsAddError: 'モデルの追加に失敗しました',
skipModelAdd: 'スキップ(後で追加)',
searchModels: 'モデルを検索...',
noMatch: '一致するモデルがありません',
orDivider: 'または',
manualModelPlaceholder: 'モデル名を入力して手動追加',
addManual: '追加',
saveAndNext: '保存して次へ',
alreadyAdded: '追加済み',
modelAlreadyExists: 'モデル「{{name}}」は既に存在します',
},
modelSource: {
title: 'モデルソースを選択',
description: 'AIエンジンにモデル機能を提供する方法を選択してください。',
space: {
title: 'LangBot サービス',
description:
'設定不要で利用可能。無料トライアルモデルクレジット付き。APIキー不要ですぐに使えます。',
action: 'LangBot サービスを使用',
},
custom: {
title: 'カスタムモデル',
description:
'お手持ちのモデルAPIキーを使用。OpenAI、Claude、Geminiなど主要モデルに対応。',
action: '独自のモデルを追加',
},
},
config: { config: {
botInfo: 'ボット情報', botInfo: 'ボット情報',
botNamePlaceholder: 'ボット名を入力', botNamePlaceholder: 'ボット名を入力',
+54
View File
@@ -1661,12 +1661,66 @@ const ruRU = {
title: 'Выберите ИИ-движок', title: 'Выберите ИИ-движок',
description: description:
'Выберите ИИ-движок, который будет управлять интеллектом вашего бота.', 'Выберите ИИ-движок, который будет управлять интеллектом вашего бота.',
orchestrated: {
title: 'Использовать оркестрированные Agent-приложения',
description:
'Использовать готовые Agent-приложения из Dify, n8n, Coze и других.',
action: 'Выбрать Agent-приложение',
selectTitle: 'Выбрать Agent-приложение',
selectDescription:
'Выберите платформу Agent-приложения для использования.',
},
llm: {
title: 'Использовать LLM напрямую',
description:
'Настроить провайдера моделей и использовать LLM для управления ботом.',
action: 'Настроить LLM',
},
}, },
spaceBanner: { spaceBanner: {
message: message:
'Подключитесь к LangBot Space для бесплатных пробных кредитов и мгновенной настройки!', 'Подключитесь к LangBot Space для бесплатных пробных кредитов и мгновенной настройки!',
action: 'Авторизация через Space', action: 'Авторизация через Space',
}, },
provider: {
title: 'Добавить провайдера моделей',
description:
'Добавьте своего провайдера моделей. Настройте API-ключ для начала работы.',
scanTitle: 'Сканирование доступных моделей',
scanDescription: 'Сканирование LLM-моделей у вашего провайдера.',
scanning: 'Сканирование моделей...',
noModelsFound:
'Модели не найдены. Проверьте настройки провайдера.',
addSelected: 'Добавить {{count}} выбранную(ых)',
modelsAdded: 'Добавлено {{count}} модель(ей)',
modelsAddError: 'Ошибка добавления моделей',
skipModelAdd: 'Пропустить, добавить позже',
searchModels: 'Поиск моделей...',
noMatch: 'Нет подходящих моделей',
orDivider: 'или',
manualModelPlaceholder: 'Введите имя модели для ручного добавления',
addManual: 'Добавить',
saveAndNext: 'Сохранить и далее',
alreadyAdded: 'Добавлено',
modelAlreadyExists: 'Модель "{{name}}" уже существует',
},
modelSource: {
title: 'Выберите источник модели',
description:
'Выберите способ предоставления возможностей модели для вашего AI-движка.',
space: {
title: 'Сервис LangBot',
description:
'Готов к использованию без настройки, с бесплатными пробными кредитами. API-ключ не требуется.',
action: 'Использовать сервис LangBot',
},
custom: {
title: 'Пользовательская модель',
description:
'Используйте свой собственный API-ключ. Поддержка OpenAI, Claude, Gemini и других.',
action: 'Добавить свою модель',
},
},
config: { config: {
botInfo: 'Информация о боте', botInfo: 'Информация о боте',
botNamePlaceholder: 'Введите имя бота', botNamePlaceholder: 'Введите имя бота',
+52
View File
@@ -1626,12 +1626,64 @@ const thTH = {
aiEngine: { aiEngine: {
title: 'เลือกเครื่องมือ AI', title: 'เลือกเครื่องมือ AI',
description: 'เลือกเครื่องมือ AI ที่จะขับเคลื่อนความฉลาดของ Bot', description: 'เลือกเครื่องมือ AI ที่จะขับเคลื่อนความฉลาดของ Bot',
orchestrated: {
title: 'ใช้แอป Agent ที่จัดเตรียมไว้',
description:
'ใช้แอป Agent ที่สร้างไว้จาก Dify, n8n, Coze และอื่นๆ',
action: 'เลือกแอป Agent',
selectTitle: 'เลือกแอป Agent',
selectDescription: 'เลือกแพลตฟอร์มแอป Agent ที่ต้องการใช้',
},
llm: {
title: 'ใช้โมเดลโดยตรง',
description:
'กำหนดค่าผู้ให้บริการโมเดลและใช้โมเดลขับเคลื่อน Bot โดยตรง',
action: 'กำหนดค่าโมเดล',
},
}, },
spaceBanner: { spaceBanner: {
message: message:
'เชื่อมต่อกับ LangBot Space เพื่อรับเครดิตทดลองใช้โมเดลฟรีและตั้งค่าทันทีโดยไม่ต้องกำหนดค่า!', 'เชื่อมต่อกับ LangBot Space เพื่อรับเครดิตทดลองใช้โมเดลฟรีและตั้งค่าทันทีโดยไม่ต้องกำหนดค่า!',
action: 'ยืนยันสิทธิ์กับ Space', action: 'ยืนยันสิทธิ์กับ Space',
}, },
provider: {
title: 'เพิ่มผู้ให้บริการโมเดล',
description:
'เพิ่มผู้ให้บริการโมเดลของคุณเอง กำหนดค่า API key เพื่อเริ่มต้น',
scanTitle: 'สแกนโมเดลที่ใช้ได้',
scanDescription: 'กำลังสแกนโมเดล LLM จากผู้ให้บริการของคุณ',
scanning: 'กำลังสแกนโมเดล...',
noModelsFound:
'ไม่พบโมเดล กรุณาตรวจสอบการตั้งค่าผู้ให้บริการ',
addSelected: 'เพิ่ม {{count}} โมเดลที่เลือก',
modelsAdded: 'เพิ่ม {{count}} โมเดลแล้ว',
modelsAddError: 'เพิ่มโมเดลไม่สำเร็จ',
skipModelAdd: 'ข้าม เพิ่มทีหลัง',
searchModels: 'ค้นหาโมเดล...',
noMatch: 'ไม่พบโมเดลที่ตรงกัน',
orDivider: 'หรือ',
manualModelPlaceholder: 'กรอกชื่อโมเดลเพื่อเพิ่มด้วยตนเอง',
addManual: 'เพิ่ม',
saveAndNext: 'บันทึกและถัดไป',
alreadyAdded: 'เพิ่มแล้ว',
modelAlreadyExists: 'โมเดล "{{name}}" มีอยู่แล้ว',
},
modelSource: {
title: 'เลือกแหล่งโมเดล',
description: 'เลือกวิธีการให้ความสามารถของโมเดลสำหรับ AI engine ของคุณ',
space: {
title: 'บริการ LangBot',
description:
'พร้อมใช้งานทันทีไม่ต้องตั้งค่า พร้อมเครดิตทดลองใช้โมเดลฟรี ไม่ต้องใช้ API key',
action: 'ใช้บริการ LangBot',
},
custom: {
title: 'โมเดลกำหนดเอง',
description:
'ใช้ API key โมเดลของคุณเอง รองรับ OpenAI, Claude, Gemini และอื่นๆ',
action: 'เพิ่มโมเดลของฉัน',
},
},
config: { config: {
botInfo: 'ข้อมูล Bot', botInfo: 'ข้อมูล Bot',
botNamePlaceholder: 'กรอกชื่อ Bot', botNamePlaceholder: 'กรอกชื่อ Bot',
+52
View File
@@ -1652,12 +1652,64 @@ const viVN = {
aiEngine: { aiEngine: {
title: 'Chọn công cụ AI', title: 'Chọn công cụ AI',
description: 'Chọn công cụ AI sẽ cung cấp trí tuệ cho Bot của bạn.', description: 'Chọn công cụ AI sẽ cung cấp trí tuệ cho Bot của bạn.',
orchestrated: {
title: 'Sử dụng ứng dụng Agent đã được phối hợp',
description:
'Sử dụng các ứng dụng Agent đã được xây dựng từ Dify, n8n, Coze.',
action: 'Chọn ứng dụng Agent',
selectTitle: 'Chọn ứng dụng Agent',
selectDescription: 'Chọn nền tảng ứng dụng Agent bạn muốn sử dụng.',
},
llm: {
title: 'Sử dụng mô hình trực tiếp',
description:
'Cấu hình nhà cung cấp mô hình và sử dụng mô hình lớn để điều khiển Bot.',
action: 'Cấu hình mô hình',
},
}, },
spaceBanner: { spaceBanner: {
message: message:
'Kết nối với LangBot Space để nhận tín dụng dùng thử mô hình miễn phí và thiết lập tức thì không cần cấu hình!', 'Kết nối với LangBot Space để nhận tín dụng dùng thử mô hình miễn phí và thiết lập tức thì không cần cấu hình!',
action: 'Ủy quyền với Space', action: 'Ủy quyền với Space',
}, },
provider: {
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',
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',
alreadyAdded: 'Đã thêm',
modelAlreadyExists: 'Mô hình "{{name}}" đã tồn tại',
},
modelSource: {
title: 'Chọn nguồn mô hình',
description: 'Chọn cách cung cấp khả năng mô hình cho AI engine của bạn.',
space: {
title: 'Dịch vụ LangBot',
description:
'Sẵn sàng sử dụng không cần cấu hình, tặng tín dụng mô hình thử nghiệm miễn phí. Không cần API key.',
action: 'Sử dụng dịch vụ LangBot',
},
custom: {
title: 'Mô hình tùy chỉnh',
description:
'Sử dụng API key mô hình của riêng bạn. Hỗ trợ OpenAI, Claude, Gemini và nhiều hơn nữa.',
action: 'Thêm mô hình của tôi',
},
},
config: { config: {
botInfo: 'Thông tin Bot', botInfo: 'Thông tin Bot',
botNamePlaceholder: 'Nhập tên Bot', botNamePlaceholder: 'Nhập tên Bot',
+48
View File
@@ -1755,11 +1755,59 @@ const zhHans = {
aiEngine: { aiEngine: {
title: '选择 AI 引擎', title: '选择 AI 引擎',
description: '选择驱动机器人智能的 AI 引擎。', description: '选择驱动机器人智能的 AI 引擎。',
orchestrated: {
title: '使用编排好的 Agent 应用',
description: '使用 Dify、n8n、Coze 等平台编排好的 Agent 应用。',
action: '选择 Agent 应用',
selectTitle: '选择 Agent 应用',
selectDescription: '选择你要使用的 Agent 应用平台。',
},
llm: {
title: '直接使用大模型',
description: '配置大模型供应商,直接使用大模型驱动机器人。',
action: '配置大模型',
},
}, },
spaceBanner: { spaceBanner: {
message: '接入 LangBot Space,获取免费试用模型额度,零配置极速开箱!', message: '接入 LangBot Space,获取免费试用模型额度,零配置极速开箱!',
action: '前往授权登录', action: '前往授权登录',
}, },
provider: {
title: '添加模型供应商',
description: '添加你自己的模型供应商,配置 API Key 后即可使用。',
scanTitle: '扫描可用模型',
scanDescription: '正在从你的供应商中扫描可用的 LLM 模型。',
scanning: '正在扫描模型...',
noModelsFound: '未发现可用模型,请检查供应商配置。',
addSelected: '添加选中的 {{count}} 个模型',
modelsAdded: '已添加 {{count}} 个模型',
modelsAddError: '添加模型失败',
skipModelAdd: '跳过,稍后添加',
searchModels: '搜索模型...',
noMatch: '没有匹配的模型',
orDivider: '或',
manualModelPlaceholder: '输入模型名称手动添加',
addManual: '添加',
saveAndNext: '保存并下一步',
alreadyAdded: '已添加',
modelAlreadyExists: '模型 "{{name}}" 已存在',
},
modelSource: {
title: '选择模型来源',
description: '选择如何为你的 AI 引擎提供模型能力。',
space: {
title: 'LangBot 服务',
description:
'零配置即可使用,提供免费试用模型额度,开箱即用,无需自备 API Key。',
action: '使用 LangBot 服务',
},
custom: {
title: '自定义模型',
description:
'使用你自己的模型 API Key,支持 OpenAI、Claude、Gemini 等主流模型。',
action: '添加我自己的模型',
},
},
config: { config: {
botInfo: '机器人信息', botInfo: '机器人信息',
botNamePlaceholder: '请输入机器人名称', botNamePlaceholder: '请输入机器人名称',
+48
View File
@@ -1578,11 +1578,59 @@ const zhHant = {
aiEngine: { aiEngine: {
title: '選擇 AI 引擎', title: '選擇 AI 引擎',
description: '選擇驅動機器人智慧的 AI 引擎。', description: '選擇驅動機器人智慧的 AI 引擎。',
orchestrated: {
title: '使用編排好的 Agent 應用',
description: '使用 Dify、n8n、Coze 等平台編排好的 Agent 應用。',
action: '選擇 Agent 應用',
selectTitle: '選擇 Agent 應用',
selectDescription: '選擇你要使用的 Agent 應用平台。',
},
llm: {
title: '直接使用大模型',
description: '配置大模型供應商,直接使用大模型驅動機器人。',
action: '配置大模型',
},
}, },
spaceBanner: { spaceBanner: {
message: '接入 LangBot Space,取得免費試用模型額度,零配置極速開箱!', message: '接入 LangBot Space,取得免費試用模型額度,零配置極速開箱!',
action: '前往授權登入', action: '前往授權登入',
}, },
provider: {
title: '新增模型供應商',
description: '新增你自己的模型供應商,配置 API Key 後即可使用。',
scanTitle: '掃描可用模型',
scanDescription: '正在從你的供應商中掃描可用的 LLM 模型。',
scanning: '正在掃描模型...',
noModelsFound: '未發現可用模型,請檢查供應商配置。',
addSelected: '新增選取的 {{count}} 個模型',
modelsAdded: '已新增 {{count}} 個模型',
modelsAddError: '新增模型失敗',
skipModelAdd: '跳過,稍後新增',
searchModels: '搜尋模型...',
noMatch: '沒有符合的模型',
orDivider: '或',
manualModelPlaceholder: '輸入模型名稱手動新增',
addManual: '新增',
saveAndNext: '儲存並下一步',
alreadyAdded: '已新增',
modelAlreadyExists: '模型「{{name}}」已存在',
},
modelSource: {
title: '選擇模型來源',
description: '選擇如何為你的 AI 引擎提供模型能力。',
space: {
title: 'LangBot 服務',
description:
'零配置即可使用,提供免費試用模型額度,開箱即用,無需自備 API Key。',
action: '使用 LangBot 服務',
},
custom: {
title: '自訂模型',
description:
'使用你自己的模型 API Key,支援 OpenAI、Claude、Gemini 等主流模型。',
action: '新增我自己的模型',
},
},
config: { config: {
botInfo: '機器人資訊', botInfo: '機器人資訊',
botNamePlaceholder: '請輸入機器人名稱', botNamePlaceholder: '請輸入機器人名稱',
+6
View File
@@ -11,6 +11,12 @@ export default defineConfig({
}, },
server: { server: {
port: 3000, port: 3000,
proxy: {
'/api': {
target: 'http://127.0.0.1:5300',
changeOrigin: true,
},
},
}, },
build: { build: {
outDir: 'dist', outDir: 'dist',