mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-20 20:44:21 +00:00
feat(agent-runner): add plugin runner host integration
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
IDynamicFormItemSchema,
|
||||
SYSTEM_FIELD_PREFIX,
|
||||
DynamicFormItemType,
|
||||
} from '@/app/infra/entities/form/dynamic';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
@@ -32,6 +33,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { systemInfo } from '@/app/infra/http';
|
||||
import { parseDynamicFormItemType } from './DynamicFormItemConfig';
|
||||
|
||||
/**
|
||||
* Resolve the value referenced by a `show_if.field` string.
|
||||
@@ -290,6 +292,13 @@ function DisabledTooltipIcon({ text }: { text: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize plugin manifest type names to frontend-compatible types
|
||||
*/
|
||||
function normalizeItemType(type: string): DynamicFormItemType {
|
||||
return parseDynamicFormItemType(type);
|
||||
}
|
||||
|
||||
export default function DynamicFormComponent({
|
||||
itemConfigList,
|
||||
onSubmit,
|
||||
@@ -372,8 +381,11 @@ export default function DynamicFormComponent({
|
||||
const formSchema = z.object(
|
||||
editableItems.reduce(
|
||||
(acc, item) => {
|
||||
// Normalize type to handle plugin manifest type names
|
||||
const normalizedType = normalizeItemType(item.type);
|
||||
|
||||
let fieldSchema;
|
||||
switch (item.type) {
|
||||
switch (normalizedType) {
|
||||
case 'integer':
|
||||
fieldSchema = z.number();
|
||||
break;
|
||||
@@ -427,6 +439,9 @@ export default function DynamicFormComponent({
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case 'text':
|
||||
fieldSchema = z.string();
|
||||
break;
|
||||
default:
|
||||
fieldSchema = z.string();
|
||||
}
|
||||
@@ -579,7 +594,14 @@ export default function DynamicFormComponent({
|
||||
}}
|
||||
/>
|
||||
|
||||
{itemConfigList.map((config) => {
|
||||
{itemConfigList.map((config, index) => {
|
||||
// Create a normalized config with type converted to frontend format
|
||||
const normalizedConfig = {
|
||||
...config,
|
||||
type: normalizeItemType(config.type),
|
||||
};
|
||||
const fieldKey = config.id || config.name || `field-${index}`;
|
||||
|
||||
if (config.show_if) {
|
||||
const dependValue = resolveShowIfValue(
|
||||
config.show_if.field,
|
||||
@@ -674,7 +696,7 @@ export default function DynamicFormComponent({
|
||||
}
|
||||
|
||||
// Webhook URL fields are display-only; render outside of form binding
|
||||
if (config.type === 'webhook-url') {
|
||||
if (normalizedConfig.type === 'webhook-url') {
|
||||
const webhookUrl = (systemContext?.webhook_url as string) || '';
|
||||
const extraWebhookUrl =
|
||||
(systemContext?.extra_webhook_url as string) || '';
|
||||
@@ -683,7 +705,7 @@ export default function DynamicFormComponent({
|
||||
|
||||
return (
|
||||
<WebhookUrlField
|
||||
key={config.id}
|
||||
key={fieldKey}
|
||||
label={extractI18nObject(config.label)}
|
||||
description={
|
||||
config.description
|
||||
@@ -696,7 +718,7 @@ export default function DynamicFormComponent({
|
||||
);
|
||||
}
|
||||
|
||||
if (config.type === 'embed-code') {
|
||||
if (normalizedConfig.type === 'embed-code') {
|
||||
const botUuid = (systemContext?.bot_uuid as string) || '';
|
||||
if (!botUuid) return null;
|
||||
|
||||
@@ -714,7 +736,7 @@ export default function DynamicFormComponent({
|
||||
|
||||
return (
|
||||
<EmbedCodeField
|
||||
key={config.id}
|
||||
key={fieldKey}
|
||||
label={extractI18nObject(config.label)}
|
||||
description={
|
||||
config.description
|
||||
@@ -729,7 +751,7 @@ export default function DynamicFormComponent({
|
||||
// QR code login button (e.g. Feishu one-click create, WeChat scan login)
|
||||
if (config.type === 'qr-code-login') {
|
||||
return (
|
||||
<FormItem key={config.id}>
|
||||
<FormItem key={fieldKey}>
|
||||
<div
|
||||
className="relative flex items-center gap-4 p-4 rounded-xl border-2 border-dashed cursor-pointer transition-all hover:border-solid hover:shadow-md group"
|
||||
style={{
|
||||
@@ -787,10 +809,10 @@ export default function DynamicFormComponent({
|
||||
}
|
||||
|
||||
// Boolean fields use a special inline layout
|
||||
if (config.type === 'boolean') {
|
||||
if (normalizedConfig.type === 'boolean') {
|
||||
return (
|
||||
<FormField
|
||||
key={config.id}
|
||||
key={fieldKey}
|
||||
control={form.control}
|
||||
name={config.name as keyof FormValues}
|
||||
render={({ field }) => (
|
||||
@@ -814,7 +836,7 @@ export default function DynamicFormComponent({
|
||||
</div>
|
||||
<FormControl>
|
||||
<DynamicFormItemComponent
|
||||
config={config}
|
||||
config={normalizedConfig}
|
||||
field={field}
|
||||
onFileUploaded={onFileUploaded}
|
||||
/>
|
||||
@@ -829,7 +851,7 @@ export default function DynamicFormComponent({
|
||||
|
||||
return (
|
||||
<FormField
|
||||
key={config.id}
|
||||
key={fieldKey}
|
||||
control={form.control}
|
||||
name={config.name as keyof FormValues}
|
||||
render={({ field }) => (
|
||||
@@ -851,7 +873,7 @@ export default function DynamicFormComponent({
|
||||
)}
|
||||
>
|
||||
<DynamicFormItemComponent
|
||||
config={config}
|
||||
config={normalizedConfig}
|
||||
field={field}
|
||||
onFileUploaded={onFileUploaded}
|
||||
/>
|
||||
|
||||
@@ -251,11 +251,13 @@ export default function DynamicFormItemComponent({
|
||||
switch (config.type) {
|
||||
case DynamicFormItemType.INT:
|
||||
case DynamicFormItemType.FLOAT:
|
||||
case DynamicFormItemType.NUMBER:
|
||||
return (
|
||||
<Input
|
||||
type="number"
|
||||
className="w-full max-w-xs"
|
||||
{...field}
|
||||
value={field.value ?? ''}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
);
|
||||
@@ -264,7 +266,11 @@ export default function DynamicFormItemComponent({
|
||||
if (config.options && config.options.length > 0) {
|
||||
return (
|
||||
<div className="flex w-full max-w-md min-w-0 items-center gap-1.5">
|
||||
<Input className="min-w-0 flex-1" {...field} />
|
||||
<Input
|
||||
className="min-w-0 flex-1"
|
||||
{...field}
|
||||
value={field.value ?? ''}
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
@@ -295,18 +301,37 @@ export default function DynamicFormItemComponent({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <Input className="w-full max-w-md" {...field} />;
|
||||
return (
|
||||
<Input
|
||||
className="w-full max-w-md"
|
||||
{...field}
|
||||
value={field.value ?? ''}
|
||||
/>
|
||||
);
|
||||
|
||||
case DynamicFormItemType.TEXT:
|
||||
return (
|
||||
<Textarea
|
||||
{...field}
|
||||
value={field.value ?? ''}
|
||||
className="min-h-[120px] w-full max-w-full resize-y overflow-x-hidden break-all"
|
||||
/>
|
||||
);
|
||||
|
||||
case DynamicFormItemType.JSON:
|
||||
return (
|
||||
<Textarea
|
||||
{...field}
|
||||
value={field.value ?? ''}
|
||||
className="min-h-[200px] font-mono text-sm"
|
||||
placeholder='{"key": "value"}'
|
||||
/>
|
||||
);
|
||||
|
||||
case DynamicFormItemType.BOOLEAN:
|
||||
return <Switch checked={field.value} onCheckedChange={field.onChange} />;
|
||||
return (
|
||||
<Switch checked={!!field.value} onCheckedChange={field.onChange} />
|
||||
);
|
||||
|
||||
case DynamicFormItemType.STRING_ARRAY:
|
||||
return (
|
||||
@@ -1428,7 +1453,7 @@ export default function DynamicFormItemComponent({
|
||||
{/* 内容输入 */}
|
||||
<Textarea
|
||||
className="min-h-20 w-full min-w-0 flex-1 resize-y overflow-x-hidden break-all sm:w-[300px]"
|
||||
value={item.content}
|
||||
value={item.content ?? ''}
|
||||
onChange={(e) => {
|
||||
const newValue = [...(field.value ?? promptItems)];
|
||||
newValue[index] = {
|
||||
|
||||
@@ -42,6 +42,18 @@ export function isDynamicFormItemType(
|
||||
}
|
||||
|
||||
export function parseDynamicFormItemType(value: string): DynamicFormItemType {
|
||||
const typeMap: Record<string, DynamicFormItemType> = {
|
||||
[DynamicFormItemType.SELECT_LLM_MODEL]:
|
||||
DynamicFormItemType.LLM_MODEL_SELECTOR,
|
||||
[DynamicFormItemType.SELECT_KNOWLEDGE_BASES]:
|
||||
DynamicFormItemType.KNOWLEDGE_BASE_MULTI_SELECTOR,
|
||||
[DynamicFormItemType.NUMBER]: DynamicFormItemType.FLOAT,
|
||||
[DynamicFormItemType.JSON]: DynamicFormItemType.TEXT,
|
||||
};
|
||||
if (value in typeMap) {
|
||||
return typeMap[value];
|
||||
}
|
||||
|
||||
return isDynamicFormItemType(value) ? value : DynamicFormItemType.UNKNOWN;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
PipelineConfigStage,
|
||||
} from '@/app/infra/entities/pipeline';
|
||||
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
|
||||
import N8nAuthFormComponent from '@/app/home/components/dynamic-form/N8nAuthFormComponent';
|
||||
import { useBoxStatus } from '@/app/infra/hooks/useBoxStatus';
|
||||
import { systemInfo } from '@/app/infra/http';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -170,6 +169,8 @@ export default function PipelineFormComponent({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
basic: {
|
||||
name: '',
|
||||
description: '',
|
||||
emoji: '⚙️',
|
||||
},
|
||||
ai: {},
|
||||
@@ -219,10 +220,11 @@ export default function PipelineFormComponent({
|
||||
.getPipeline(pipelineId || '')
|
||||
.then((resp: GetPipelineResponseData) => {
|
||||
setIsDefaultPipeline(resp.pipeline.is_default ?? false);
|
||||
|
||||
const loadedValues = {
|
||||
basic: {
|
||||
name: resp.pipeline.name,
|
||||
description: resp.pipeline.description,
|
||||
name: resp.pipeline.name ?? '',
|
||||
description: resp.pipeline.description ?? '',
|
||||
emoji: resp.pipeline.emoji || '⚙️',
|
||||
},
|
||||
ai: resp.pipeline.config.ai,
|
||||
@@ -235,7 +237,7 @@ export default function PipelineFormComponent({
|
||||
initializedStagesRef.current.clear();
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
}, [form, isEditMode, pipelineId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEditMode) {
|
||||
@@ -308,7 +310,7 @@ export default function PipelineFormComponent({
|
||||
});
|
||||
}
|
||||
|
||||
// Called from DynamicFormComponent/N8nAuthFormComponent onSubmit callbacks.
|
||||
// Called from DynamicFormComponent onSubmit callbacks.
|
||||
// On the first emission for a stage (mount-time default filling), the
|
||||
// snapshot is synchronously re-captured so that hasUnsavedChanges stays false.
|
||||
// However, if the form is already dirty (the user has made real changes),
|
||||
@@ -323,7 +325,7 @@ export default function PipelineFormComponent({
|
||||
const isFirstEmission = !initializedStagesRef.current.has(stageKey);
|
||||
|
||||
const currentValues =
|
||||
(form.getValues(formName) as Record<string, any>) || {};
|
||||
(form.getValues(formName) as Record<string, unknown>) || {};
|
||||
form.setValue(formName, {
|
||||
...currentValues,
|
||||
[stageName]: values,
|
||||
@@ -342,14 +344,47 @@ export default function PipelineFormComponent({
|
||||
}
|
||||
}
|
||||
|
||||
function handleRunnerConfigEmit(stageName: string, values: object) {
|
||||
const stageKey = `ai.runner_config.${stageName}`;
|
||||
const isFirstEmission = !initializedStagesRef.current.has(stageKey);
|
||||
|
||||
const currentRunnerConfigs =
|
||||
(form.getValues('ai.runner_config') as Record<string, unknown>) || {};
|
||||
form.setValue('ai.runner_config', {
|
||||
...currentRunnerConfigs,
|
||||
[stageName]: values,
|
||||
});
|
||||
|
||||
if (isFirstEmission) {
|
||||
initializedStagesRef.current.add(stageKey);
|
||||
const currentSnapshot = JSON.stringify(form.getValues());
|
||||
if (savedSnapshotRef.current === '' || !hasUnsavedChangesRef.current) {
|
||||
savedSnapshotRef.current = currentSnapshot;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderDynamicForms(
|
||||
stage: PipelineConfigStage,
|
||||
formName: keyof FormValues,
|
||||
) {
|
||||
const forcedBoxTemplate =
|
||||
systemInfo.limitation?.force_box_session_id_template || '';
|
||||
const boxScopeForced = !!forcedBoxTemplate;
|
||||
const isLocalAgentRunner =
|
||||
stage.name === 'local-agent' ||
|
||||
stage.name === 'plugin:langbot/local-agent/default';
|
||||
const stageSystemContext = isLocalAgentRunner
|
||||
? {
|
||||
box_available: boxAvailable,
|
||||
box_scope_editable: boxAvailable && !boxScopeForced,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
// Special handling for AI config section
|
||||
if (formName === 'ai') {
|
||||
// Get the currently selected runner
|
||||
const currentRunner = form.watch('ai.runner.runner');
|
||||
const runnerConfig = (form.watch('ai.runner') as any) || {};
|
||||
const currentRunner = runnerConfig.id;
|
||||
|
||||
// If this is the runner selector stage, render it directly
|
||||
if (stage.name === 'runner') {
|
||||
@@ -367,8 +402,9 @@ export default function PipelineFormComponent({
|
||||
<DynamicFormComponent
|
||||
itemConfigList={stage.config}
|
||||
initialValues={
|
||||
(form.watch(formName) as Record<string, any>)?.[stage.name] ||
|
||||
{}
|
||||
(form.watch(formName) as Record<string, unknown>)?.[
|
||||
stage.name
|
||||
] || {}
|
||||
}
|
||||
onSubmit={(values) => {
|
||||
handleDynamicFormEmit(formName, stage.name, values);
|
||||
@@ -384,8 +420,24 @@ export default function PipelineFormComponent({
|
||||
return null;
|
||||
}
|
||||
|
||||
// For n8n-service-api config, use N8nAuthFormComponent for form linkage
|
||||
if (stage.name === 'n8n-service-api') {
|
||||
// For plugin runner configs, store in ai.runner_config[runnerId]
|
||||
|
||||
const isPluginRunner =
|
||||
currentRunner && currentRunner.startsWith('plugin:');
|
||||
const stageSystemContext =
|
||||
stage.name === 'plugin:langbot/local-agent/default'
|
||||
? { box_available: boxAvailable }
|
||||
: undefined;
|
||||
if (isPluginRunner) {
|
||||
const runnerConfigs = (form.watch('ai.runner_config') as any) || {};
|
||||
const stageInitialValues = runnerConfigs[stage.name] || {};
|
||||
const effectiveInitialValues =
|
||||
isLocalAgentRunner && boxScopeForced
|
||||
? {
|
||||
...stageInitialValues,
|
||||
'box-session-id-template': forcedBoxTemplate,
|
||||
}
|
||||
: stageInitialValues;
|
||||
return (
|
||||
<Card key={stage.name}>
|
||||
<CardHeader>
|
||||
@@ -397,15 +449,13 @@ export default function PipelineFormComponent({
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<N8nAuthFormComponent
|
||||
<DynamicFormComponent
|
||||
itemConfigList={stage.config}
|
||||
initialValues={
|
||||
(form.watch(formName) as Record<string, any>)?.[stage.name] ||
|
||||
{}
|
||||
}
|
||||
initialValues={effectiveInitialValues}
|
||||
onSubmit={(values) => {
|
||||
handleDynamicFormEmit(formName, stage.name, values);
|
||||
handleRunnerConfigEmit(stage.name, values);
|
||||
}}
|
||||
systemContext={stageSystemContext}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -418,23 +468,6 @@ export default function PipelineFormComponent({
|
||||
// opt-in via ``disable_if`` + ``disabled_tooltip`` rather than every page
|
||||
// hard-coding a banner. Field-level gating keeps unrelated fields
|
||||
// untouched.
|
||||
//
|
||||
// ``box_scope_editable`` folds the two reasons the Sandbox Scope selector
|
||||
// can be locked into a single flag the yaml ``disable_if`` consumes:
|
||||
// 1. Box sandbox is unavailable, or
|
||||
// 2. the deployment pins all pipelines to a fixed scope via
|
||||
// ``system.limitation.force_box_session_id_template`` (SaaS).
|
||||
const forcedBoxTemplate =
|
||||
systemInfo.limitation?.force_box_session_id_template || '';
|
||||
const boxScopeForced = !!forcedBoxTemplate;
|
||||
const stageSystemContext =
|
||||
stage.name === 'local-agent'
|
||||
? {
|
||||
box_available: boxAvailable,
|
||||
box_scope_editable: boxAvailable && !boxScopeForced,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
// When the deployment pins every pipeline to a fixed sandbox scope (SaaS
|
||||
// ``force_box_session_id_template``), the Sandbox Scope selector is locked.
|
||||
// The runtime already overrides the scope on every exec, but the stored
|
||||
@@ -446,7 +479,7 @@ export default function PipelineFormComponent({
|
||||
const stageInitialValues: Record<string, any> =
|
||||
(form.watch(formName) as Record<string, any>)?.[stage.name] || {};
|
||||
const effectiveInitialValues =
|
||||
stage.name === 'local-agent' && boxScopeForced
|
||||
isLocalAgentRunner && boxScopeForced
|
||||
? {
|
||||
...stageInitialValues,
|
||||
'box-session-id-template': forcedBoxTemplate,
|
||||
@@ -580,7 +613,7 @@ export default function PipelineFormComponent({
|
||||
<span className="text-destructive">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
<Input {...field} value={field.value ?? ''} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -611,7 +644,7 @@ export default function PipelineFormComponent({
|
||||
<FormItem>
|
||||
<FormLabel>{t('common.description')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
<Input {...field} value={field.value ?? ''} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { useEffect, useRef, useState, useCallback, useMemo } from 'react';
|
||||
import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useTheme } from '@/components/providers/theme-provider';
|
||||
@@ -24,7 +24,10 @@ export default function PluginPagesPage() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const id = searchParams.get('id');
|
||||
const { t } = useTranslation();
|
||||
const { setDetailEntityName, pluginPages } = useSidebarData();
|
||||
const { setDetailEntityName, pluginPages, refreshPlugins } = useSidebarData();
|
||||
const [lookupCompleteForId, setLookupCompleteForId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
// Find the matching page for breadcrumb
|
||||
const page = pluginPages.find((p) => p.id === id);
|
||||
@@ -34,6 +37,18 @@ export default function PluginPagesPage() {
|
||||
return () => setDetailEntityName(null);
|
||||
}, [page, id, setDetailEntityName]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id || page) return;
|
||||
let cancelled = false;
|
||||
setLookupCompleteForId(null);
|
||||
refreshPlugins().finally(() => {
|
||||
if (!cancelled) setLookupCompleteForId(id);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [id, page, refreshPlugins]);
|
||||
|
||||
if (!id) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full text-muted-foreground">
|
||||
@@ -54,9 +69,23 @@ export default function PluginPagesPage() {
|
||||
|
||||
const author = parts[0];
|
||||
const pluginName = parts[1];
|
||||
// Use the asset path from the page manifest, not the page ID
|
||||
const assetPath = page?.path ?? parts.slice(2).join('/');
|
||||
const pageId = parts.slice(2).join('/');
|
||||
if (!page) {
|
||||
if (lookupCompleteForId === id) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full text-muted-foreground">
|
||||
{t('pluginPages.invalidPage')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full text-muted-foreground">
|
||||
Loading...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const assetPath = page.path;
|
||||
const pageId = page.pageId;
|
||||
|
||||
return (
|
||||
<PluginPageIframe
|
||||
@@ -84,7 +113,11 @@ function PluginPageIframe({
|
||||
const { resolvedTheme } = useTheme();
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const assetUrl = httpClient.getPluginAssetURL(author, pluginName, pagePath);
|
||||
const assetUrl = useMemo(() => {
|
||||
const url = httpClient.getPluginAssetURL(author, pluginName, pagePath);
|
||||
const separator = url.includes('?') ? '&' : '?';
|
||||
return `${url}${separator}_lb_page_v=${Date.now()}`;
|
||||
}, [author, pluginName, pagePath]);
|
||||
|
||||
// Send context (theme + language) to iframe
|
||||
// Use '*' as targetOrigin because sandboxed iframe has opaque (null) origin
|
||||
|
||||
@@ -70,6 +70,11 @@ export enum DynamicFormItemType {
|
||||
WEBHOOK_URL = 'webhook-url',
|
||||
EMBED_CODE = 'embed-code',
|
||||
QR_CODE_LOGIN = 'qr-code-login',
|
||||
// Plugin manifest type aliases for compatibility
|
||||
SELECT_LLM_MODEL = 'select-llm-model',
|
||||
SELECT_KNOWLEDGE_BASES = 'select-knowledge-bases',
|
||||
NUMBER = 'number',
|
||||
JSON = 'json',
|
||||
}
|
||||
|
||||
export interface IFileConfig {
|
||||
|
||||
+49
-25
@@ -86,7 +86,6 @@ export default function WizardPage() {
|
||||
const [selectedAdapter, setSelectedAdapter] = useState<string | null>(null);
|
||||
const [selectedRunner, setSelectedRunner] = useState<string | null>(null);
|
||||
const [botName, setBotName] = useState('');
|
||||
|
||||
const [botDescription, _setBotDescription] = useState('');
|
||||
const [adapterConfig, setAdapterConfig] = useState<Record<string, unknown>>(
|
||||
{},
|
||||
@@ -202,7 +201,7 @@ export default function WizardPage() {
|
||||
|
||||
const runnerOptions = useMemo(() => {
|
||||
if (!runnerStage) return [];
|
||||
const runnerField = runnerStage.config.find((c) => c.name === 'runner');
|
||||
const runnerField = runnerStage.config.find((c) => c.name === 'id');
|
||||
return runnerField?.options ?? [];
|
||||
}, [runnerStage]);
|
||||
|
||||
@@ -257,9 +256,11 @@ export default function WizardPage() {
|
||||
const handleSelectRunner = useCallback(
|
||||
(runner: string) => {
|
||||
setSelectedRunner(runner);
|
||||
const configStage = aiConfigTab?.stages.find((s) => s.name === runner);
|
||||
setRunnerConfig(configStage ? getDefaultValues(configStage.config) : {});
|
||||
saveProgress({ step: 2, selected_runner: runner });
|
||||
},
|
||||
[saveProgress],
|
||||
[aiConfigTab, saveProgress],
|
||||
);
|
||||
|
||||
// ---- Navigation helpers ----
|
||||
@@ -427,14 +428,36 @@ export default function WizardPage() {
|
||||
// (includes trigger, safety, ai, output sections).
|
||||
// Then merge only the AI section with the wizard's runner config.
|
||||
const createdPipeline = await httpClient.getPipeline(pipelineResp.uuid);
|
||||
const fullConfig = createdPipeline.pipeline.config;
|
||||
const fullConfig = createdPipeline.pipeline.config as unknown as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const fullAiConfig =
|
||||
fullConfig.ai && typeof fullConfig.ai === 'object'
|
||||
? (fullConfig.ai as Record<string, unknown>)
|
||||
: {};
|
||||
const existingRunner =
|
||||
fullAiConfig.runner && typeof fullAiConfig.runner === 'object'
|
||||
? (fullAiConfig.runner as Record<string, unknown>)
|
||||
: {};
|
||||
const existingRunnerConfigs =
|
||||
fullAiConfig.runner_config &&
|
||||
typeof fullAiConfig.runner_config === 'object'
|
||||
? (fullAiConfig.runner_config as Record<string, unknown>)
|
||||
: {};
|
||||
|
||||
const mergedConfig = {
|
||||
...fullConfig,
|
||||
ai: {
|
||||
...fullConfig.ai,
|
||||
runner: { runner: selectedRunner },
|
||||
[selectedRunner]: runnerConfig,
|
||||
...fullAiConfig,
|
||||
runner: {
|
||||
...existingRunner,
|
||||
id: selectedRunner,
|
||||
},
|
||||
runner_config: {
|
||||
...existingRunnerConfigs,
|
||||
[selectedRunner]: runnerConfig,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1113,26 +1136,27 @@ function StepAIEngine({
|
||||
})}
|
||||
|
||||
{/* Space promotion banner */}
|
||||
{selected === 'local-agent' && isLocalAccount && (
|
||||
<div className="animate-in fade-in slide-in-from-left-2 duration-300">
|
||||
<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" />
|
||||
<p className="text-xs font-medium">
|
||||
{t('wizard.spaceBanner.message')}
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onSpaceAuth}
|
||||
className="w-full"
|
||||
>
|
||||
{t('wizard.spaceBanner.action')}
|
||||
</Button>
|
||||
{selected === 'plugin:langbot/local-agent/default' &&
|
||||
isLocalAccount && (
|
||||
<div className="animate-in fade-in slide-in-from-left-2 duration-300">
|
||||
<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" />
|
||||
<p className="text-xs font-medium">
|
||||
{t('wizard.spaceBanner.message')}
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onSpaceAuth}
|
||||
className="w-full"
|
||||
>
|
||||
{t('wizard.spaceBanner.action')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user