mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-04 04:54:36 +00:00
Feat/onboarding wizard (#2086)
* feat(web): add onboarding wizard for guided bot creation
Implement a full-screen 4-step wizard at /wizard that guides users
through selecting a platform, configuring a bot, choosing an AI engine,
and completing setup. The wizard uses DynamicFormComponent for adapter
and pipeline configuration, embeds BotLogListComponent for real-time
debugging, persists state to localStorage, and integrates with Space
OAuth flow. Also fixes a prompt-editor crash in DynamicFormComponent
when value is undefined.
* feat(wizard): redesign step 0/1 flow, add skip dialog, auto-expand log images
- Step 0: Remove bot name/description fields; auto-derive name from adapter
label; create disabled bot on confirm; advance to Step 1 automatically
- Step 1: Replace 'Create Bot' with 'Save & Enable Bot'; update adapter
config and enable bot; disable form fields after saving
- Add skip confirmation AlertDialog with i18n message
- Add LanguageSelector to wizard header
- Move wizard sidebar entry to last position to prevent fallback redirect loop
- Add defaultExpanded prop to BotLogCard; auto-expand entries with images
in wizard via autoExpandImages prop on BotLogListComponent
- Remove automatic default pipeline creation (write_default_pipeline) from
backend persistence manager since the wizard now handles pipeline creation
- Update all 4 locale files (en-US, zh-Hans, zh-Hant, ja-JP)
* fix(wizard): hide detailed logs link in wizard, allow re-editing bot config after save
- Add hideDetailedLogsLink prop to BotLogListComponent; pass it in wizard
- Remove isEditing on DynamicFormComponent so form stays editable after save
- Always show save button; label changes to 'Re-save' after first save
- Add resaveBot i18n key to all 4 locale files
* style(wizard): move save button into config card header
* fix(wizard): initialize userInfo/systemInfo so model selector works
The wizard runs outside /home layout, so userInfo was null. This caused
the model-fallback-selector to filter out all Space models, showing an
empty dropdown. Fix by calling initializeUserInfo() and
initializeSystemInfo() before fetching wizard data.
Also:
- Hide log toolbar in wizard via hideToolbar prop on BotLogListComponent
- Add empty state message for bot logs (noLogs i18n key, all 4 locales)
* feat(wizard): redesign AI Engine step with left-right split layout
Before selecting a runner: centered grid of runner cards.
After selecting: left panel shows compact runner list for switching,
right panel shows runner config form with slide-in animations.
Also fix prompt field default: add default value to prompt-editor field
in ai.yaml metadata so the prompt is pre-populated with
'You are a helpful assistant.' instead of being empty.
* feat(pipeline): add default values to ai.yaml runner configs and show_if for n8n auth fields
- Sync default values from default-pipeline-config.json to all runner
config fields in ai.yaml so wizard forms are pre-populated
- Add show_if conditions to n8n-service-api auth fields so only the
relevant credentials appear based on selected auth-type
- Fix prompt-editor crash in DynamicFormItemComponent when field.value
is undefined (Array.isArray guard + fallback)
- Improve wizard Step 2 split layout with fixed column widths,
independent scroll, ring clipping fix, and mobile responsiveness
- Use key={selected} on DynamicFormComponent to force remount on
runner switch
- Improve pipeline creation flow: create → fetch defaults → merge AI
section → update (preserves trigger/safety/output defaults)
* feat(dynamic-form): add systemContext prop with __system.* namespace for show_if conditions
- Add systemContext prop to DynamicFormComponent for injecting external
variables accessible via __system.* prefix in show_if conditions
- Extract resolveShowIfValue() helper for cleaner field resolution
- Pass { is_wizard: true } from wizard to hide knowledge-bases field
- Remove bot config save toast in wizard (keep inline indicator)
* feat(sidebar): render wizard as standalone item before Home group with fallback redirect fix
* fix(wizard): remove unused setBotDescription to fix lint error
This commit is contained in:
@@ -2,18 +2,16 @@ from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import typing
|
||||
import json
|
||||
import uuid
|
||||
|
||||
|
||||
import sqlalchemy.ext.asyncio as sqlalchemy_asyncio
|
||||
import sqlalchemy
|
||||
|
||||
from . import database, migration
|
||||
from ..entity.persistence import base, pipeline, metadata, model as persistence_model
|
||||
from ..entity.persistence import base, metadata, model as persistence_model
|
||||
from ..entity import persistence
|
||||
from ..core import app
|
||||
from ..utils import constants, importutil
|
||||
from ..api.http.service import pipeline as pipeline_service
|
||||
from . import databases, migrations
|
||||
|
||||
importutil.import_modules_in_pkg(databases)
|
||||
@@ -78,7 +76,6 @@ class PersistenceManager:
|
||||
|
||||
self.ap.logger.info(f'Successfully upgraded database to version {last_migration_number}.')
|
||||
|
||||
await self.write_default_pipeline()
|
||||
await self.write_space_model_providers()
|
||||
|
||||
async def create_tables(self):
|
||||
@@ -101,29 +98,6 @@ class PersistenceManager:
|
||||
if row is None:
|
||||
await self.execute_async(sqlalchemy.insert(metadata.Metadata).values(item))
|
||||
|
||||
async def write_default_pipeline(self):
|
||||
# write default pipeline
|
||||
result = await self.execute_async(sqlalchemy.select(pipeline.LegacyPipeline))
|
||||
default_pipeline_uuid = None
|
||||
if result.first() is None:
|
||||
self.ap.logger.info('Creating default pipeline...')
|
||||
|
||||
pipeline_config = json.loads(importutil.read_resource_file('templates/default-pipeline-config.json'))
|
||||
|
||||
default_pipeline_uuid = str(uuid.uuid4())
|
||||
pipeline_data = {
|
||||
'uuid': default_pipeline_uuid,
|
||||
'for_version': self.ap.ver_mgr.get_current_version(),
|
||||
'stages': pipeline_service.default_stage_order,
|
||||
'is_default': True,
|
||||
'name': 'ChatPipeline',
|
||||
'description': 'Default pipeline, new bots will be bound to this pipeline | 默认提供的流水线,您配置的机器人将自动绑定到此流水线',
|
||||
'config': pipeline_config,
|
||||
'extensions_preferences': {},
|
||||
}
|
||||
|
||||
await self.execute_async(sqlalchemy.insert(pipeline.LegacyPipeline).values(pipeline_data))
|
||||
|
||||
async def write_space_model_providers(self):
|
||||
space_models_gateway_api_url = self.ap.instance_config.data.get('space', {}).get(
|
||||
'models_gateway_api_url', 'https://api.langbot.cloud/v1'
|
||||
|
||||
@@ -74,6 +74,10 @@ stages:
|
||||
type: integer
|
||||
required: true
|
||||
default: 10
|
||||
show_if:
|
||||
field: __system.is_wizard
|
||||
operator: neq
|
||||
value: true
|
||||
- name: prompt
|
||||
label:
|
||||
en_US: Prompt
|
||||
@@ -83,6 +87,9 @@ stages:
|
||||
zh_Hans: 除非您了解消息结构,否则请只使用 system 单提示词
|
||||
type: prompt-editor
|
||||
required: true
|
||||
default:
|
||||
- role: system
|
||||
content: "You are a helpful assistant."
|
||||
- name: knowledge-bases
|
||||
label:
|
||||
en_US: Knowledge Bases
|
||||
@@ -93,6 +100,10 @@ stages:
|
||||
type: knowledge-base-multi-selector
|
||||
required: false
|
||||
default: []
|
||||
show_if:
|
||||
field: __system.is_wizard
|
||||
operator: neq
|
||||
value: true
|
||||
- name: tbox-app-api
|
||||
label:
|
||||
en_US: Tbox App API
|
||||
@@ -107,12 +118,14 @@ stages:
|
||||
zh_Hans: API 密钥
|
||||
type: string
|
||||
required: true
|
||||
default: ''
|
||||
- name: app-id
|
||||
label:
|
||||
en_US: App ID
|
||||
zh_Hans: 应用 ID
|
||||
type: string
|
||||
required: true
|
||||
default: ''
|
||||
- name: dify-service-api
|
||||
label:
|
||||
en_US: Dify Service API
|
||||
@@ -127,6 +140,7 @@ stages:
|
||||
zh_Hans: 基础 URL
|
||||
type: string
|
||||
required: true
|
||||
default: 'https://api.dify.ai/v1'
|
||||
- name: base-prompt
|
||||
label:
|
||||
en_US: Base PROMPT
|
||||
@@ -163,6 +177,7 @@ stages:
|
||||
zh_Hans: API 密钥
|
||||
type: string
|
||||
required: true
|
||||
default: 'your-api-key'
|
||||
- name: dashscope-app-api
|
||||
label:
|
||||
en_US: Aliyun Dashscope App API
|
||||
@@ -193,12 +208,14 @@ stages:
|
||||
zh_Hans: API 密钥
|
||||
type: string
|
||||
required: true
|
||||
default: 'your-api-key'
|
||||
- name: app-id
|
||||
label:
|
||||
en_US: App ID
|
||||
zh_Hans: 应用 ID
|
||||
type: string
|
||||
required: true
|
||||
default: 'your-app-id'
|
||||
- name: references_quote
|
||||
label:
|
||||
en_US: References Quote
|
||||
@@ -226,6 +243,7 @@ stages:
|
||||
zh_Hans: n8n 工作流的 webhook URL
|
||||
type: string
|
||||
required: true
|
||||
default: 'http://your-n8n-webhook-url'
|
||||
- name: auth-type
|
||||
label:
|
||||
en_US: Authentication Type
|
||||
@@ -263,6 +281,10 @@ stages:
|
||||
type: string
|
||||
required: false
|
||||
default: ''
|
||||
show_if:
|
||||
field: auth-type
|
||||
operator: eq
|
||||
value: 'basic'
|
||||
- name: basic-password
|
||||
label:
|
||||
en_US: Password
|
||||
@@ -273,6 +295,10 @@ stages:
|
||||
type: string
|
||||
required: false
|
||||
default: ''
|
||||
show_if:
|
||||
field: auth-type
|
||||
operator: eq
|
||||
value: 'basic'
|
||||
- name: jwt-secret
|
||||
label:
|
||||
en_US: Secret
|
||||
@@ -283,6 +309,10 @@ stages:
|
||||
type: string
|
||||
required: false
|
||||
default: ''
|
||||
show_if:
|
||||
field: auth-type
|
||||
operator: eq
|
||||
value: 'jwt'
|
||||
- name: jwt-algorithm
|
||||
label:
|
||||
en_US: Algorithm
|
||||
@@ -293,6 +323,10 @@ stages:
|
||||
type: string
|
||||
required: false
|
||||
default: 'HS256'
|
||||
show_if:
|
||||
field: auth-type
|
||||
operator: eq
|
||||
value: 'jwt'
|
||||
- name: header-name
|
||||
label:
|
||||
en_US: Header Name
|
||||
@@ -303,6 +337,10 @@ stages:
|
||||
type: string
|
||||
required: false
|
||||
default: ''
|
||||
show_if:
|
||||
field: auth-type
|
||||
operator: eq
|
||||
value: 'header'
|
||||
- name: header-value
|
||||
label:
|
||||
en_US: Header Value
|
||||
@@ -313,6 +351,10 @@ stages:
|
||||
type: string
|
||||
required: false
|
||||
default: ''
|
||||
show_if:
|
||||
field: auth-type
|
||||
operator: eq
|
||||
value: 'header'
|
||||
- name: timeout
|
||||
label:
|
||||
en_US: Timeout
|
||||
@@ -350,6 +392,7 @@ stages:
|
||||
zh_Hans: Langflow 服务器的基础 URL
|
||||
type: string
|
||||
required: true
|
||||
default: 'http://localhost:7860'
|
||||
- name: api-key
|
||||
label:
|
||||
en_US: API Key
|
||||
@@ -359,6 +402,7 @@ stages:
|
||||
zh_Hans: Langflow 服务器的 API 密钥
|
||||
type: string
|
||||
required: true
|
||||
default: 'your-api-key'
|
||||
- name: flow-id
|
||||
label:
|
||||
en_US: Flow ID
|
||||
@@ -368,6 +412,7 @@ stages:
|
||||
zh_Hans: 要运行的流程 ID
|
||||
type: string
|
||||
required: true
|
||||
default: 'your-flow-id'
|
||||
- name: input-type
|
||||
label:
|
||||
en_US: Input Type
|
||||
@@ -415,6 +460,7 @@ stages:
|
||||
zh_Hans: Coze服务器的 API 密钥
|
||||
type: string
|
||||
required: true
|
||||
default: ''
|
||||
- name: bot-id
|
||||
label:
|
||||
en_US: Bot ID
|
||||
@@ -424,6 +470,7 @@ stages:
|
||||
zh_Hans: 要运行的机器人 ID
|
||||
type: string
|
||||
required: true
|
||||
default: ''
|
||||
- name: api-base
|
||||
label:
|
||||
en_US: API Base URL
|
||||
|
||||
@@ -46,8 +46,12 @@ function SpaceOAuthCallbackContent() {
|
||||
}
|
||||
setStatus('success');
|
||||
toast.success(t('common.spaceLoginSuccess'));
|
||||
|
||||
// If wizard state exists, redirect back to wizard instead of home
|
||||
const wizardState = localStorage.getItem('langbot_wizard_state');
|
||||
const redirectTo = wizardState ? '/wizard' : '/home';
|
||||
setTimeout(() => {
|
||||
router.push('/home');
|
||||
router.push(redirectTo);
|
||||
}, 1000);
|
||||
} catch (err) {
|
||||
setStatus('error');
|
||||
|
||||
@@ -19,11 +19,17 @@ const LEVEL_STYLES: Record<string, string> = {
|
||||
|
||||
const SHORT_TEXT_LIMIT = 120;
|
||||
|
||||
export function BotLogCard({ botLog }: { botLog: BotLog }) {
|
||||
export function BotLogCard({
|
||||
botLog,
|
||||
defaultExpanded = false,
|
||||
}: {
|
||||
botLog: BotLog;
|
||||
defaultExpanded?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const baseURL = httpClient.getBaseUrl();
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [expanded, setExpanded] = useState(defaultExpanded);
|
||||
|
||||
function copySessionId() {
|
||||
const text = botLog.message_session_id;
|
||||
|
||||
@@ -17,7 +17,20 @@ import { debounce } from 'lodash';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export function BotLogListComponent({ botId }: { botId: string }) {
|
||||
export function BotLogListComponent({
|
||||
botId,
|
||||
autoExpandImages = false,
|
||||
hideDetailedLogsLink = false,
|
||||
hideToolbar = false,
|
||||
}: {
|
||||
botId: string;
|
||||
/** When true, log entries with images are rendered expanded by default */
|
||||
autoExpandImages?: boolean;
|
||||
/** When true, hides the "View Detailed Logs" navigation button */
|
||||
hideDetailedLogsLink?: boolean;
|
||||
/** When true, hides the entire toolbar (auto-refresh, level filter, detailed logs link) */
|
||||
hideToolbar?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const manager = useRef(new BotLogManager(botId)).current;
|
||||
@@ -158,77 +171,91 @@ export function BotLogListComponent({ botId }: { botId: string }) {
|
||||
ref={listContainerRef}
|
||||
>
|
||||
{/* Toolbar */}
|
||||
<div className="flex items-center gap-3 pb-3 shrink-0 flex-wrap">
|
||||
{/* Auto-refresh toggle */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t('bots.enableAutoRefresh')}
|
||||
</span>
|
||||
<Switch
|
||||
checked={autoFlush}
|
||||
onCheckedChange={(v) => setAutoFlush(v)}
|
||||
/>
|
||||
</div>
|
||||
{!hideToolbar && (
|
||||
<div className="flex items-center gap-3 pb-3 shrink-0 flex-wrap">
|
||||
{/* Auto-refresh toggle */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t('bots.enableAutoRefresh')}
|
||||
</span>
|
||||
<Switch
|
||||
checked={autoFlush}
|
||||
onCheckedChange={(v) => setAutoFlush(v)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Level filter */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t('bots.logLevel')}
|
||||
</span>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-[160px] justify-between"
|
||||
>
|
||||
<span className="text-sm truncate">{getDisplayText()}</span>
|
||||
<ChevronDownIcon className="size-3.5 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[160px] p-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
{logLevels.map((level) => (
|
||||
<div
|
||||
key={level.value}
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<Checkbox
|
||||
id={level.value}
|
||||
checked={selectedLevels.includes(level.value)}
|
||||
onCheckedChange={() => handleLevelToggle(level.value)}
|
||||
/>
|
||||
<label
|
||||
htmlFor={level.value}
|
||||
className="text-sm font-medium leading-none cursor-pointer"
|
||||
{/* Level filter */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t('bots.logLevel')}
|
||||
</span>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-[160px] justify-between"
|
||||
>
|
||||
<span className="text-sm truncate">{getDisplayText()}</span>
|
||||
<ChevronDownIcon className="size-3.5 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[160px] p-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
{logLevels.map((level) => (
|
||||
<div
|
||||
key={level.value}
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
{level.label}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<Checkbox
|
||||
id={level.value}
|
||||
checked={selectedLevels.includes(level.value)}
|
||||
onCheckedChange={() => handleLevelToggle(level.value)}
|
||||
/>
|
||||
<label
|
||||
htmlFor={level.value}
|
||||
className="text-sm font-medium leading-none cursor-pointer"
|
||||
>
|
||||
{level.label}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
{/* Link to detailed logs */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-1"
|
||||
onClick={() => router.push(`/home/monitoring?botId=${botId}`)}
|
||||
>
|
||||
<ExternalLink className="size-3.5" />
|
||||
<span className="text-sm">{t('bots.viewDetailedLogs')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
{/* Link to detailed logs */}
|
||||
{!hideDetailedLogsLink && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-1"
|
||||
onClick={() => router.push(`/home/monitoring?botId=${botId}`)}
|
||||
>
|
||||
<ExternalLink className="size-3.5" />
|
||||
<span className="text-sm">{t('bots.viewDetailedLogs')}</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Log cards */}
|
||||
<div className="flex flex-col gap-2">
|
||||
{filteredLogs.map((botLog) => (
|
||||
<BotLogCard botLog={botLog} key={botLog.seq_id} />
|
||||
))}
|
||||
</div>
|
||||
{filteredLogs.length === 0 ? (
|
||||
<div className="flex-1 flex items-center justify-center py-12">
|
||||
<p className="text-sm text-muted-foreground">{t('bots.noLogs')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
{filteredLogs.map((botLog) => (
|
||||
<BotLogCard
|
||||
botLog={botLog}
|
||||
key={botLog.seq_id}
|
||||
defaultExpanded={autoExpandImages && botLog.images.length > 0}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,30 @@ import { extractI18nObject } from '@/i18n/I18nProvider';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
/**
|
||||
* Resolve the value referenced by a `show_if.field` string.
|
||||
*
|
||||
* Fields prefixed with `__system.` are looked up in the caller-supplied
|
||||
* `systemContext` dictionary (e.g. `__system.is_wizard` → `systemContext.is_wizard`).
|
||||
* All other field names are resolved from the live form values first, then
|
||||
* fall back to `externalDependentValues`.
|
||||
*/
|
||||
function resolveShowIfValue(
|
||||
field: string,
|
||||
watchedValues: Record<string, unknown>,
|
||||
externalDependentValues?: Record<string, unknown>,
|
||||
systemContext?: Record<string, unknown>,
|
||||
): unknown {
|
||||
if (field.startsWith('__system.')) {
|
||||
const key = field.slice('__system.'.length);
|
||||
return systemContext?.[key];
|
||||
}
|
||||
if (watchedValues[field] !== undefined) {
|
||||
return watchedValues[field];
|
||||
}
|
||||
return externalDependentValues?.[field];
|
||||
}
|
||||
|
||||
export default function DynamicFormComponent({
|
||||
itemConfigList,
|
||||
onSubmit,
|
||||
@@ -23,6 +47,7 @@ export default function DynamicFormComponent({
|
||||
onFileUploaded,
|
||||
isEditing,
|
||||
externalDependentValues,
|
||||
systemContext,
|
||||
}: {
|
||||
itemConfigList: IDynamicFormItemSchema[];
|
||||
onSubmit?: (val: object) => unknown;
|
||||
@@ -30,6 +55,9 @@ export default function DynamicFormComponent({
|
||||
onFileUploaded?: (fileKey: string) => void;
|
||||
isEditing?: boolean;
|
||||
externalDependentValues?: Record<string, unknown>;
|
||||
/** Extra variables accessible via the `__system.*` namespace in show_if conditions.
|
||||
* e.g. `{ is_wizard: true }` makes `show_if: { field: "__system.is_wizard", ... }` work. */
|
||||
systemContext?: Record<string, unknown>;
|
||||
}) {
|
||||
const isInitialMount = useRef(true);
|
||||
const previousInitialValues = useRef(initialValues);
|
||||
@@ -61,6 +89,13 @@ export default function DynamicFormComponent({
|
||||
fallbacks: [],
|
||||
};
|
||||
}
|
||||
if (item.type === 'prompt-editor') {
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
// Default to a single empty system prompt entry
|
||||
return [{ role: 'system', content: '' }];
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
@@ -241,14 +276,12 @@ export default function DynamicFormComponent({
|
||||
<div className="space-y-4">
|
||||
{itemConfigList.map((config) => {
|
||||
if (config.show_if) {
|
||||
const dependValue =
|
||||
watchedValues[
|
||||
config.show_if.field as keyof typeof watchedValues
|
||||
] !== undefined
|
||||
? watchedValues[
|
||||
config.show_if.field as keyof typeof watchedValues
|
||||
]
|
||||
: externalDependentValues?.[config.show_if.field];
|
||||
const dependValue = resolveShowIfValue(
|
||||
config.show_if.field,
|
||||
watchedValues as Record<string, unknown>,
|
||||
externalDependentValues,
|
||||
systemContext,
|
||||
);
|
||||
|
||||
if (
|
||||
config.show_if.operator === 'eq' &&
|
||||
|
||||
@@ -775,10 +775,18 @@ export default function DynamicFormItemComponent({
|
||||
</Select>
|
||||
);
|
||||
|
||||
case DynamicFormItemType.PROMPT_EDITOR:
|
||||
case DynamicFormItemType.PROMPT_EDITOR: {
|
||||
// Guard: field.value may be undefined when the form resets or
|
||||
// initialValues haven't propagated yet. Fall back to a default
|
||||
// single system-prompt entry to prevent the .map() crash.
|
||||
const promptItems: { role: string; content: string }[] = Array.isArray(
|
||||
field.value,
|
||||
)
|
||||
? field.value
|
||||
: [{ role: 'system', content: '' }];
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{field.value.map(
|
||||
{promptItems.map(
|
||||
(item: { role: string; content: string }, index: number) => (
|
||||
<div key={index} className="flex gap-2 items-center">
|
||||
{/* 角色选择 */}
|
||||
@@ -790,7 +798,7 @@ export default function DynamicFormItemComponent({
|
||||
<Select
|
||||
value={item.role}
|
||||
onValueChange={(value) => {
|
||||
const newValue = [...field.value];
|
||||
const newValue = [...(field.value ?? promptItems)];
|
||||
newValue[index] = { ...newValue[index], role: value };
|
||||
field.onChange(newValue);
|
||||
}}
|
||||
@@ -811,7 +819,7 @@ export default function DynamicFormItemComponent({
|
||||
className="w-[300px]"
|
||||
value={item.content}
|
||||
onChange={(e) => {
|
||||
const newValue = [...field.value];
|
||||
const newValue = [...(field.value ?? promptItems)];
|
||||
newValue[index] = {
|
||||
...newValue[index],
|
||||
content: e.target.value,
|
||||
@@ -825,7 +833,7 @@ export default function DynamicFormItemComponent({
|
||||
type="button"
|
||||
className="p-2 hover:bg-gray-100 rounded"
|
||||
onClick={() => {
|
||||
const newValue = field.value.filter(
|
||||
const newValue = (field.value ?? promptItems).filter(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(_: any, i: number) => i !== index,
|
||||
);
|
||||
@@ -849,13 +857,17 @@ export default function DynamicFormItemComponent({
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
field.onChange([...field.value, { role: 'user', content: '' }]);
|
||||
field.onChange([
|
||||
...(field.value ?? promptItems),
|
||||
{ role: 'user', content: '' },
|
||||
]);
|
||||
}}
|
||||
>
|
||||
{t('common.addRound')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
case DynamicFormItemType.FILE:
|
||||
return (
|
||||
|
||||
@@ -1181,8 +1181,11 @@ export default function HomeSidebar({
|
||||
// Route already matches — just select without navigating (preserves ?id= query params)
|
||||
selectChild(matchedChild);
|
||||
} else {
|
||||
// No match — redirect to default route
|
||||
handleChildClick(sidebarConfigList[0]);
|
||||
// No match — redirect to the first route under /home
|
||||
const defaultChild =
|
||||
sidebarConfigList.find((c) => c.route.startsWith('/home')) ??
|
||||
sidebarConfigList[0];
|
||||
handleChildClick(defaultChild);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1249,6 +1252,28 @@ export default function HomeSidebar({
|
||||
|
||||
{/* Navigation items grouped by section */}
|
||||
<SidebarContent>
|
||||
{/* Standalone items (e.g. Quick Start) — rendered before section groups */}
|
||||
{sidebarConfigList
|
||||
.filter((c) => c.section === 'standalone')
|
||||
.map((config) => (
|
||||
<SidebarGroup key={config.id}>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
isActive={selectedChild?.id === config.id}
|
||||
onClick={() => handleChildClick(config)}
|
||||
tooltip={config.name}
|
||||
>
|
||||
{config.icon}
|
||||
<span>{config.name}</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
))}
|
||||
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>{t('sidebar.home')}</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { I18nObject } from '@/app/infra/entities/common';
|
||||
|
||||
export type SidebarSection = 'home' | 'extensions';
|
||||
export type SidebarSection = 'home' | 'extensions' | 'standalone';
|
||||
|
||||
export interface ISidebarChildVO {
|
||||
id: string;
|
||||
|
||||
@@ -6,6 +6,28 @@ const t = (key: string) => {
|
||||
};
|
||||
|
||||
export const sidebarConfigList = [
|
||||
// ── Quick Start ──
|
||||
new SidebarChildVO({
|
||||
id: 'wizard',
|
||||
name: t('sidebar.quickStart'),
|
||||
icon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M13 9H21L11 24V15H4L13 0V9ZM11 11V7.22063L7.53238 13H13V17.3944L17.263 11H11Z"></path>
|
||||
</svg>
|
||||
),
|
||||
route: '/wizard',
|
||||
description: t('wizard.sidebarDescription'),
|
||||
helpLink: {
|
||||
en_US: '',
|
||||
zh_Hans: '',
|
||||
},
|
||||
section: 'standalone',
|
||||
}),
|
||||
|
||||
// ── Home section ──
|
||||
new SidebarChildVO({
|
||||
id: 'monitoring',
|
||||
|
||||
1086
web/src/app/wizard/page.tsx
Normal file
1086
web/src/app/wizard/page.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ const enUS = {
|
||||
installedPlugins: 'Installed Plugins',
|
||||
pluginMarket: 'Marketplace',
|
||||
mcpServers: 'MCP Servers',
|
||||
quickStart: 'Quick Start',
|
||||
},
|
||||
common: {
|
||||
login: 'Login',
|
||||
@@ -321,6 +322,7 @@ const enUS = {
|
||||
viewDetails: 'Details',
|
||||
collapse: 'Collapse',
|
||||
imagesAttached: 'image(s) attached',
|
||||
noLogs: 'No logs yet',
|
||||
sessionMonitor: {
|
||||
title: 'Sessions',
|
||||
sessions: 'Sessions',
|
||||
@@ -1109,6 +1111,68 @@ const enUS = {
|
||||
maxExtensionsReached:
|
||||
'Maximum number of extensions ({{max}}) reached. Please remove an existing MCP server or plugin before adding a new one.',
|
||||
},
|
||||
wizard: {
|
||||
sidebarDescription: 'Create a bot with guided steps',
|
||||
loading: 'Loading wizard...',
|
||||
loadError: 'Failed to load wizard data',
|
||||
skip: 'Skip',
|
||||
skipConfirmMessage:
|
||||
'You can re-enter the Quick Start wizard from the sidebar later, or create a bot manually.',
|
||||
skipConfirmOk: 'OK',
|
||||
prev: 'Previous',
|
||||
next: 'Next',
|
||||
finish: 'Create & Deploy',
|
||||
confirmCreateBot: 'Confirm, Create Bot',
|
||||
createSuccess: 'Pipeline created and linked to bot successfully!',
|
||||
botCreateSuccess: 'Bot created successfully!',
|
||||
botSaveSuccess: 'Bot configuration saved and enabled!',
|
||||
createError: 'Failed to create resources',
|
||||
spaceAuthError: 'Failed to initiate Space authorization',
|
||||
step: {
|
||||
platform: 'Platform',
|
||||
botConfig: 'Bot Setup',
|
||||
aiEngine: 'AI Engine',
|
||||
done: 'Done',
|
||||
},
|
||||
platform: {
|
||||
title: 'Select a Platform',
|
||||
description: 'Choose the messaging platform your bot will connect to.',
|
||||
},
|
||||
botConfig: {
|
||||
title: 'Configure Your Bot',
|
||||
description: 'Set up your bot and verify it works before continuing.',
|
||||
saveBot: 'Save & Enable Bot',
|
||||
resaveBot: 'Re-save Configuration',
|
||||
botSaved:
|
||||
'Bot configuration saved and enabled. Check the logs to verify the connection.',
|
||||
logsTitle: 'Bot Logs',
|
||||
logsDescription:
|
||||
'Monitor bot activity to verify the platform connection is working.',
|
||||
},
|
||||
aiEngine: {
|
||||
title: 'Select an AI Engine',
|
||||
description:
|
||||
"Choose the AI engine that will power your bot's intelligence.",
|
||||
},
|
||||
spaceBanner: {
|
||||
message:
|
||||
'Connect to LangBot Space for free trial model credits and zero-config instant setup!',
|
||||
action: 'Authorize with Space',
|
||||
},
|
||||
config: {
|
||||
botInfo: 'Bot Information',
|
||||
botNamePlaceholder: 'Enter bot name',
|
||||
botDescPlaceholder: 'Enter bot description (optional)',
|
||||
platformConfig: '{{platform}} Configuration',
|
||||
aiConfig: '{{engine}} Configuration',
|
||||
},
|
||||
done: {
|
||||
title: 'All Set!',
|
||||
description:
|
||||
'Your bot has been created and connected to the AI pipeline. You can now manage it from the workbench.',
|
||||
backToWorkbench: 'Back to Workbench',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default enUS;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
installedPlugins: 'インストール済みプラグイン',
|
||||
pluginMarket: 'プラグインマーケット',
|
||||
mcpServers: 'MCPサーバー',
|
||||
quickStart: 'クイックスタート',
|
||||
},
|
||||
common: {
|
||||
login: 'ログイン',
|
||||
@@ -322,6 +323,7 @@
|
||||
allLevels: 'すべてのレベル',
|
||||
selectLevel: 'レベルを選択',
|
||||
levelsSelected: 'レベル選択済み',
|
||||
noLogs: 'ログはまだありません',
|
||||
sessionMonitor: {
|
||||
title: 'セッション監視',
|
||||
sessions: 'セッション一覧',
|
||||
@@ -1083,6 +1085,70 @@
|
||||
maxExtensionsReached:
|
||||
'拡張機能数が上限({{max}}個)に達しました。新しい MCP サーバーやプラグインを追加するには、既存のものを削除してください。',
|
||||
},
|
||||
wizard: {
|
||||
sidebarDescription: 'ガイド付きステップでボットを作成',
|
||||
loading: 'ウィザードを読み込み中...',
|
||||
loadError: 'ウィザードデータの読み込みに失敗しました',
|
||||
skip: 'スキップ',
|
||||
skipConfirmMessage:
|
||||
'サイドバーからクイックスタートウィザードに再度アクセスするか、手動でボットを作成できます。',
|
||||
skipConfirmOk: 'OK',
|
||||
prev: '前へ',
|
||||
next: '次へ',
|
||||
finish: '作成&デプロイ',
|
||||
confirmCreateBot: '確定、ボットを作成',
|
||||
createSuccess: 'パイプラインが作成され、ボットにリンクされました!',
|
||||
botCreateSuccess: 'ボットが正常に作成されました!',
|
||||
botSaveSuccess: 'ボット設定が保存され、有効になりました!',
|
||||
createError: 'リソースの作成に失敗しました',
|
||||
spaceAuthError: 'Space 認証の開始に失敗しました',
|
||||
step: {
|
||||
platform: 'プラットフォーム',
|
||||
botConfig: 'ボット設定',
|
||||
aiEngine: 'AIエンジン',
|
||||
done: '完了',
|
||||
},
|
||||
platform: {
|
||||
title: 'プラットフォームを選択',
|
||||
description:
|
||||
'ボットが接続するメッセージングプラットフォームを選択してください。',
|
||||
},
|
||||
botConfig: {
|
||||
title: 'ボットを設定',
|
||||
description:
|
||||
'ボットをセットアップし、正常に動作することを確認してから続行してください。',
|
||||
saveBot: '保存して有効化',
|
||||
resaveBot: '設定を再保存',
|
||||
botSaved:
|
||||
'ボット設定が保存され、有効になりました。ログを確認して接続を検証してください。',
|
||||
logsTitle: 'ボットログ',
|
||||
logsDescription:
|
||||
'ボットの活動を監視して、プラットフォーム接続が正常に動作していることを確認します。',
|
||||
},
|
||||
aiEngine: {
|
||||
title: 'AIエンジンを選択',
|
||||
description:
|
||||
'ボットのインテリジェンスを駆動するAIエンジンを選択してください。',
|
||||
},
|
||||
spaceBanner: {
|
||||
message:
|
||||
'LangBot Spaceに接続して、無料トライアルモデルクレジットとゼロ設定の即時セットアップを入手!',
|
||||
action: 'Spaceで認証',
|
||||
},
|
||||
config: {
|
||||
botInfo: 'ボット情報',
|
||||
botNamePlaceholder: 'ボット名を入力',
|
||||
botDescPlaceholder: 'ボットの説明を入力(任意)',
|
||||
platformConfig: '{{platform}} 設定',
|
||||
aiConfig: '{{engine}} 設定',
|
||||
},
|
||||
done: {
|
||||
title: '完了しました!',
|
||||
description:
|
||||
'ボットが作成され、AIパイプラインに接続されました。ワークベンチから管理できます。',
|
||||
backToWorkbench: 'ワークベンチに戻る',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default jaJP;
|
||||
|
||||
@@ -5,6 +5,7 @@ const zhHans = {
|
||||
installedPlugins: '已安装插件',
|
||||
pluginMarket: '插件市场',
|
||||
mcpServers: 'MCP 服务器',
|
||||
quickStart: '快速开始',
|
||||
},
|
||||
common: {
|
||||
login: '登录',
|
||||
@@ -306,6 +307,7 @@ const zhHans = {
|
||||
viewDetails: '详情',
|
||||
collapse: '收起',
|
||||
imagesAttached: '张图片',
|
||||
noLogs: '暂无日志',
|
||||
sessionMonitor: {
|
||||
title: '会话监控',
|
||||
sessions: '会话列表',
|
||||
@@ -1056,6 +1058,64 @@ const zhHans = {
|
||||
maxExtensionsReached:
|
||||
'已达到扩展数量上限({{max}}个)。请先删除已有的 MCP 服务器或插件后再添加新的。',
|
||||
},
|
||||
wizard: {
|
||||
sidebarDescription: '通过引导步骤创建机器人',
|
||||
loading: '正在加载向导...',
|
||||
loadError: '加载向导数据失败',
|
||||
skip: '跳过',
|
||||
skipConfirmMessage:
|
||||
'您之后可以在侧边栏重新进入快速开始向导,或手动创建机器人。',
|
||||
skipConfirmOk: '确定',
|
||||
prev: '上一步',
|
||||
next: '下一步',
|
||||
finish: '创建并部署',
|
||||
confirmCreateBot: '确定,创建机器人',
|
||||
createSuccess: '流水线已创建并关联到机器人!',
|
||||
botCreateSuccess: '机器人创建成功!',
|
||||
botSaveSuccess: '机器人配置已保存并启用!',
|
||||
createError: '创建资源失败',
|
||||
spaceAuthError: '无法发起 Space 授权',
|
||||
step: {
|
||||
platform: '平台接入',
|
||||
botConfig: '机器人配置',
|
||||
aiEngine: 'AI 引擎',
|
||||
done: '完成',
|
||||
},
|
||||
platform: {
|
||||
title: '选择平台',
|
||||
description: '选择机器人要接入的消息平台。',
|
||||
},
|
||||
botConfig: {
|
||||
title: '配置机器人',
|
||||
description: '配置好机器人并确认其正常运行后再继续。',
|
||||
saveBot: '保存并启用',
|
||||
resaveBot: '重新保存配置',
|
||||
botSaved: '机器人配置已保存并启用,请查看日志确认连接正常。',
|
||||
logsTitle: '机器人日志',
|
||||
logsDescription: '监控机器人活动,确认平台连接是否正常工作。',
|
||||
},
|
||||
aiEngine: {
|
||||
title: '选择 AI 引擎',
|
||||
description: '选择驱动机器人智能的 AI 引擎。',
|
||||
},
|
||||
spaceBanner: {
|
||||
message: '接入 LangBot Space,获取免费试用模型额度,零配置极速开箱!',
|
||||
action: '前往授权登录',
|
||||
},
|
||||
config: {
|
||||
botInfo: '机器人信息',
|
||||
botNamePlaceholder: '请输入机器人名称',
|
||||
botDescPlaceholder: '请输入机器人描述(可选)',
|
||||
platformConfig: '{{platform}} 配置',
|
||||
aiConfig: '{{engine}} 配置',
|
||||
},
|
||||
done: {
|
||||
title: '一切就绪!',
|
||||
description:
|
||||
'机器人已创建并连接到 AI 流水线。你现在可以在工作台中管理它。',
|
||||
backToWorkbench: '返回工作台',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default zhHans;
|
||||
|
||||
@@ -5,6 +5,7 @@ const zhHant = {
|
||||
installedPlugins: '已安裝外掛',
|
||||
pluginMarket: '外掛市場',
|
||||
mcpServers: 'MCP 伺服器',
|
||||
quickStart: '快速開始',
|
||||
},
|
||||
common: {
|
||||
login: '登入',
|
||||
@@ -301,6 +302,7 @@ const zhHant = {
|
||||
allLevels: '全部級別',
|
||||
selectLevel: '選擇級別',
|
||||
levelsSelected: '個級別已選',
|
||||
noLogs: '暫無日誌',
|
||||
sessionMonitor: {
|
||||
title: '會話監控',
|
||||
sessions: '會話列表',
|
||||
@@ -1023,6 +1025,64 @@ const zhHant = {
|
||||
maxExtensionsReached:
|
||||
'已達到擴充功能數量上限({{max}}個)。請先刪除已有的 MCP 伺服器或外掛後再新增。',
|
||||
},
|
||||
wizard: {
|
||||
sidebarDescription: '透過引導步驟建立機器人',
|
||||
loading: '正在載入嚮導...',
|
||||
loadError: '載入嚮導資料失敗',
|
||||
skip: '跳過',
|
||||
skipConfirmMessage:
|
||||
'您之後可以在側邊欄重新進入快速開始嚮導,或手動建立機器人。',
|
||||
skipConfirmOk: '確定',
|
||||
prev: '上一步',
|
||||
next: '下一步',
|
||||
finish: '建立並部署',
|
||||
confirmCreateBot: '確定,建立機器人',
|
||||
createSuccess: '流水線已建立並關聯到機器人!',
|
||||
botCreateSuccess: '機器人建立成功!',
|
||||
botSaveSuccess: '機器人配置已儲存並啟用!',
|
||||
createError: '建立資源失敗',
|
||||
spaceAuthError: '無法發起 Space 授權',
|
||||
step: {
|
||||
platform: '平台接入',
|
||||
botConfig: '機器人配置',
|
||||
aiEngine: 'AI 引擎',
|
||||
done: '完成',
|
||||
},
|
||||
platform: {
|
||||
title: '選擇平台',
|
||||
description: '選擇機器人要接入的訊息平台。',
|
||||
},
|
||||
botConfig: {
|
||||
title: '配置機器人',
|
||||
description: '配置好機器人並確認其正常運作後再繼續。',
|
||||
saveBot: '儲存並啟用',
|
||||
resaveBot: '重新儲存配置',
|
||||
botSaved: '機器人配置已儲存並啟用,請查看日誌確認連接正常。',
|
||||
logsTitle: '機器人日誌',
|
||||
logsDescription: '監控機器人活動,確認平台連接是否正常運作。',
|
||||
},
|
||||
aiEngine: {
|
||||
title: '選擇 AI 引擎',
|
||||
description: '選擇驅動機器人智慧的 AI 引擎。',
|
||||
},
|
||||
spaceBanner: {
|
||||
message: '接入 LangBot Space,取得免費試用模型額度,零配置極速開箱!',
|
||||
action: '前往授權登入',
|
||||
},
|
||||
config: {
|
||||
botInfo: '機器人資訊',
|
||||
botNamePlaceholder: '請輸入機器人名稱',
|
||||
botDescPlaceholder: '請輸入機器人描述(可選)',
|
||||
platformConfig: '{{platform}} 配置',
|
||||
aiConfig: '{{engine}} 配置',
|
||||
},
|
||||
done: {
|
||||
title: '一切就緒!',
|
||||
description:
|
||||
'機器人已建立並連接到 AI 流水線。你現在可以在工作台中管理它。',
|
||||
backToWorkbench: '返回工作台',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default zhHant;
|
||||
|
||||
Reference in New Issue
Block a user