From 4c904c2375680539f3e30914b2996cf38764e6e6 Mon Sep 17 00:00:00 2001
From: Junyan Chin
Date: Sat, 28 Mar 2026 15:50:32 +0800
Subject: [PATCH] Fix/frontend optimizations (#2088)
* fix(web): auto-redirect to wizard on first visit and change sidebar icons to blue
* refactor(wizard): use backend metadata table instead of localStorage for wizard completion state
- Add wizard_completed field to system info API (read from metadata table)
- Add POST /api/v1/system/wizard/completed endpoint to mark wizard done
- Frontend home layout checks systemInfo.wizard_completed for auto-redirect
- Wizard calls markWizardCompleted API on skip/finish
- Ensures consistent behavior across all browsers on the same instance
* fix(wizard): update systemInfo in memory before navigation to prevent redirect loop
* fix(monitoring): prevent horizontal overflow and unify empty state styles
* fix(wizard): use Object.assign for systemInfo and await wizard completion API
- Replace systemInfo reassignment with Object.assign in all 3 locations
to preserve object identity across module imports
- Await markWizardCompleted() POST in wizard skip/finish handlers
instead of fire-and-forget to ensure backend persistence
- Always re-fetch systemInfo in home layout to get latest
wizard_completed state from backend
* fix(wizard): prevent redirect loop by blocking navigation on failed status save
- Refactor wizard_completed (boolean) to wizard_status (string: none/skipped/completed)
- Remove ALL localStorage usage from wizard page (form state persistence)
- Replace AlertDialogAction with Button so skip dialog stays open during POST
- Add loading spinners for skip and complete actions
- If POST fails, show error toast and keep dialog/button active for retry
- If POST succeeds, update in-memory state and navigate
* fix(wizard): fix row[0].value bug causing GET /info to always return wizard_status=none
conn.execute(select(Entity)) returns Row with raw column values, not ORM
entities. row[0] is the key column (a string), so row[0].value raises
AttributeError which was silently swallowed by except-pass, making the
GET endpoint always return wizard_status=none regardless of DB state.
* fix(wizard): replace AlertDialog with Dialog for skip confirmation to remove slide animation
* chore: optimize toast in wizard
* fix(wizard): set default token value for Telegram adapter and initialize adapter config in wizard
* feat(web): move webhook URL to dynamic form system, add market category filter, fix layout overflow
- Add 'webhook-url' dynamic form field type rendered as read-only input
with copy button, defined in adapter YAML specs instead of hardcoded
in BotForm. Supports show_if conditions for optional-webhook adapters.
- Remove hardcoded webhook display logic from BotForm.tsx, pass webhook
URLs via systemContext to DynamicFormComponent.
- Fetch webhook URLs after bot creation in wizard and pass to Step 1.
- Support ?category= query param on /home/market page for filtering by
component type (mirrors langbot-space behavior).
- Link 'install knowledge engine' hint to /home/market?category=KnowledgeEngine.
- Fix SidebarInset missing min-w-0 causing content overflow when sidebar
is expanded.
- Add vertical divider between plugin detail config and readme panels.
- Fix infinite re-render loop in DynamicFormComponent by memoizing
editableItems array.
* fix: lint
* fix(web): change systemInfo to const to satisfy prefer-const lint rule
* fix: update adapter descriptions for clarity and usage requirements
---
.../pkg/api/http/controller/groups/system.py | 44 ++++
.../pkg/platform/sources/aiocqhttp.yaml | 4 +-
src/langbot/pkg/platform/sources/discord.yaml | 3 +-
src/langbot/pkg/platform/sources/lark.yaml | 23 +-
src/langbot/pkg/platform/sources/line.yaml | 22 +-
.../pkg/platform/sources/officialaccount.yaml | 12 +-
.../pkg/platform/sources/openclaw_weixin.yaml | 6 +-
.../pkg/platform/sources/qqofficial.yaml | 12 +-
src/langbot/pkg/platform/sources/satori.yaml | 2 +-
src/langbot/pkg/platform/sources/slack.yaml | 14 +-
.../pkg/platform/sources/telegram.yaml | 4 +-
.../pkg/platform/sources/wechatpad.yaml | 4 +-
src/langbot/pkg/platform/sources/wecom.yaml | 12 +-
.../pkg/platform/sources/wecombot.yaml | 16 +-
src/langbot/pkg/platform/sources/wecomcs.yaml | 12 +-
.../home/bots/components/bot-form/BotForm.tsx | 134 +---------
.../dynamic-form/DynamicFormComponent.tsx | 132 +++++++++-
.../components/home-sidebar/HomeSidebar.tsx | 3 +-
.../home-sidebar/sidbarConfigList.tsx | 8 +
.../knowledge/components/kb-form/KBForm.tsx | 2 +-
web/src/app/home/layout.tsx | 31 ++-
.../overview-cards/TrafficChart.tsx | 19 +-
web/src/app/home/monitoring/page.tsx | 62 ++---
.../app/home/plugins/PluginDetailContent.tsx | 2 +
.../plugin-market/PluginMarketComponent.tsx | 30 ++-
web/src/app/infra/entities/api/index.ts | 1 +
web/src/app/infra/entities/form/dynamic.ts | 1 +
web/src/app/infra/http/BackendClient.ts | 4 +
web/src/app/infra/http/index.ts | 9 +-
web/src/app/wizard/page.tsx | 239 +++++++++---------
web/src/components/ui/sidebar.tsx | 2 +-
web/src/i18n/locales/en-US.ts | 2 +
web/src/i18n/locales/ja-JP.ts | 2 +
web/src/i18n/locales/zh-Hans.ts | 2 +
web/src/i18n/locales/zh-Hant.ts | 2 +
web/src/styles/github-markdown.css | 10 +-
36 files changed, 543 insertions(+), 344 deletions(-)
diff --git a/src/langbot/pkg/api/http/controller/groups/system.py b/src/langbot/pkg/api/http/controller/groups/system.py
index 4e606dd5..280985aa 100644
--- a/src/langbot/pkg/api/http/controller/groups/system.py
+++ b/src/langbot/pkg/api/http/controller/groups/system.py
@@ -1,7 +1,9 @@
import quart
+import sqlalchemy
from .. import group
from .....utils import constants
+from .....entity.persistence.metadata import Metadata
@group.group_class('system', '/api/v1/system')
@@ -9,6 +11,19 @@ class SystemRouterGroup(group.RouterGroup):
async def initialize(self) -> None:
@self.route('/info', methods=['GET'], auth_type=group.AuthType.NONE)
async def _() -> str:
+ # Read wizard_status from metadata table
+ # Possible values: 'skipped', 'completed'; absent key means 'none'
+ wizard_status = 'none'
+ try:
+ result = await self.ap.persistence_mgr.execute_async(
+ sqlalchemy.select(Metadata).where(Metadata.key == 'wizard_status')
+ )
+ row = result.first()
+ if row:
+ wizard_status = row.value
+ except Exception:
+ pass
+
return self.success(
data={
'version': constants.semantic_version,
@@ -27,9 +42,38 @@ class SystemRouterGroup(group.RouterGroup):
'disable_models_service', False
),
'limitation': self.ap.instance_config.data.get('system', {}).get('limitation', {}),
+ 'wizard_status': wizard_status,
}
)
+ @self.route('/wizard/completed', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
+ async def _() -> str:
+ """Mark wizard status in metadata table.
+
+ Accepts JSON body: { "status": "skipped" | "completed" }
+ """
+ data = await quart.request.get_json(silent=True) or {}
+ status = data.get('status', 'completed')
+ if status not in ('skipped', 'completed'):
+ return self.http_status(400, 400, f'Invalid wizard status: {status}')
+
+ try:
+ result = await self.ap.persistence_mgr.execute_async(
+ sqlalchemy.select(Metadata).where(Metadata.key == 'wizard_status')
+ )
+ if result.first():
+ await self.ap.persistence_mgr.execute_async(
+ sqlalchemy.update(Metadata).where(Metadata.key == 'wizard_status').values(value=status)
+ )
+ else:
+ await self.ap.persistence_mgr.execute_async(
+ sqlalchemy.insert(Metadata).values(key='wizard_status', value=status)
+ )
+ except Exception as e:
+ return self.http_status(500, 500, f'Failed to update wizard status: {e}')
+
+ return self.success(data={})
+
@self.route('/tasks', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
async def _() -> str:
task_type = quart.request.args.get('type')
diff --git a/src/langbot/pkg/platform/sources/aiocqhttp.yaml b/src/langbot/pkg/platform/sources/aiocqhttp.yaml
index a3ced8e2..7c530c19 100644
--- a/src/langbot/pkg/platform/sources/aiocqhttp.yaml
+++ b/src/langbot/pkg/platform/sources/aiocqhttp.yaml
@@ -6,8 +6,8 @@ metadata:
en_US: OneBot v11
zh_Hans: OneBot v11
description:
- en_US: OneBot v11 Adapter
- zh_Hans: OneBot v11 适配器,请查看文档了解使用方式
+ en_US: OneBot v11 Adapter, used for QQ bots
+ zh_Hans: OneBot v11 适配器,用于接入 QQ 机器人协议端,请查看文档了解使用方式
icon: onebot.png
spec:
config:
diff --git a/src/langbot/pkg/platform/sources/discord.yaml b/src/langbot/pkg/platform/sources/discord.yaml
index f000c2d9..57392994 100644
--- a/src/langbot/pkg/platform/sources/discord.yaml
+++ b/src/langbot/pkg/platform/sources/discord.yaml
@@ -7,7 +7,8 @@ metadata:
zh_Hans: Discord
description:
en_US: Discord Adapter
- zh_Hans: Discord 适配器,请查看文档了解使用方式
+ zh_Hans: Discord 适配器,需要可连接 Discord 服务器的网络环境
+ ja_JP: Discord アダプター
icon: discord.svg
spec:
config:
diff --git a/src/langbot/pkg/platform/sources/lark.yaml b/src/langbot/pkg/platform/sources/lark.yaml
index 7db5cdaa..2b84d415 100644
--- a/src/langbot/pkg/platform/sources/lark.yaml
+++ b/src/langbot/pkg/platform/sources/lark.yaml
@@ -6,8 +6,9 @@ metadata:
en_US: Lark
zh_Hans: 飞书
description:
- en_US: Lark Adapter
- zh_Hans: 飞书适配器,请查看文档了解使用方式
+ en_US: Lark Adapter, supports both long connection and Webhook modes. Please refer to the documentation for usage details.
+ zh_Hans: 飞书适配器,支持长连接和 Webhook 两种接入方式,请查看文档了解使用方式
+ ja_JP: Lark アダプター、長期接続およびWebhookモードの両方をサポートしています。使用方法の詳細については、ドキュメントを参照してください。
icon: lark.svg
spec:
config:
@@ -45,6 +46,20 @@ spec:
type: boolean
required: true
default: false
+ - name: webhook_url
+ label:
+ en_US: Webhook Callback URL
+ zh_Hans: Webhook 回调地址
+ description:
+ en_US: Copy this URL and paste it into your Lark app's webhook configuration
+ zh_Hans: 复制此地址并粘贴到飞书应用的 Webhook 配置中
+ type: webhook-url
+ required: false
+ default: ""
+ show_if:
+ field: enable-webhook
+ operator: eq
+ value: true
- name: encrypt-key
label:
en_US: Encrypt Key
@@ -55,6 +70,10 @@ spec:
type: string
required: true
default: ""
+ show_if:
+ field: enable-webhook
+ operator: eq
+ value: true
- name: enable-stream-reply
label:
en_US: Enable Stream Reply Mode
diff --git a/src/langbot/pkg/platform/sources/line.yaml b/src/langbot/pkg/platform/sources/line.yaml
index 5b399337..1944cc81 100644
--- a/src/langbot/pkg/platform/sources/line.yaml
+++ b/src/langbot/pkg/platform/sources/line.yaml
@@ -6,13 +6,27 @@ metadata:
en_US: LINE
zh_Hans: LINE
description:
- en_US: LINE Adapter
- zh_Hans: LINE适配器,请查看文档了解使用方式
- ja_JP: LINEアダプター、ドキュメントを参照してください
- zh_Hant: LINE適配器,請查看文檔了解使用方式
+ en_US: LINE Adapter, requires a public URL to receive LINE message pushes, please refer to the documentation for usage details
+ zh_Hans: LINE适配器,需要公网地址以接收 LINE 消息推送,请查看文档了解使用方式
+ ja_JP: LINEアダプター、LINEのメッセージプッシュを受信するためにパブリックURLが必要です。使用方法の詳細については、ドキュメントを参照してください。
+ zh_Hant: LINE適配器,需要公网地址以接收 LINE 消息推送,请查看文档了解使用方式
icon: line.png
spec:
config:
+ - name: webhook_url
+ label:
+ en_US: Webhook Callback URL
+ zh_Hans: Webhook 回调地址
+ ja_JP: Webhook コールバック URL
+ zh_Hant: Webhook 回調地址
+ description:
+ en_US: Copy this URL and paste it into your LINE channel's webhook configuration
+ zh_Hans: 复制此地址并粘贴到 LINE 频道的 Webhook 配置中
+ ja_JP: この URL をコピーして LINE チャンネルの Webhook 設定に貼り付けてください
+ zh_Hant: 複製此地址並粘貼到 LINE 頻道的 Webhook 配置中
+ type: webhook-url
+ required: false
+ default: ""
- name: channel_access_token
label:
en_US: Channel access token
diff --git a/src/langbot/pkg/platform/sources/officialaccount.yaml b/src/langbot/pkg/platform/sources/officialaccount.yaml
index fda0a912..c42a7c88 100644
--- a/src/langbot/pkg/platform/sources/officialaccount.yaml
+++ b/src/langbot/pkg/platform/sources/officialaccount.yaml
@@ -7,10 +7,20 @@ metadata:
zh_Hans: 微信公众号
description:
en_US: Official Account Adapter
- zh_Hans: 微信公众号适配器,请查看文档了解使用方式
+ zh_Hans: 微信公众号适配器,需要公网地址以接收消息推送,请查看文档了解使用方式
icon: officialaccount.png
spec:
config:
+ - name: webhook_url
+ label:
+ en_US: Webhook Callback URL
+ zh_Hans: Webhook 回调地址
+ description:
+ en_US: Copy this URL and paste it into your Official Account webhook configuration
+ zh_Hans: 复制此地址并粘贴到微信公众号的 Webhook 配置中
+ type: webhook-url
+ required: false
+ default: ""
- name: token
label:
en_US: Token
diff --git a/src/langbot/pkg/platform/sources/openclaw_weixin.yaml b/src/langbot/pkg/platform/sources/openclaw_weixin.yaml
index c5400a59..c203d760 100644
--- a/src/langbot/pkg/platform/sources/openclaw_weixin.yaml
+++ b/src/langbot/pkg/platform/sources/openclaw_weixin.yaml
@@ -4,10 +4,10 @@ metadata:
name: openclaw-weixin
label:
en_US: OpenClaw WeChat
- zh_Hans: OpenClaw 微信
+ zh_Hans: 个人微信机器人
description:
en_US: OpenClaw WeChat adapter, supports personal WeChat via QR code login
- zh_Hans: OpenClaw 微信适配器,通过扫码登录支持个人微信
+ zh_Hans: 微信官方个人助手,扫码即可登录使用
icon: wechat.png
spec:
config:
@@ -27,7 +27,7 @@ spec:
zh_Hans: 令牌
description:
en_US: Bearer token obtained after QR code login authorization. Leave empty to trigger QR code login on startup.
- zh_Hans: 扫码登录授权后获取的 Bearer 令牌。留空则启动时自动触发扫码登录。
+ zh_Hans: 扫码登录授权后获取的 Bearer 令牌。请留空并保存,将在启动时输出二维码到日志,扫码后即可自动登录。
type: string
required: false
default: ""
diff --git a/src/langbot/pkg/platform/sources/qqofficial.yaml b/src/langbot/pkg/platform/sources/qqofficial.yaml
index 54d800bb..a374265a 100644
--- a/src/langbot/pkg/platform/sources/qqofficial.yaml
+++ b/src/langbot/pkg/platform/sources/qqofficial.yaml
@@ -7,10 +7,20 @@ metadata:
zh_Hans: QQ 官方 API
description:
en_US: QQ Official API (Webhook)
- zh_Hans: QQ 官方 API (Webhook),请查看文档了解使用方式
+ zh_Hans: QQ 官方 API (Webhook),需要公网地址以接收消息推送,请查看文档了解使用方式
icon: qqofficial.svg
spec:
config:
+ - name: webhook_url
+ label:
+ en_US: Webhook Callback URL
+ zh_Hans: Webhook 回调地址
+ description:
+ en_US: Copy this URL and paste it into your QQ Official API webhook configuration
+ zh_Hans: 复制此地址并粘贴到 QQ 官方 API 的 Webhook 配置中
+ type: webhook-url
+ required: false
+ default: ""
- name: appid
label:
en_US: App ID
diff --git a/src/langbot/pkg/platform/sources/satori.yaml b/src/langbot/pkg/platform/sources/satori.yaml
index e473aa9d..e1635286 100644
--- a/src/langbot/pkg/platform/sources/satori.yaml
+++ b/src/langbot/pkg/platform/sources/satori.yaml
@@ -7,7 +7,7 @@ metadata:
zh_Hans: Satori
description:
en_US: SatoriAdapter
- zh_Hans: 古明地觉协议适配器
+ zh_Hans: Satori 协议适配器,支持多种平台的接入,请查看文档了解使用方式
icon: satori.png
spec:
config:
diff --git a/src/langbot/pkg/platform/sources/slack.yaml b/src/langbot/pkg/platform/sources/slack.yaml
index 13d303bb..2f3a438b 100644
--- a/src/langbot/pkg/platform/sources/slack.yaml
+++ b/src/langbot/pkg/platform/sources/slack.yaml
@@ -7,10 +7,22 @@ metadata:
zh_Hans: Slack
description:
en_US: Slack Adapter
- zh_Hans: Slack 适配器,请查看文档了解使用方式
+ zh_Hans: Slack 适配器,需要公网地址以接收 Slack 消息推送,请查看文档了解使用方式
+ ja_JP: Slack アダプター、Slackのメッセージプッシュを受信するためにパブリックURLが必要です。使用方法の詳細については、ドキュメントを参照してください。
+ zh_Hant: Slack 适配器,需要公网地址以接收 Slack 消息推送,请查看文档了解使用方式
icon: slack.png
spec:
config:
+ - name: webhook_url
+ label:
+ en_US: Webhook Callback URL
+ zh_Hans: Webhook 回调地址
+ description:
+ en_US: Copy this URL and paste it into your Slack app's event subscription configuration
+ zh_Hans: 复制此地址并粘贴到 Slack 应用的事件订阅配置中
+ type: webhook-url
+ required: false
+ default: ""
- name: bot_token
label:
en_US: Bot Token
diff --git a/src/langbot/pkg/platform/sources/telegram.yaml b/src/langbot/pkg/platform/sources/telegram.yaml
index d29c359e..14c84289 100644
--- a/src/langbot/pkg/platform/sources/telegram.yaml
+++ b/src/langbot/pkg/platform/sources/telegram.yaml
@@ -7,7 +7,7 @@ metadata:
zh_Hans: 电报
description:
en_US: Telegram Adapter
- zh_Hans: 电报适配器,请查看文档了解使用方式
+ zh_Hans: Telegram 适配器,请查看文档了解使用方式
icon: telegram.svg
spec:
config:
@@ -17,7 +17,7 @@ spec:
zh_Hans: 令牌
type: string
required: true
- default: ""
+ default: "token_from_botfather"
- name: markdown_card
label:
en_US: Markdown Card
diff --git a/src/langbot/pkg/platform/sources/wechatpad.yaml b/src/langbot/pkg/platform/sources/wechatpad.yaml
index b936dcae..f1e1b674 100644
--- a/src/langbot/pkg/platform/sources/wechatpad.yaml
+++ b/src/langbot/pkg/platform/sources/wechatpad.yaml
@@ -4,10 +4,10 @@ metadata:
name: wechatpad
label:
en_US: WeChatPad
- zh_CN: WeChatPad(个人微信ipad)
+ zh_Hans: WeChatPad(个人微信ipad)
description:
en_US: WeChatPad Adapter
- zh_CN: WeChatPad 适配器
+ zh_Hans: WeChatPad 适配器,基于WeChatPad的个人微信解决方案,请查看文档了解使用方式
icon: wechatpad.png
spec:
config:
diff --git a/src/langbot/pkg/platform/sources/wecom.yaml b/src/langbot/pkg/platform/sources/wecom.yaml
index c732f699..ecbb51ba 100644
--- a/src/langbot/pkg/platform/sources/wecom.yaml
+++ b/src/langbot/pkg/platform/sources/wecom.yaml
@@ -7,10 +7,20 @@ metadata:
zh_Hans: 企业微信
description:
en_US: WeCom Adapter
- zh_Hans: 企业微信适配器,请查看文档了解使用方式
+ zh_Hans: 企业微信内部机器人,请查看文档了解使用方式
icon: wecom.png
spec:
config:
+ - name: webhook_url
+ label:
+ en_US: Webhook Callback URL
+ zh_Hans: Webhook 回调地址
+ description:
+ en_US: Copy this URL and paste it into your WeCom app's webhook configuration
+ zh_Hans: 复制此地址并粘贴到企业微信应用的 Webhook 配置中
+ type: webhook-url
+ required: false
+ default: ""
- name: corpid
label:
en_US: Corpid
diff --git a/src/langbot/pkg/platform/sources/wecombot.yaml b/src/langbot/pkg/platform/sources/wecombot.yaml
index bf851a44..7b5c34ba 100644
--- a/src/langbot/pkg/platform/sources/wecombot.yaml
+++ b/src/langbot/pkg/platform/sources/wecombot.yaml
@@ -7,7 +7,7 @@ metadata:
zh_Hans: 企业微信智能机器人
description:
en_US: WeComBot Adapter
- zh_Hans: 企业微信智能机器人适配器,请查看文档了解使用方式
+ zh_Hans: 企业微信智能机器人,支持长连接和 Webhook 两种接入方式,请查看文档了解使用方式
icon: wecombot.png
spec:
config:
@@ -35,6 +35,20 @@ spec:
type: boolean
required: true
default: false
+ - name: webhook_url
+ label:
+ en_US: Webhook Callback URL
+ zh_Hans: Webhook 回调地址
+ description:
+ en_US: Copy this URL and paste it into your WeComBot webhook configuration
+ zh_Hans: 复制此地址并粘贴到企业微信智能机器人的 Webhook 配置中
+ type: webhook-url
+ required: false
+ default: ""
+ show_if:
+ field: enable-webhook
+ operator: eq
+ value: true
- name: Secret
label:
en_US: Secret
diff --git a/src/langbot/pkg/platform/sources/wecomcs.yaml b/src/langbot/pkg/platform/sources/wecomcs.yaml
index a1be068e..f28cad75 100644
--- a/src/langbot/pkg/platform/sources/wecomcs.yaml
+++ b/src/langbot/pkg/platform/sources/wecomcs.yaml
@@ -7,10 +7,20 @@ metadata:
zh_Hans: 企业微信客服
description:
en_US: WeComCSAdapter
- zh_Hans: 企业微信客服适配器
+ zh_Hans: 企业微信对外客服机器人,需要公网地址以接收消息推送,请查看文档了解使用方式
icon: wecom.png
spec:
config:
+ - name: webhook_url
+ label:
+ en_US: Webhook Callback URL
+ zh_Hans: Webhook 回调地址
+ description:
+ en_US: Copy this URL and paste it into your WeCom Customer Service webhook configuration
+ zh_Hans: 复制此地址并粘贴到企业微信客服的 Webhook 配置中
+ type: webhook-url
+ required: false
+ default: ""
- name: corpid
label:
en_US: Corpid
diff --git a/web/src/app/home/bots/components/bot-form/BotForm.tsx b/web/src/app/home/bots/components/bot-form/BotForm.tsx
index 7048aab7..8fdcd9bb 100644
--- a/web/src/app/home/bots/components/bot-form/BotForm.tsx
+++ b/web/src/app/home/bots/components/bot-form/BotForm.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useMemo, useRef, useState } from 'react';
+import React, { useEffect, useRef, useState } from 'react';
import {
IChooseAdapterEntity,
IPipelineEntity,
@@ -19,9 +19,7 @@ import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
-import { Copy, Check } from 'lucide-react';
-import { Button } from '@/components/ui/button';
import {
Form,
FormControl,
@@ -110,30 +108,11 @@ export default function BotForm({
const [, setIsLoading] = useState(false);
const [webhookUrl, setWebhookUrl] = useState('');
const [extraWebhookUrl, setExtraWebhookUrl] = useState('');
- const [copied, setCopied] = useState(false);
- const [extraCopied, setExtraCopied] = useState(false);
// Watch adapter and adapter_config for filtering
const currentAdapter = form.watch('adapter');
const currentAdapterConfig = form.watch('adapter_config');
- // Derive the filtered config list via useMemo instead of useEffect+setState
- // to avoid creating new array references that would cause DynamicFormComponent
- // to re-subscribe its form.watch, re-emit values, and trigger an infinite loop.
- // Only depend on the specific field we care about (enable-webhook) rather than
- // the entire currentAdapterConfig object, which changes on every emission.
- const enableWebhook = currentAdapterConfig?.['enable-webhook'];
- const filteredDynamicFormConfigList = useMemo(() => {
- if (currentAdapter === 'lark' && enableWebhook === false) {
- // Hide encrypt-key field when webhook is disabled
- return dynamicFormConfigList.filter(
- (config) => config.name !== 'encrypt-key',
- );
- }
- // For non-Lark adapters or when webhook is enabled/undefined, show all fields
- return dynamicFormConfigList;
- }, [currentAdapter, enableWebhook, dynamicFormConfigList]);
-
// Notify parent when dirty state changes
const { isDirty } = form.formState;
useEffect(() => {
@@ -144,43 +123,6 @@ export default function BotForm({
setBotFormValues();
}, []);
- const copyToClipboard = (
- text: string,
- setStatus: React.Dispatch>,
- ) => {
- if (navigator.clipboard && navigator.clipboard.writeText) {
- navigator.clipboard
- .writeText(text)
- .then(() => {
- setStatus(true);
- setTimeout(() => setStatus(false), 2000);
- })
- .catch(() => {
- fallbackCopy(text, setStatus);
- });
- } else {
- fallbackCopy(text, setStatus);
- }
- };
-
- const fallbackCopy = (
- text: string,
- setStatus: React.Dispatch>,
- ) => {
- const textarea = document.createElement('textarea');
- textarea.value = text;
- textarea.style.position = 'fixed';
- textarea.style.opacity = '0';
- document.body.appendChild(textarea);
- textarea.select();
- const successful = document.execCommand('copy');
- document.body.removeChild(textarea);
- if (successful) {
- setStatus(true);
- setTimeout(() => setStatus(false), 2000);
- }
- };
-
function setBotFormValues() {
isInitializing.current = true;
initBotFormComponent().then(() => {
@@ -384,12 +326,6 @@ export default function BotForm({
}
}
- // --- Webhook URL display helper ---
- const showWebhook =
- initBotId &&
- webhookUrl &&
- (currentAdapter !== 'lark' || enableWebhook !== false);
-
return (
- {/* Webhook URL: shown after adapter is selected (edit mode only) */}
- {showWebhook && (
-
- {t('bots.webhookUrl')}
-
- {
- (e.target as HTMLInputElement).select();
- }}
- />
-
-
- {extraWebhookUrl && (
-
- {
- (e.target as HTMLInputElement).select();
- }}
- />
-
-
- )}
-
- {extraWebhookUrl
- ? t('bots.webhookUrlHintEither')
- : t('bots.webhookUrlHint')}
-
-
- )}
-
- {showDynamicForm && filteredDynamicFormConfigList.length > 0 && (
+ {showDynamicForm && dynamicFormConfigList.length > 0 && (
{
form.setValue('adapter_config', values, {
shouldDirty: !isInitializing.current,
});
}}
+ systemContext={{
+ webhook_url: webhookUrl,
+ extra_webhook_url: extraWebhookUrl,
+ }}
/>
)}
diff --git a/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx b/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx
index f9e86815..672b57e6 100644
--- a/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx
+++ b/web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx
@@ -11,10 +11,13 @@ import {
FormMessage,
} from '@/components/ui/form';
import DynamicFormItemComponent from '@/app/home/components/dynamic-form/DynamicFormItemComponent';
-import { useEffect, useRef } from 'react';
+import { useEffect, useMemo, useRef, useState } from 'react';
import { extractI18nObject } from '@/i18n/I18nProvider';
import { useTranslation } from 'react-i18next';
import { cn } from '@/lib/utils';
+import { Input } from '@/components/ui/input';
+import { Button } from '@/components/ui/button';
+import { Copy, Check } from 'lucide-react';
/**
* Resolve the value referenced by a `show_if.field` string.
@@ -40,6 +43,89 @@ function resolveShowIfValue(
return externalDependentValues?.[field];
}
+/**
+ * Display-only component for webhook URL fields.
+ * Rendered outside of react-hook-form binding since the value is
+ * read-only and comes from systemContext, not user input.
+ */
+function WebhookUrlField({
+ label,
+ description,
+ url,
+ extraUrl,
+}: {
+ label: string;
+ description?: string;
+ url: string;
+ extraUrl?: string;
+}) {
+ const [copied, setCopied] = useState(false);
+ const [extraCopied, setExtraCopied] = useState(false);
+
+ const handleCopy = (text: string, setter: (v: boolean) => void) => {
+ if (navigator.clipboard && navigator.clipboard.writeText) {
+ navigator.clipboard
+ .writeText(text)
+ .then(() => {
+ setter(true);
+ setTimeout(() => setter(false), 2000);
+ })
+ .catch(() => {});
+ }
+ };
+
+ return (
+
+ {label}
+
+ (e.target as HTMLInputElement).select()}
+ />
+
+
+ {extraUrl && (
+
+ (e.target as HTMLInputElement).select()}
+ />
+
+
+ )}
+ {description && (
+ {description}
+ )}
+
+ );
+}
+
export default function DynamicFormComponent({
itemConfigList,
onSubmit,
@@ -99,9 +185,16 @@ export default function DynamicFormComponent({
return value;
};
+ // Filter out display-only field types (e.g. webhook-url) that should not
+ // participate in form state, validation, or value emission.
+ const editableItems = useMemo(
+ () => itemConfigList.filter((item) => item.type !== 'webhook-url'),
+ [itemConfigList],
+ );
+
// 根据 itemConfigList 动态生成 zod schema
const formSchema = z.object(
- itemConfigList.reduce(
+ editableItems.reduce(
(acc, item) => {
let fieldSchema;
switch (item.type) {
@@ -179,7 +272,7 @@ export default function DynamicFormComponent({
const form = useForm({
resolver: zodResolver(formSchema),
- defaultValues: itemConfigList.reduce((acc, item) => {
+ defaultValues: editableItems.reduce((acc, item) => {
// 优先使用 initialValues,如果没有则使用默认值
const rawValue = initialValues?.[item.name] ?? item.default;
return {
@@ -207,7 +300,7 @@ export default function DynamicFormComponent({
if (initialValues && hasRealChange) {
// 合并默认值和初始值
- const mergedValues = itemConfigList.reduce(
+ const mergedValues = editableItems.reduce(
(acc, item) => {
const rawValue = initialValues[item.name] ?? item.default;
acc[item.name] = normalizeFieldValue(item, rawValue) as object;
@@ -222,7 +315,7 @@ export default function DynamicFormComponent({
previousInitialValues.current = initialValues;
}
- }, [initialValues, form, itemConfigList]);
+ }, [initialValues, form, editableItems]);
// Get reactive form values for conditional rendering
const watchedValues = form.watch();
@@ -238,7 +331,7 @@ export default function DynamicFormComponent({
// even if the user saves without modifying any field.
// form.watch(callback) only fires on subsequent changes, not on mount.
const formValues = form.getValues();
- const initialFinalValues = itemConfigList.reduce(
+ const initialFinalValues = editableItems.reduce(
(acc, item) => {
acc[item.name] = formValues[item.name] ?? item.default;
return acc;
@@ -258,7 +351,7 @@ export default function DynamicFormComponent({
const subscription = form.watch(() => {
const formValues = form.getValues();
- const finalValues = itemConfigList.reduce(
+ const finalValues = editableItems.reduce(
(acc, item) => {
acc[item.name] = formValues[item.name] ?? item.default;
return acc;
@@ -269,7 +362,7 @@ export default function DynamicFormComponent({
previousInitialValues.current = finalValues as Record;
});
return () => subscription.unsubscribe();
- }, [form, itemConfigList]);
+ }, [form, editableItems]);
return (
{t('knowledge.installEngineHint')}
diff --git a/web/src/app/home/layout.tsx b/web/src/app/home/layout.tsx
index cdcd5373..a9e62c47 100644
--- a/web/src/app/home/layout.tsx
+++ b/web/src/app/home/layout.tsx
@@ -15,8 +15,13 @@ import {
useSidebarData,
} from '@/app/home/components/home-sidebar/SidebarDataContext';
import { I18nObject } from '@/app/infra/entities/common';
-import { userInfo, initializeUserInfo } from '@/app/infra/http';
-import { usePathname } from 'next/navigation';
+import {
+ userInfo,
+ systemInfo,
+ initializeUserInfo,
+ initializeSystemInfo,
+} from '@/app/infra/http';
+import { usePathname, useRouter } from 'next/navigation';
import Link from 'next/link';
import { extractI18nObject } from '@/i18n/I18nProvider';
import { CircleHelp } from 'lucide-react';
@@ -50,6 +55,8 @@ export default function HomeLayout({
}: Readonly<{
children: React.ReactNode;
}>) {
+ const router = useRouter();
+
// Initialize user info if not already initialized
useEffect(() => {
if (!userInfo) {
@@ -57,6 +64,22 @@ export default function HomeLayout({
}
}, []);
+ // Auto-redirect to wizard on first visit (wizard not yet completed on this instance)
+ useEffect(() => {
+ const checkWizard = async () => {
+ try {
+ // Always re-fetch to ensure we have the latest wizard_status from backend
+ await initializeSystemInfo();
+ if (systemInfo.wizard_status === 'none') {
+ router.replace('/wizard');
+ }
+ } catch {
+ // If fetching system info fails, don't redirect
+ }
+ };
+ checkWizard();
+ }, [router]);
+
return (
{children}
@@ -143,7 +166,9 @@ function HomeLayoutInner({ children }: { children: React.ReactNode }) {
- {mainContent}
+
+ {mainContent}
+
diff --git a/web/src/app/home/monitoring/components/overview-cards/TrafficChart.tsx b/web/src/app/home/monitoring/components/overview-cards/TrafficChart.tsx
index b419cb7b..f6b5d6e7 100644
--- a/web/src/app/home/monitoring/components/overview-cards/TrafficChart.tsx
+++ b/web/src/app/home/monitoring/components/overview-cards/TrafficChart.tsx
@@ -147,23 +147,16 @@ export default function TrafficChart({
{t('monitoring.trafficChart.title')}
-
+
-
- {t('monitoring.trafficChart.noData')}
-
+
{t('monitoring.trafficChart.noData')}
);
diff --git a/web/src/app/home/monitoring/page.tsx b/web/src/app/home/monitoring/page.tsx
index b3937aad..80661ea7 100644
--- a/web/src/app/home/monitoring/page.tsx
+++ b/web/src/app/home/monitoring/page.tsx
@@ -188,7 +188,7 @@ function MonitoringPageContent() {
};
return (
-
+
{/* Filters and Refresh Button - Sticky */}
@@ -379,26 +379,18 @@ function MonitoringPageContent() {
{!loading &&
(!data || !data.messages || data.messages.length === 0) && (
-
+
-
+
{t('monitoring.messageList.noMessages')}
-
-
- {t('monitoring.messageList.noMessagesDescription')}
-
+
)}
@@ -600,23 +592,18 @@ function MonitoringPageContent() {
(!data ||
!data.modelCalls ||
data.modelCalls.length === 0) && (
-
+
-
+
{t('monitoring.modelCalls.noData')}
-
+
)}
@@ -775,23 +762,18 @@ function MonitoringPageContent() {
{!loading &&
(!data || !data.errors || data.errors.length === 0) && (
-
+
-
+
{t('monitoring.errors.noErrors')}
-
+
)}
diff --git a/web/src/app/home/plugins/PluginDetailContent.tsx b/web/src/app/home/plugins/PluginDetailContent.tsx
index 285cf331..71a66533 100644
--- a/web/src/app/home/plugins/PluginDetailContent.tsx
+++ b/web/src/app/home/plugins/PluginDetailContent.tsx
@@ -86,6 +86,8 @@ export default function PluginDetailContent({ id }: { id: string }) {
onFormSubmit={handleFormSubmit}
/>
+ {/* Divider */}
+
{/* Right side - Readme */}
diff --git a/web/src/app/home/plugins/components/plugin-market/PluginMarketComponent.tsx b/web/src/app/home/plugins/components/plugin-market/PluginMarketComponent.tsx
index 9f20c8c2..dca07ddc 100644
--- a/web/src/app/home/plugins/components/plugin-market/PluginMarketComponent.tsx
+++ b/web/src/app/home/plugins/components/plugin-market/PluginMarketComponent.tsx
@@ -1,6 +1,7 @@
'use client';
import { useState, useEffect, useCallback, useRef, Suspense } from 'react';
+import { useSearchParams } from 'next/navigation';
import { Input } from '@/components/ui/input';
import {
Select,
@@ -46,9 +47,24 @@ function MarketPageContent({
installPlugin: (plugin: PluginV4) => void;
}) {
const { t } = useTranslation();
+ const searchParams = useSearchParams();
+
+ const validCategories = [
+ 'Tool',
+ 'Command',
+ 'EventListener',
+ 'KnowledgeEngine',
+ 'Parser',
+ ];
const [searchQuery, setSearchQuery] = useState('');
- const [componentFilter, setComponentFilter] = useState
('all');
+ const [componentFilter, setComponentFilter] = useState(() => {
+ const category = searchParams.get('category');
+ if (category && validCategories.includes(category)) {
+ return category;
+ }
+ return 'all';
+ });
const [selectedTags, setSelectedTags] = useState([]);
const [availableTags, setAvailableTags] = useState([]);
const [tagNames, setTagNames] = useState>({});
@@ -284,6 +300,18 @@ function MarketPageContent({
setComponentFilter(value);
setCurrentPage(1);
setPlugins([]);
+
+ // Update URL query param to keep it in sync
+ const params = new URLSearchParams(window.location.search);
+ if (value === 'all') {
+ params.delete('category');
+ } else {
+ params.set('category', value);
+ }
+ const newUrl = params.toString()
+ ? `${window.location.pathname}?${params.toString()}`
+ : window.location.pathname;
+ window.history.replaceState({}, '', newUrl);
// fetchPlugins will be called by useEffect when componentFilter changes
}, []);
diff --git a/web/src/app/infra/entities/api/index.ts b/web/src/app/infra/entities/api/index.ts
index b9af3d93..26881cda 100644
--- a/web/src/app/infra/entities/api/index.ts
+++ b/web/src/app/infra/entities/api/index.ts
@@ -260,6 +260,7 @@ export interface ApiRespSystemInfo {
allow_modify_login_info: boolean;
disable_models_service: boolean;
limitation: SystemLimitation;
+ wizard_status: string; // 'none' | 'skipped' | 'completed'
}
export interface RagMigrationStatusResp {
diff --git a/web/src/app/infra/entities/form/dynamic.ts b/web/src/app/infra/entities/form/dynamic.ts
index 3f57b0f9..f13871eb 100644
--- a/web/src/app/infra/entities/form/dynamic.ts
+++ b/web/src/app/infra/entities/form/dynamic.ts
@@ -42,6 +42,7 @@ export enum DynamicFormItemType {
KNOWLEDGE_BASE_MULTI_SELECTOR = 'knowledge-base-multi-selector',
PLUGIN_SELECTOR = 'plugin-selector',
BOT_SELECTOR = 'bot-selector',
+ WEBHOOK_URL = 'webhook-url',
}
export interface IFileConfig {
diff --git a/web/src/app/infra/http/BackendClient.ts b/web/src/app/infra/http/BackendClient.ts
index d234ddd1..c2e2b247 100644
--- a/web/src/app/infra/http/BackendClient.ts
+++ b/web/src/app/infra/http/BackendClient.ts
@@ -701,6 +701,10 @@ export class BackendClient extends BaseHttpClient {
return this.get('/api/v1/system/info');
}
+ public updateWizardStatus(status: 'skipped' | 'completed'): Promise {
+ return this.post('/api/v1/system/wizard/completed', { status });
+ }
+
public getAsyncTasks(): Promise {
return this.get('/api/v1/system/tasks');
}
diff --git a/web/src/app/infra/http/index.ts b/web/src/app/infra/http/index.ts
index 3ef8761d..1da49ba9 100644
--- a/web/src/app/infra/http/index.ts
+++ b/web/src/app/infra/http/index.ts
@@ -3,7 +3,7 @@ import { CloudServiceClient } from './CloudServiceClient';
import { ApiRespSystemInfo } from '@/app/infra/entities/api';
// 系统信息
-export let systemInfo: ApiRespSystemInfo = {
+export const systemInfo: ApiRespSystemInfo = {
debug: false,
version: '',
edition: 'community',
@@ -16,6 +16,7 @@ export let systemInfo: ApiRespSystemInfo = {
max_pipelines: -1,
max_extensions: -1,
},
+ wizard_status: 'none',
};
// 用户信息
@@ -50,7 +51,7 @@ if (typeof window !== 'undefined' && systemInfo.cloud_service_url === '') {
backendClient
.getSystemInfo()
.then((info) => {
- systemInfo = info;
+ Object.assign(systemInfo, info);
cloudServiceClient.updateBaseURL(info.cloud_service_url);
})
.catch((error) => {
@@ -65,7 +66,7 @@ if (typeof window !== 'undefined' && systemInfo.cloud_service_url === '') {
export const getCloudServiceClient = async (): Promise => {
if (systemInfo.cloud_service_url === '') {
try {
- systemInfo = await backendClient.getSystemInfo();
+ Object.assign(systemInfo, await backendClient.getSystemInfo());
// 更新 cloud service client 的 baseURL
cloudServiceClient.updateBaseURL(systemInfo.cloud_service_url);
} catch (error) {
@@ -90,7 +91,7 @@ export const getCloudServiceClientSync = (): CloudServiceClient => {
*/
export const initializeSystemInfo = async (): Promise => {
try {
- systemInfo = await backendClient.getSystemInfo();
+ Object.assign(systemInfo, await backendClient.getSystemInfo());
cloudServiceClient.updateBaseURL(systemInfo.cloud_service_url);
} catch (error) {
console.error('Failed to initialize system info:', error);
diff --git a/web/src/app/wizard/page.tsx b/web/src/app/wizard/page.tsx
index b9e32d8a..e5a2f907 100644
--- a/web/src/app/wizard/page.tsx
+++ b/web/src/app/wizard/page.tsx
@@ -18,6 +18,7 @@ import {
import { httpClient } from '@/app/infra/http/HttpClient';
import {
userInfo,
+ systemInfo,
initializeUserInfo,
initializeSystemInfo,
} from '@/app/infra/http';
@@ -29,6 +30,7 @@ import {
} from '@/app/infra/entities/pipeline';
import {
DynamicFormItemConfig,
+ getDefaultValues,
parseDynamicFormItemType,
} from '@/app/home/components/dynamic-form/DynamicFormItemConfig';
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
@@ -47,63 +49,20 @@ import { LoadingSpinner } from '@/components/ui/loading-spinner';
import { cn } from '@/lib/utils';
import { LanguageSelector } from '@/components/ui/language-selector';
import {
- AlertDialog,
- AlertDialogAction,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
-} from '@/components/ui/alert-dialog';
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
-interface WizardState {
- currentStep: number;
- selectedAdapter: string | null;
- selectedRunner: string | null;
- botName: string;
- botDescription: string;
- adapterConfig: Record;
- runnerConfig: Record;
- createdBotUuid: string | null;
-}
-
-const WIZARD_STORAGE_KEY = 'langbot_wizard_state';
-
const TOTAL_STEPS = 4;
-// ---------------------------------------------------------------------------
-// Persistence helpers
-// ---------------------------------------------------------------------------
-
-function loadWizardState(): WizardState | null {
- if (typeof window === 'undefined') return null;
- try {
- const raw = localStorage.getItem(WIZARD_STORAGE_KEY);
- if (!raw) return null;
- return JSON.parse(raw) as WizardState;
- } catch {
- return null;
- }
-}
-
-function saveWizardState(state: WizardState): void {
- if (typeof window === 'undefined') return;
- try {
- localStorage.setItem(WIZARD_STORAGE_KEY, JSON.stringify(state));
- } catch {
- // localStorage may be full - silently ignore
- }
-}
-
-function clearWizardState(): void {
- if (typeof window === 'undefined') return;
- localStorage.removeItem(WIZARD_STORAGE_KEY);
-}
-
// ---------------------------------------------------------------------------
// Main Wizard Page (full-screen, no sidebar)
// ---------------------------------------------------------------------------
@@ -113,30 +72,19 @@ export default function WizardPage() {
const router = useRouter();
// ---- Wizard state ----
- const restoredState = useRef(loadWizardState());
- const [currentStep, setCurrentStep] = useState(
- restoredState.current?.currentStep ?? 0,
- );
- const [selectedAdapter, setSelectedAdapter] = useState(
- restoredState.current?.selectedAdapter ?? null,
- );
- const [selectedRunner, setSelectedRunner] = useState(
- restoredState.current?.selectedRunner ?? null,
- );
- const [botName, setBotName] = useState(restoredState.current?.botName ?? '');
+ const [currentStep, setCurrentStep] = useState(0);
+ const [selectedAdapter, setSelectedAdapter] = useState(null);
+ const [selectedRunner, setSelectedRunner] = useState(null);
+ const [botName, setBotName] = useState('');
// eslint-disable-next-line @typescript-eslint/no-unused-vars
- const [botDescription, _setBotDescription] = useState(
- restoredState.current?.botDescription ?? '',
- );
+ const [botDescription, _setBotDescription] = useState('');
const [adapterConfig, setAdapterConfig] = useState>(
- restoredState.current?.adapterConfig ?? {},
- );
- const [runnerConfig, setRunnerConfig] = useState>(
- restoredState.current?.runnerConfig ?? {},
- );
- const [createdBotUuid, setCreatedBotUuid] = useState(
- restoredState.current?.createdBotUuid ?? null,
+ {},
);
+ const [runnerConfig, setRunnerConfig] = useState>({});
+ const [createdBotUuid, setCreatedBotUuid] = useState(null);
+ const [webhookUrl, setWebhookUrl] = useState('');
+ const [extraWebhookUrl, setExtraWebhookUrl] = useState('');
// ---- Remote data ----
const [adapters, setAdapters] = useState([]);
@@ -149,29 +97,6 @@ export default function WizardPage() {
const [isSavingBot, setIsSavingBot] = useState(false);
const [botSaved, setBotSaved] = useState(false);
- // ---- Persist state on every change ----
- useEffect(() => {
- saveWizardState({
- currentStep,
- selectedAdapter,
- selectedRunner,
- botName,
- botDescription,
- adapterConfig,
- runnerConfig,
- createdBotUuid,
- });
- }, [
- currentStep,
- selectedAdapter,
- selectedRunner,
- botName,
- botDescription,
- adapterConfig,
- runnerConfig,
- createdBotUuid,
- ]);
-
// ---- Fetch remote data ----
useEffect(() => {
let cancelled = false;
@@ -300,16 +225,34 @@ export default function WizardPage() {
: selectedAdapter;
setBotName(defaultName);
+ const defaultConfig = adapter
+ ? getDefaultValues(adapter.spec.config)
+ : {};
+
const bot: Bot = {
name: defaultName,
description: '',
adapter: selectedAdapter,
- adapter_config: {},
+ adapter_config: defaultConfig,
enable: false,
};
const resp = await httpClient.createBot(bot);
setCreatedBotUuid(resp.uuid);
- toast.success(t('wizard.botCreateSuccess'));
+
+ // Fetch runtime info to get webhook URL(s)
+ try {
+ const botData = await httpClient.getBot(resp.uuid);
+ const runtimeValues = botData.bot.adapter_runtime_values as
+ | Record
+ | undefined;
+ setWebhookUrl((runtimeValues?.webhook_full_url as string) || '');
+ setExtraWebhookUrl(
+ (runtimeValues?.extra_webhook_full_url as string) || '',
+ );
+ } catch {
+ // Non-critical — webhook URL display is optional
+ }
+
// Advance to Step 1
setCurrentStep(1);
} catch (err) {
@@ -338,6 +281,20 @@ export default function WizardPage() {
enable: true,
});
setBotSaved(true);
+
+ // Re-fetch runtime info to get updated webhook URL(s)
+ try {
+ const botData = await httpClient.getBot(createdBotUuid);
+ const runtimeValues = botData.bot.adapter_runtime_values as
+ | Record
+ | undefined;
+ setWebhookUrl((runtimeValues?.webhook_full_url as string) || '');
+ setExtraWebhookUrl(
+ (runtimeValues?.extra_webhook_full_url as string) || '',
+ );
+ } catch {
+ // Non-critical
+ }
} catch (err) {
const apiErr = err as { msg?: string };
toast.error(
@@ -403,7 +360,6 @@ export default function WizardPage() {
use_pipeline_uuid: pipelineResp.uuid,
});
- toast.success(t('wizard.createSuccess'));
setCurrentStep(3);
} catch (err) {
const apiErr = err as { msg?: string };
@@ -442,11 +398,24 @@ export default function WizardPage() {
// ---- Skip handler ----
const [showSkipConfirm, setShowSkipConfirm] = useState(false);
+ const [isSkipping, setIsSkipping] = useState(false);
- const handleSkipConfirm = useCallback(() => {
- clearWizardState();
+ const handleSkipConfirm = useCallback(async () => {
+ if (systemInfo.wizard_status === 'none') {
+ setIsSkipping(true);
+ try {
+ await httpClient.updateWizardStatus('skipped');
+ systemInfo.wizard_status = 'skipped';
+ } catch {
+ toast.error(t('wizard.skipSaveError'));
+ setIsSkipping(false);
+ return; // Dialog stays open — user can retry
+ }
+ setIsSkipping(false);
+ }
+ setShowSkipConfirm(false);
router.push('/home');
- }, [router]);
+ }, [router, t]);
// ---- Render ----
@@ -563,6 +532,8 @@ export default function WizardPage() {
isSavingBot={isSavingBot}
botSaved={botSaved}
onSaveBot={handleSaveBot}
+ webhookUrl={webhookUrl}
+ extraWebhookUrl={extraWebhookUrl}
/>
)}
{currentStep === 2 && (
@@ -623,21 +594,31 @@ export default function WizardPage() {
)}
{/* Skip confirmation dialog */}
-
-
-
- {t('wizard.skip')}
-
+
-
-
-
+
+
+
+
+
-
-
-
+
+
+
+
);
}
@@ -722,6 +703,8 @@ function StepBotConfig({
isSavingBot,
botSaved,
onSaveBot,
+ webhookUrl,
+ extraWebhookUrl,
}: {
adapterConfigItems: IDynamicFormItemSchema[];
adapterConfigValues: Record
;
@@ -732,6 +715,8 @@ function StepBotConfig({
isSavingBot: boolean;
botSaved: boolean;
onSaveBot: () => void;
+ webhookUrl: string;
+ extraWebhookUrl: string;
}) {
const { t } = useTranslation();
@@ -787,7 +772,11 @@ function StepBotConfig({
itemConfigList={adapterConfigItems}
initialValues={adapterConfigValues as Record}
onSubmit={stableAdapterConfigCb}
- systemContext={{ is_wizard: true }}
+ systemContext={{
+ is_wizard: true,
+ webhook_url: webhookUrl,
+ extra_webhook_url: extraWebhookUrl,
+ }}
/>
@@ -1037,10 +1026,23 @@ function StepDone() {
})),
);
- const handleBack = useCallback(() => {
- clearWizardState();
+ const [isCompleting, setIsCompleting] = useState(false);
+
+ const handleBack = useCallback(async () => {
+ if (systemInfo.wizard_status === 'none') {
+ setIsCompleting(true);
+ try {
+ await httpClient.updateWizardStatus('completed');
+ systemInfo.wizard_status = 'completed';
+ } catch {
+ toast.error(t('wizard.completeSaveError'));
+ setIsCompleting(false);
+ return; // Don't navigate — let user retry
+ }
+ setIsCompleting(false);
+ }
router.push('/home/bots');
- }, [router]);
+ }, [router, t]);
return (
@@ -1065,7 +1067,8 @@ function StepDone() {
{t('wizard.done.description')}
-