mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 03:55:55 +00:00
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
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
import quart
|
import quart
|
||||||
|
import sqlalchemy
|
||||||
|
|
||||||
from .. import group
|
from .. import group
|
||||||
from .....utils import constants
|
from .....utils import constants
|
||||||
|
from .....entity.persistence.metadata import Metadata
|
||||||
|
|
||||||
|
|
||||||
@group.group_class('system', '/api/v1/system')
|
@group.group_class('system', '/api/v1/system')
|
||||||
@@ -9,6 +11,19 @@ class SystemRouterGroup(group.RouterGroup):
|
|||||||
async def initialize(self) -> None:
|
async def initialize(self) -> None:
|
||||||
@self.route('/info', methods=['GET'], auth_type=group.AuthType.NONE)
|
@self.route('/info', methods=['GET'], auth_type=group.AuthType.NONE)
|
||||||
async def _() -> str:
|
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(
|
return self.success(
|
||||||
data={
|
data={
|
||||||
'version': constants.semantic_version,
|
'version': constants.semantic_version,
|
||||||
@@ -27,9 +42,38 @@ class SystemRouterGroup(group.RouterGroup):
|
|||||||
'disable_models_service', False
|
'disable_models_service', False
|
||||||
),
|
),
|
||||||
'limitation': self.ap.instance_config.data.get('system', {}).get('limitation', {}),
|
'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)
|
@self.route('/tasks', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
||||||
async def _() -> str:
|
async def _() -> str:
|
||||||
task_type = quart.request.args.get('type')
|
task_type = quart.request.args.get('type')
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ metadata:
|
|||||||
en_US: OneBot v11
|
en_US: OneBot v11
|
||||||
zh_Hans: OneBot v11
|
zh_Hans: OneBot v11
|
||||||
description:
|
description:
|
||||||
en_US: OneBot v11 Adapter
|
en_US: OneBot v11 Adapter, used for QQ bots
|
||||||
zh_Hans: OneBot v11 适配器,请查看文档了解使用方式
|
zh_Hans: OneBot v11 适配器,用于接入 QQ 机器人协议端,请查看文档了解使用方式
|
||||||
icon: onebot.png
|
icon: onebot.png
|
||||||
spec:
|
spec:
|
||||||
config:
|
config:
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ metadata:
|
|||||||
zh_Hans: Discord
|
zh_Hans: Discord
|
||||||
description:
|
description:
|
||||||
en_US: Discord Adapter
|
en_US: Discord Adapter
|
||||||
zh_Hans: Discord 适配器,请查看文档了解使用方式
|
zh_Hans: Discord 适配器,需要可连接 Discord 服务器的网络环境
|
||||||
|
ja_JP: Discord アダプター
|
||||||
icon: discord.svg
|
icon: discord.svg
|
||||||
spec:
|
spec:
|
||||||
config:
|
config:
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ metadata:
|
|||||||
en_US: Lark
|
en_US: Lark
|
||||||
zh_Hans: 飞书
|
zh_Hans: 飞书
|
||||||
description:
|
description:
|
||||||
en_US: Lark Adapter
|
en_US: Lark Adapter, supports both long connection and Webhook modes. Please refer to the documentation for usage details.
|
||||||
zh_Hans: 飞书适配器,请查看文档了解使用方式
|
zh_Hans: 飞书适配器,支持长连接和 Webhook 两种接入方式,请查看文档了解使用方式
|
||||||
|
ja_JP: Lark アダプター、長期接続およびWebhookモードの両方をサポートしています。使用方法の詳細については、ドキュメントを参照してください。
|
||||||
icon: lark.svg
|
icon: lark.svg
|
||||||
spec:
|
spec:
|
||||||
config:
|
config:
|
||||||
@@ -45,6 +46,20 @@ spec:
|
|||||||
type: boolean
|
type: boolean
|
||||||
required: true
|
required: true
|
||||||
default: false
|
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
|
- name: encrypt-key
|
||||||
label:
|
label:
|
||||||
en_US: Encrypt Key
|
en_US: Encrypt Key
|
||||||
@@ -55,6 +70,10 @@ spec:
|
|||||||
type: string
|
type: string
|
||||||
required: true
|
required: true
|
||||||
default: ""
|
default: ""
|
||||||
|
show_if:
|
||||||
|
field: enable-webhook
|
||||||
|
operator: eq
|
||||||
|
value: true
|
||||||
- name: enable-stream-reply
|
- name: enable-stream-reply
|
||||||
label:
|
label:
|
||||||
en_US: Enable Stream Reply Mode
|
en_US: Enable Stream Reply Mode
|
||||||
|
|||||||
@@ -6,13 +6,27 @@ metadata:
|
|||||||
en_US: LINE
|
en_US: LINE
|
||||||
zh_Hans: LINE
|
zh_Hans: LINE
|
||||||
description:
|
description:
|
||||||
en_US: LINE Adapter
|
en_US: LINE Adapter, requires a public URL to receive LINE message pushes, please refer to the documentation for usage details
|
||||||
zh_Hans: LINE适配器,请查看文档了解使用方式
|
zh_Hans: LINE适配器,需要公网地址以接收 LINE 消息推送,请查看文档了解使用方式
|
||||||
ja_JP: LINEアダプター、ドキュメントを参照してください
|
ja_JP: LINEアダプター、LINEのメッセージプッシュを受信するためにパブリックURLが必要です。使用方法の詳細については、ドキュメントを参照してください。
|
||||||
zh_Hant: LINE適配器,請查看文檔了解使用方式
|
zh_Hant: LINE適配器,需要公网地址以接收 LINE 消息推送,请查看文档了解使用方式
|
||||||
icon: line.png
|
icon: line.png
|
||||||
spec:
|
spec:
|
||||||
config:
|
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
|
- name: channel_access_token
|
||||||
label:
|
label:
|
||||||
en_US: Channel access token
|
en_US: Channel access token
|
||||||
|
|||||||
@@ -7,10 +7,20 @@ metadata:
|
|||||||
zh_Hans: 微信公众号
|
zh_Hans: 微信公众号
|
||||||
description:
|
description:
|
||||||
en_US: Official Account Adapter
|
en_US: Official Account Adapter
|
||||||
zh_Hans: 微信公众号适配器,请查看文档了解使用方式
|
zh_Hans: 微信公众号适配器,需要公网地址以接收消息推送,请查看文档了解使用方式
|
||||||
icon: officialaccount.png
|
icon: officialaccount.png
|
||||||
spec:
|
spec:
|
||||||
config:
|
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
|
- name: token
|
||||||
label:
|
label:
|
||||||
en_US: Token
|
en_US: Token
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ metadata:
|
|||||||
name: openclaw-weixin
|
name: openclaw-weixin
|
||||||
label:
|
label:
|
||||||
en_US: OpenClaw WeChat
|
en_US: OpenClaw WeChat
|
||||||
zh_Hans: OpenClaw 微信
|
zh_Hans: 个人微信机器人
|
||||||
description:
|
description:
|
||||||
en_US: OpenClaw WeChat adapter, supports personal WeChat via QR code login
|
en_US: OpenClaw WeChat adapter, supports personal WeChat via QR code login
|
||||||
zh_Hans: OpenClaw 微信适配器,通过扫码登录支持个人微信
|
zh_Hans: 微信官方个人助手,扫码即可登录使用
|
||||||
icon: wechat.png
|
icon: wechat.png
|
||||||
spec:
|
spec:
|
||||||
config:
|
config:
|
||||||
@@ -27,7 +27,7 @@ spec:
|
|||||||
zh_Hans: 令牌
|
zh_Hans: 令牌
|
||||||
description:
|
description:
|
||||||
en_US: Bearer token obtained after QR code login authorization. Leave empty to trigger QR code login on startup.
|
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
|
type: string
|
||||||
required: false
|
required: false
|
||||||
default: ""
|
default: ""
|
||||||
|
|||||||
@@ -7,10 +7,20 @@ metadata:
|
|||||||
zh_Hans: QQ 官方 API
|
zh_Hans: QQ 官方 API
|
||||||
description:
|
description:
|
||||||
en_US: QQ Official API (Webhook)
|
en_US: QQ Official API (Webhook)
|
||||||
zh_Hans: QQ 官方 API (Webhook),请查看文档了解使用方式
|
zh_Hans: QQ 官方 API (Webhook),需要公网地址以接收消息推送,请查看文档了解使用方式
|
||||||
icon: qqofficial.svg
|
icon: qqofficial.svg
|
||||||
spec:
|
spec:
|
||||||
config:
|
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
|
- name: appid
|
||||||
label:
|
label:
|
||||||
en_US: App ID
|
en_US: App ID
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ metadata:
|
|||||||
zh_Hans: Satori
|
zh_Hans: Satori
|
||||||
description:
|
description:
|
||||||
en_US: SatoriAdapter
|
en_US: SatoriAdapter
|
||||||
zh_Hans: 古明地觉协议适配器
|
zh_Hans: Satori 协议适配器,支持多种平台的接入,请查看文档了解使用方式
|
||||||
icon: satori.png
|
icon: satori.png
|
||||||
spec:
|
spec:
|
||||||
config:
|
config:
|
||||||
|
|||||||
@@ -7,10 +7,22 @@ metadata:
|
|||||||
zh_Hans: Slack
|
zh_Hans: Slack
|
||||||
description:
|
description:
|
||||||
en_US: Slack Adapter
|
en_US: Slack Adapter
|
||||||
zh_Hans: Slack 适配器,请查看文档了解使用方式
|
zh_Hans: Slack 适配器,需要公网地址以接收 Slack 消息推送,请查看文档了解使用方式
|
||||||
|
ja_JP: Slack アダプター、Slackのメッセージプッシュを受信するためにパブリックURLが必要です。使用方法の詳細については、ドキュメントを参照してください。
|
||||||
|
zh_Hant: Slack 适配器,需要公网地址以接收 Slack 消息推送,请查看文档了解使用方式
|
||||||
icon: slack.png
|
icon: slack.png
|
||||||
spec:
|
spec:
|
||||||
config:
|
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
|
- name: bot_token
|
||||||
label:
|
label:
|
||||||
en_US: Bot Token
|
en_US: Bot Token
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ metadata:
|
|||||||
zh_Hans: 电报
|
zh_Hans: 电报
|
||||||
description:
|
description:
|
||||||
en_US: Telegram Adapter
|
en_US: Telegram Adapter
|
||||||
zh_Hans: 电报适配器,请查看文档了解使用方式
|
zh_Hans: Telegram 适配器,请查看文档了解使用方式
|
||||||
icon: telegram.svg
|
icon: telegram.svg
|
||||||
spec:
|
spec:
|
||||||
config:
|
config:
|
||||||
@@ -17,7 +17,7 @@ spec:
|
|||||||
zh_Hans: 令牌
|
zh_Hans: 令牌
|
||||||
type: string
|
type: string
|
||||||
required: true
|
required: true
|
||||||
default: ""
|
default: "token_from_botfather"
|
||||||
- name: markdown_card
|
- name: markdown_card
|
||||||
label:
|
label:
|
||||||
en_US: Markdown Card
|
en_US: Markdown Card
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ metadata:
|
|||||||
name: wechatpad
|
name: wechatpad
|
||||||
label:
|
label:
|
||||||
en_US: WeChatPad
|
en_US: WeChatPad
|
||||||
zh_CN: WeChatPad(个人微信ipad)
|
zh_Hans: WeChatPad(个人微信ipad)
|
||||||
description:
|
description:
|
||||||
en_US: WeChatPad Adapter
|
en_US: WeChatPad Adapter
|
||||||
zh_CN: WeChatPad 适配器
|
zh_Hans: WeChatPad 适配器,基于WeChatPad的个人微信解决方案,请查看文档了解使用方式
|
||||||
icon: wechatpad.png
|
icon: wechatpad.png
|
||||||
spec:
|
spec:
|
||||||
config:
|
config:
|
||||||
|
|||||||
@@ -7,10 +7,20 @@ metadata:
|
|||||||
zh_Hans: 企业微信
|
zh_Hans: 企业微信
|
||||||
description:
|
description:
|
||||||
en_US: WeCom Adapter
|
en_US: WeCom Adapter
|
||||||
zh_Hans: 企业微信适配器,请查看文档了解使用方式
|
zh_Hans: 企业微信内部机器人,请查看文档了解使用方式
|
||||||
icon: wecom.png
|
icon: wecom.png
|
||||||
spec:
|
spec:
|
||||||
config:
|
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
|
- name: corpid
|
||||||
label:
|
label:
|
||||||
en_US: Corpid
|
en_US: Corpid
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ metadata:
|
|||||||
zh_Hans: 企业微信智能机器人
|
zh_Hans: 企业微信智能机器人
|
||||||
description:
|
description:
|
||||||
en_US: WeComBot Adapter
|
en_US: WeComBot Adapter
|
||||||
zh_Hans: 企业微信智能机器人适配器,请查看文档了解使用方式
|
zh_Hans: 企业微信智能机器人,支持长连接和 Webhook 两种接入方式,请查看文档了解使用方式
|
||||||
icon: wecombot.png
|
icon: wecombot.png
|
||||||
spec:
|
spec:
|
||||||
config:
|
config:
|
||||||
@@ -35,6 +35,20 @@ spec:
|
|||||||
type: boolean
|
type: boolean
|
||||||
required: true
|
required: true
|
||||||
default: false
|
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
|
- name: Secret
|
||||||
label:
|
label:
|
||||||
en_US: Secret
|
en_US: Secret
|
||||||
|
|||||||
@@ -7,10 +7,20 @@ metadata:
|
|||||||
zh_Hans: 企业微信客服
|
zh_Hans: 企业微信客服
|
||||||
description:
|
description:
|
||||||
en_US: WeComCSAdapter
|
en_US: WeComCSAdapter
|
||||||
zh_Hans: 企业微信客服适配器
|
zh_Hans: 企业微信对外客服机器人,需要公网地址以接收消息推送,请查看文档了解使用方式
|
||||||
icon: wecom.png
|
icon: wecom.png
|
||||||
spec:
|
spec:
|
||||||
config:
|
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
|
- name: corpid
|
||||||
label:
|
label:
|
||||||
en_US: Corpid
|
en_US: Corpid
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
IChooseAdapterEntity,
|
IChooseAdapterEntity,
|
||||||
IPipelineEntity,
|
IPipelineEntity,
|
||||||
@@ -19,9 +19,7 @@ import { useForm } from 'react-hook-form';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Copy, Check } from 'lucide-react';
|
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -110,30 +108,11 @@ export default function BotForm({
|
|||||||
const [, setIsLoading] = useState<boolean>(false);
|
const [, setIsLoading] = useState<boolean>(false);
|
||||||
const [webhookUrl, setWebhookUrl] = useState<string>('');
|
const [webhookUrl, setWebhookUrl] = useState<string>('');
|
||||||
const [extraWebhookUrl, setExtraWebhookUrl] = useState<string>('');
|
const [extraWebhookUrl, setExtraWebhookUrl] = useState<string>('');
|
||||||
const [copied, setCopied] = useState<boolean>(false);
|
|
||||||
const [extraCopied, setExtraCopied] = useState<boolean>(false);
|
|
||||||
|
|
||||||
// Watch adapter and adapter_config for filtering
|
// Watch adapter and adapter_config for filtering
|
||||||
const currentAdapter = form.watch('adapter');
|
const currentAdapter = form.watch('adapter');
|
||||||
const currentAdapterConfig = form.watch('adapter_config');
|
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
|
// Notify parent when dirty state changes
|
||||||
const { isDirty } = form.formState;
|
const { isDirty } = form.formState;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -144,43 +123,6 @@ export default function BotForm({
|
|||||||
setBotFormValues();
|
setBotFormValues();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const copyToClipboard = (
|
|
||||||
text: string,
|
|
||||||
setStatus: React.Dispatch<React.SetStateAction<boolean>>,
|
|
||||||
) => {
|
|
||||||
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<React.SetStateAction<boolean>>,
|
|
||||||
) => {
|
|
||||||
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() {
|
function setBotFormValues() {
|
||||||
isInitializing.current = true;
|
isInitializing.current = true;
|
||||||
initBotFormComponent().then(() => {
|
initBotFormComponent().then(() => {
|
||||||
@@ -384,12 +326,6 @@ export default function BotForm({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Webhook URL display helper ---
|
|
||||||
const showWebhook =
|
|
||||||
initBotId &&
|
|
||||||
webhookUrl &&
|
|
||||||
(currentAdapter !== 'lark' || enableWebhook !== false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
@@ -574,75 +510,19 @@ export default function BotForm({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Webhook URL: shown after adapter is selected (edit mode only) */}
|
{showDynamicForm && dynamicFormConfigList.length > 0 && (
|
||||||
{showWebhook && (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t('bots.webhookUrl')}</FormLabel>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Input
|
|
||||||
value={webhookUrl}
|
|
||||||
readOnly
|
|
||||||
className="flex-1 bg-muted"
|
|
||||||
onClick={(e) => {
|
|
||||||
(e.target as HTMLInputElement).select();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => copyToClipboard(webhookUrl, setCopied)}
|
|
||||||
>
|
|
||||||
{copied ? (
|
|
||||||
<Check className="h-4 w-4 text-green-600" />
|
|
||||||
) : (
|
|
||||||
<Copy className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{extraWebhookUrl && (
|
|
||||||
<div className="flex items-center gap-2 mt-2">
|
|
||||||
<Input
|
|
||||||
value={extraWebhookUrl}
|
|
||||||
readOnly
|
|
||||||
className="flex-1 bg-muted"
|
|
||||||
onClick={(e) => {
|
|
||||||
(e.target as HTMLInputElement).select();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() =>
|
|
||||||
copyToClipboard(extraWebhookUrl, setExtraCopied)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{extraCopied ? (
|
|
||||||
<Check className="h-4 w-4 text-green-600" />
|
|
||||||
) : (
|
|
||||||
<Copy className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<FormDescription>
|
|
||||||
{extraWebhookUrl
|
|
||||||
? t('bots.webhookUrlHintEither')
|
|
||||||
: t('bots.webhookUrlHint')}
|
|
||||||
</FormDescription>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showDynamicForm && filteredDynamicFormConfigList.length > 0 && (
|
|
||||||
<DynamicFormComponent
|
<DynamicFormComponent
|
||||||
itemConfigList={filteredDynamicFormConfigList}
|
itemConfigList={dynamicFormConfigList}
|
||||||
initialValues={currentAdapterConfig}
|
initialValues={currentAdapterConfig}
|
||||||
onSubmit={(values) => {
|
onSubmit={(values) => {
|
||||||
form.setValue('adapter_config', values, {
|
form.setValue('adapter_config', values, {
|
||||||
shouldDirty: !isInitializing.current,
|
shouldDirty: !isInitializing.current,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
systemContext={{
|
||||||
|
webhook_url: webhookUrl,
|
||||||
|
extra_webhook_url: extraWebhookUrl,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -11,10 +11,13 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@/components/ui/form';
|
} from '@/components/ui/form';
|
||||||
import DynamicFormItemComponent from '@/app/home/components/dynamic-form/DynamicFormItemComponent';
|
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 { extractI18nObject } from '@/i18n/I18nProvider';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { cn } from '@/lib/utils';
|
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.
|
* Resolve the value referenced by a `show_if.field` string.
|
||||||
@@ -40,6 +43,89 @@ function resolveShowIfValue(
|
|||||||
return externalDependentValues?.[field];
|
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 (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{label}</FormLabel>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
value={url}
|
||||||
|
readOnly
|
||||||
|
className="flex-1 bg-muted"
|
||||||
|
onClick={(e) => (e.target as HTMLInputElement).select()}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleCopy(url, setCopied)}
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<Check className="h-4 w-4 text-green-600" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{extraUrl && (
|
||||||
|
<div className="flex items-center gap-2 mt-2">
|
||||||
|
<Input
|
||||||
|
value={extraUrl}
|
||||||
|
readOnly
|
||||||
|
className="flex-1 bg-muted"
|
||||||
|
onClick={(e) => (e.target as HTMLInputElement).select()}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleCopy(extraUrl, setExtraCopied)}
|
||||||
|
>
|
||||||
|
{extraCopied ? (
|
||||||
|
<Check className="h-4 w-4 text-green-600" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{description && (
|
||||||
|
<p className="text-sm text-muted-foreground">{description}</p>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function DynamicFormComponent({
|
export default function DynamicFormComponent({
|
||||||
itemConfigList,
|
itemConfigList,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
@@ -99,9 +185,16 @@ export default function DynamicFormComponent({
|
|||||||
return value;
|
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
|
// 根据 itemConfigList 动态生成 zod schema
|
||||||
const formSchema = z.object(
|
const formSchema = z.object(
|
||||||
itemConfigList.reduce(
|
editableItems.reduce(
|
||||||
(acc, item) => {
|
(acc, item) => {
|
||||||
let fieldSchema;
|
let fieldSchema;
|
||||||
switch (item.type) {
|
switch (item.type) {
|
||||||
@@ -179,7 +272,7 @@ export default function DynamicFormComponent({
|
|||||||
|
|
||||||
const form = useForm<FormValues>({
|
const form = useForm<FormValues>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: itemConfigList.reduce((acc, item) => {
|
defaultValues: editableItems.reduce((acc, item) => {
|
||||||
// 优先使用 initialValues,如果没有则使用默认值
|
// 优先使用 initialValues,如果没有则使用默认值
|
||||||
const rawValue = initialValues?.[item.name] ?? item.default;
|
const rawValue = initialValues?.[item.name] ?? item.default;
|
||||||
return {
|
return {
|
||||||
@@ -207,7 +300,7 @@ export default function DynamicFormComponent({
|
|||||||
|
|
||||||
if (initialValues && hasRealChange) {
|
if (initialValues && hasRealChange) {
|
||||||
// 合并默认值和初始值
|
// 合并默认值和初始值
|
||||||
const mergedValues = itemConfigList.reduce(
|
const mergedValues = editableItems.reduce(
|
||||||
(acc, item) => {
|
(acc, item) => {
|
||||||
const rawValue = initialValues[item.name] ?? item.default;
|
const rawValue = initialValues[item.name] ?? item.default;
|
||||||
acc[item.name] = normalizeFieldValue(item, rawValue) as object;
|
acc[item.name] = normalizeFieldValue(item, rawValue) as object;
|
||||||
@@ -222,7 +315,7 @@ export default function DynamicFormComponent({
|
|||||||
|
|
||||||
previousInitialValues.current = initialValues;
|
previousInitialValues.current = initialValues;
|
||||||
}
|
}
|
||||||
}, [initialValues, form, itemConfigList]);
|
}, [initialValues, form, editableItems]);
|
||||||
|
|
||||||
// Get reactive form values for conditional rendering
|
// Get reactive form values for conditional rendering
|
||||||
const watchedValues = form.watch();
|
const watchedValues = form.watch();
|
||||||
@@ -238,7 +331,7 @@ export default function DynamicFormComponent({
|
|||||||
// even if the user saves without modifying any field.
|
// even if the user saves without modifying any field.
|
||||||
// form.watch(callback) only fires on subsequent changes, not on mount.
|
// form.watch(callback) only fires on subsequent changes, not on mount.
|
||||||
const formValues = form.getValues();
|
const formValues = form.getValues();
|
||||||
const initialFinalValues = itemConfigList.reduce(
|
const initialFinalValues = editableItems.reduce(
|
||||||
(acc, item) => {
|
(acc, item) => {
|
||||||
acc[item.name] = formValues[item.name] ?? item.default;
|
acc[item.name] = formValues[item.name] ?? item.default;
|
||||||
return acc;
|
return acc;
|
||||||
@@ -258,7 +351,7 @@ export default function DynamicFormComponent({
|
|||||||
|
|
||||||
const subscription = form.watch(() => {
|
const subscription = form.watch(() => {
|
||||||
const formValues = form.getValues();
|
const formValues = form.getValues();
|
||||||
const finalValues = itemConfigList.reduce(
|
const finalValues = editableItems.reduce(
|
||||||
(acc, item) => {
|
(acc, item) => {
|
||||||
acc[item.name] = formValues[item.name] ?? item.default;
|
acc[item.name] = formValues[item.name] ?? item.default;
|
||||||
return acc;
|
return acc;
|
||||||
@@ -269,7 +362,7 @@ export default function DynamicFormComponent({
|
|||||||
previousInitialValues.current = finalValues as Record<string, object>;
|
previousInitialValues.current = finalValues as Record<string, object>;
|
||||||
});
|
});
|
||||||
return () => subscription.unsubscribe();
|
return () => subscription.unsubscribe();
|
||||||
}, [form, itemConfigList]);
|
}, [form, editableItems]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
@@ -307,6 +400,29 @@ export default function DynamicFormComponent({
|
|||||||
// All fields are disabled when editing (creation_settings are immutable)
|
// All fields are disabled when editing (creation_settings are immutable)
|
||||||
const isFieldDisabled = !!isEditing;
|
const isFieldDisabled = !!isEditing;
|
||||||
|
|
||||||
|
// Webhook URL fields are display-only; render outside of form binding
|
||||||
|
if (config.type === 'webhook-url') {
|
||||||
|
const webhookUrl = (systemContext?.webhook_url as string) || '';
|
||||||
|
const extraWebhookUrl =
|
||||||
|
(systemContext?.extra_webhook_url as string) || '';
|
||||||
|
|
||||||
|
if (!webhookUrl) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WebhookUrlField
|
||||||
|
key={config.id}
|
||||||
|
label={extractI18nObject(config.label)}
|
||||||
|
description={
|
||||||
|
config.description
|
||||||
|
? extractI18nObject(config.description)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
url={webhookUrl}
|
||||||
|
extraUrl={extraWebhookUrl || undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Boolean fields use a special inline layout
|
// Boolean fields use a special inline layout
|
||||||
if (config.type === 'boolean') {
|
if (config.type === 'boolean') {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1313,7 +1313,7 @@ export default function HomeSidebar({
|
|||||||
onClick={() => setApiKeyDialogOpen(true)}
|
onClick={() => setApiKeyDialogOpen(true)}
|
||||||
tooltip={t('common.apiIntegration')}
|
tooltip={t('common.apiIntegration')}
|
||||||
>
|
>
|
||||||
<KeyRound className="size-4" />
|
<KeyRound className="size-4 text-blue-500" />
|
||||||
<span>{t('common.apiIntegration')}</span>
|
<span>{t('common.apiIntegration')}</span>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
@@ -1331,6 +1331,7 @@ export default function HomeSidebar({
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
className="text-blue-500"
|
||||||
>
|
>
|
||||||
<path d="M10.6144 17.7956C10.277 18.5682 9.20776 18.5682 8.8704 17.7956L7.99275 15.7854C7.21171 13.9966 5.80589 12.5726 4.0523 11.7942L1.63658 10.7219C.868536 10.381.868537 9.26368 1.63658 8.92276L3.97685 7.88394C5.77553 7.08552 7.20657 5.60881 7.97427 3.75892L8.8633 1.61673C9.19319.821767 10.2916.821765 10.6215 1.61673L11.5105 3.75894C12.2782 5.60881 13.7092 7.08552 15.5079 7.88394L17.8482 8.92276C18.6162 9.26368 18.6162 10.381 17.8482 10.7219L15.4325 11.7942C13.6789 12.5726 12.2731 13.9966 11.492 15.7854L10.6144 17.7956ZM4.53956 9.82234C6.8254 10.837 8.68402 12.5048 9.74238 14.7996 10.8008 12.5048 12.6594 10.837 14.9452 9.82234 12.6321 8.79557 10.7676 7.04647 9.74239 4.71088 8.71719 7.04648 6.85267 8.79557 4.53956 9.82234ZM19.4014 22.6899 19.6482 22.1242C20.0882 21.1156 20.8807 20.3125 21.8695 19.8732L22.6299 19.5353C23.0412 19.3526 23.0412 18.7549 22.6299 18.5722L21.9121 18.2532C20.8978 17.8026 20.0911 16.9698 19.6586 15.9269L19.4052 15.3156C19.2285 14.8896 18.6395 14.8896 18.4628 15.3156L18.2094 15.9269C17.777 16.9698 16.9703 17.8026 15.956 18.2532L15.2381 18.5722C14.8269 18.7549 14.8269 19.3526 15.2381 19.5353L15.9985 19.8732C16.9874 20.3125 17.7798 21.1156 18.2198 22.1242L18.4667 22.6899C18.6473 23.104 19.2207 23.104 19.4014 22.6899ZM18.3745 19.0469 18.937 18.4883 19.4878 19.0469 18.937 19.5898 18.3745 19.0469Z" />
|
<path d="M10.6144 17.7956C10.277 18.5682 9.20776 18.5682 8.8704 17.7956L7.99275 15.7854C7.21171 13.9966 5.80589 12.5726 4.0523 11.7942L1.63658 10.7219C.868536 10.381.868537 9.26368 1.63658 8.92276L3.97685 7.88394C5.77553 7.08552 7.20657 5.60881 7.97427 3.75892L8.8633 1.61673C9.19319.821767 10.2916.821765 10.6215 1.61673L11.5105 3.75894C12.2782 5.60881 13.7092 7.08552 15.5079 7.88394L17.8482 8.92276C18.6162 9.26368 18.6162 10.381 17.8482 10.7219L15.4325 11.7942C13.6789 12.5726 12.2731 13.9966 11.492 15.7854L10.6144 17.7956ZM4.53956 9.82234C6.8254 10.837 8.68402 12.5048 9.74238 14.7996 10.8008 12.5048 12.6594 10.837 14.9452 9.82234 12.6321 8.79557 10.7676 7.04647 9.74239 4.71088 8.71719 7.04648 6.85267 8.79557 4.53956 9.82234ZM19.4014 22.6899 19.6482 22.1242C20.0882 21.1156 20.8807 20.3125 21.8695 19.8732L22.6299 19.5353C23.0412 19.3526 23.0412 18.7549 22.6299 18.5722L21.9121 18.2532C20.8978 17.8026 20.0911 16.9698 19.6586 15.9269L19.4052 15.3156C19.2285 14.8896 18.6395 14.8896 18.4628 15.3156L18.2094 15.9269C17.777 16.9698 16.9703 17.8026 15.956 18.2532L15.2381 18.5722C14.8269 18.7549 14.8269 19.3526 15.2381 19.5353L15.9985 19.8732C16.9874 20.3125 17.7798 21.1156 18.2198 22.1242L18.4667 22.6899C18.6473 23.104 19.2207 23.104 19.4014 22.6899ZM18.3745 19.0469 18.937 18.4883 19.4878 19.0469 18.937 19.5898 18.3745 19.0469Z" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export const sidebarConfigList = [
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
|
className="text-blue-500"
|
||||||
>
|
>
|
||||||
<path d="M13 9H21L11 24V15H4L13 0V9ZM11 11V7.22063L7.53238 13H13V17.3944L17.263 11H11Z"></path>
|
<path d="M13 9H21L11 24V15H4L13 0V9ZM11 11V7.22063L7.53238 13H13V17.3944L17.263 11H11Z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -37,6 +38,7 @@ export const sidebarConfigList = [
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
|
className="text-blue-500"
|
||||||
>
|
>
|
||||||
<path d="M2 3.9934C2 3.44476 2.45531 3 2.9918 3H21.0082C21.556 3 22 3.44495 22 3.9934V20.0066C22 20.5552 21.5447 21 21.0082 21H2.9918C2.44405 21 2 20.5551 2 20.0066V3.9934ZM4 5V19H20V5H4ZM6 7H18V9H6V7ZM6 11H18V13H6V11ZM6 15H12V17H6V15Z"></path>
|
<path d="M2 3.9934C2 3.44476 2.45531 3 2.9918 3H21.0082C21.556 3 22 3.44495 22 3.9934V20.0066C22 20.5552 21.5447 21 21.0082 21H2.9918C2.44405 21 2 20.5551 2 20.0066V3.9934ZM4 5V19H20V5H4ZM6 7H18V9H6V7ZM6 11H18V13H6V11ZM6 15H12V17H6V15Z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -57,6 +59,7 @@ export const sidebarConfigList = [
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
|
className="text-blue-500"
|
||||||
>
|
>
|
||||||
<path d="M13.5 2C13.5 2.44425 13.3069 2.84339 13 3.11805V5H18C19.6569 5 21 6.34315 21 8V18C21 19.6569 19.6569 21 18 21H6C4.34315 21 3 19.6569 3 18V8C3 6.34315 4.34315 5 6 5H11V3.11805C10.6931 2.84339 10.5 2.44425 10.5 2C10.5 1.17157 11.1716 0.5 12 0.5C12.8284 0.5 13.5 1.17157 13.5 2ZM6 7C5.44772 7 5 7.44772 5 8V18C5 18.5523 5.44772 19 6 19H18C18.5523 19 19 18.5523 19 18V8C19 7.44772 18.5523 7 18 7H13H11H6ZM2 10H0V16H2V10ZM22 10H24V16H22V10ZM9 14.5C9.82843 14.5 10.5 13.8284 10.5 13C10.5 12.1716 9.82843 11.5 9 11.5C8.17157 11.5 7.5 12.1716 7.5 13C7.5 13.8284 8.17157 14.5 9 14.5ZM15 14.5C15.8284 14.5 16.5 13.8284 16.5 13C16.5 12.1716 15.8284 11.5 15 11.5C14.1716 11.5 13.5 12.1716 13.5 13C13.5 13.8284 14.1716 14.5 15 14.5Z"></path>
|
<path d="M13.5 2C13.5 2.44425 13.3069 2.84339 13 3.11805V5H18C19.6569 5 21 6.34315 21 8V18C21 19.6569 19.6569 21 18 21H6C4.34315 21 3 19.6569 3 18V8C3 6.34315 4.34315 5 6 5H11V3.11805C10.6931 2.84339 10.5 2.44425 10.5 2C10.5 1.17157 11.1716 0.5 12 0.5C12.8284 0.5 13.5 1.17157 13.5 2ZM6 7C5.44772 7 5 7.44772 5 8V18C5 18.5523 5.44772 19 6 19H18C18.5523 19 19 18.5523 19 18V8C19 7.44772 18.5523 7 18 7H13H11H6ZM2 10H0V16H2V10ZM22 10H24V16H22V10ZM9 14.5C9.82843 14.5 10.5 13.8284 10.5 13C10.5 12.1716 9.82843 11.5 9 11.5C8.17157 11.5 7.5 12.1716 7.5 13C7.5 13.8284 8.17157 14.5 9 14.5ZM15 14.5C15.8284 14.5 16.5 13.8284 16.5 13C16.5 12.1716 15.8284 11.5 15 11.5C14.1716 11.5 13.5 12.1716 13.5 13C13.5 13.8284 14.1716 14.5 15 14.5Z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -78,6 +81,7 @@ export const sidebarConfigList = [
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
|
className="text-blue-500"
|
||||||
>
|
>
|
||||||
<path d="M6 21.5C4.067 21.5 2.5 19.933 2.5 18C2.5 16.067 4.067 14.5 6 14.5C7.5852 14.5 8.92427 15.5539 9.35481 16.9992L15 16.9994V15L17 14.9994V9.24339L14.757 6.99938H9V9.00003H3V3.00003H9V4.99939H14.757L18 1.75739L22.2426 6.00003L19 9.24139V14.9994L21 15V21H15V18.9994L9.35499 19.0003C8.92464 20.4459 7.58543 21.5 6 21.5ZM6 16.5C5.17157 16.5 4.5 17.1716 4.5 18C4.5 18.8285 5.17157 19.5 6 19.5C6.82843 19.5 7.5 18.8285 7.5 18C7.5 17.1716 6.82843 16.5 6 16.5ZM19 17H17V19H19V17ZM18 4.58581L16.5858 6.00003L18 7.41424L19.4142 6.00003L18 4.58581ZM7 5.00003H5V7.00003H7V5.00003Z"></path>
|
<path d="M6 21.5C4.067 21.5 2.5 19.933 2.5 18C2.5 16.067 4.067 14.5 6 14.5C7.5852 14.5 8.92427 15.5539 9.35481 16.9992L15 16.9994V15L17 14.9994V9.24339L14.757 6.99938H9V9.00003H3V3.00003H9V4.99939H14.757L18 1.75739L22.2426 6.00003L19 9.24139V14.9994L21 15V21H15V18.9994L9.35499 19.0003C8.92464 20.4459 7.58543 21.5 6 21.5ZM6 16.5C5.17157 16.5 4.5 17.1716 4.5 18C4.5 18.8285 5.17157 19.5 6 19.5C6.82843 19.5 7.5 18.8285 7.5 18C7.5 17.1716 6.82843 16.5 6 16.5ZM19 17H17V19H19V17ZM18 4.58581L16.5858 6.00003L18 7.41424L19.4142 6.00003L18 4.58581ZM7 5.00003H5V7.00003H7V5.00003Z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -99,6 +103,7 @@ export const sidebarConfigList = [
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
|
className="text-blue-500"
|
||||||
>
|
>
|
||||||
<path d="M3 18.5V5C3 3.34315 4.34315 2 6 2H20C20.5523 2 21 2.44772 21 3V21C21 21.5523 20.5523 22 20 22H6.5C4.567 22 3 20.433 3 18.5ZM19 20V17H6.5C5.67157 17 5 17.6716 5 18.5C5 19.3284 5.67157 20 6.5 20H19ZM10 4H6C5.44772 4 5 4.44772 5 5V15.3368C5.45463 15.1208 5.9632 15 6.5 15H19V4H17V12L13.5 10L10 12V4Z"></path>
|
<path d="M3 18.5V5C3 3.34315 4.34315 2 6 2H20C20.5523 2 21 2.44772 21 3V21C21 21.5523 20.5523 22 20 22H6.5C4.567 22 3 20.433 3 18.5ZM19 20V17H6.5C5.67157 17 5 17.6716 5 18.5C5 19.3284 5.67157 20 6.5 20H19ZM10 4H6C5.44772 4 5 4.44772 5 5V15.3368C5.45463 15.1208 5.9632 15 6.5 15H19V4H17V12L13.5 10L10 12V4Z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -122,6 +127,7 @@ export const sidebarConfigList = [
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
|
className="text-blue-500"
|
||||||
>
|
>
|
||||||
<path d="M7 5C7 2.79086 8.79086 1 11 1C13.2091 1 15 2.79086 15 5H18C18.5523 5 19 5.44772 19 6V9C21.2091 9 23 10.7909 23 13C23 15.2091 21.2091 17 19 17V20C19 20.5523 18.5523 21 18 21H4C3.44772 21 3 20.5523 3 20V6C3 5.44772 3.44772 5 4 5H7ZM11 3C9.89543 3 9 3.89543 9 5C9 5.23554 9.0403 5.45952 9.11355 5.66675C9.22172 5.97282 9.17461 6.31235 8.98718 6.57739C8.79974 6.84243 8.49532 7 8.17071 7H5V19H17V15.8293C17 15.5047 17.1576 15.2003 17.4226 15.0128C17.6877 14.8254 18.0272 14.7783 18.3332 14.8865C18.5405 14.9597 18.7645 15 19 15C20.1046 15 21 14.1046 21 13C21 11.8954 20.1046 11 19 11C18.7645 11 18.5405 11.0403 18.3332 11.1135C18.0272 11.2217 17.6877 11.1746 17.4226 10.9872C17.1576 10.7997 17 10.4953 17 10.1707V7H13.8293C13.5047 7 13.2003 6.84243 13.0128 6.57739C12.8254 6.31235 12.7783 5.97282 12.8865 5.66675C12.9597 5.45952 13 5.23555 13 5C13 3.89543 12.1046 3 11 3Z"></path>
|
<path d="M7 5C7 2.79086 8.79086 1 11 1C13.2091 1 15 2.79086 15 5H18C18.5523 5 19 5.44772 19 6V9C21.2091 9 23 10.7909 23 13C23 15.2091 21.2091 17 19 17V20C19 20.5523 18.5523 21 18 21H4C3.44772 21 3 20.5523 3 20V6C3 5.44772 3.44772 5 4 5H7ZM11 3C9.89543 3 9 3.89543 9 5C9 5.23554 9.0403 5.45952 9.11355 5.66675C9.22172 5.97282 9.17461 6.31235 8.98718 6.57739C8.79974 6.84243 8.49532 7 8.17071 7H5V19H17V15.8293C17 15.5047 17.1576 15.2003 17.4226 15.0128C17.6877 14.8254 18.0272 14.7783 18.3332 14.8865C18.5405 14.9597 18.7645 15 19 15C20.1046 15 21 14.1046 21 13C21 11.8954 20.1046 11 19 11C18.7645 11 18.5405 11.0403 18.3332 11.1135C18.0272 11.2217 17.6877 11.1746 17.4226 10.9872C17.1576 10.7997 17 10.4953 17 10.1707V7H13.8293C13.5047 7 13.2003 6.84243 13.0128 6.57739C12.8254 6.31235 12.7783 5.97282 12.8865 5.66675C12.9597 5.45952 13 5.23555 13 5C13 3.89543 12.1046 3 11 3Z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -143,6 +149,7 @@ export const sidebarConfigList = [
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
|
className="text-blue-500"
|
||||||
>
|
>
|
||||||
<path d="M21 13.242V20H22V22H2V20H3V13.242C1.79401 12.435 1 11.0602 1 9.5C1 8.67286 1.25027 7.90335 1.67755 7.2612L4.5547 2.36088C4.80513 1.93859 5.26028 1.67578 5.76 1.67578H18.24C18.7397 1.67578 19.1949 1.93859 19.4453 2.36088L22.3225 7.2612C22.7497 7.90335 23 8.67286 23 9.5C23 11.0602 22.206 12.435 21 13.242ZM19 13.972C18.4511 14.0706 17.8794 14.0706 17.3305 13.972C16.1644 13.7566 15.1377 13.0712 14.5 12.1C13.8623 13.0712 12.8356 13.7566 11.6695 13.972C11.1206 14.0706 10.5489 14.0706 10 13.972C9.45108 14.0706 8.87938 14.0706 8.33053 13.972C7.16437 13.7566 6.13771 13.0712 5.5 12.1C4.86229 13.0712 3.83563 13.7566 2.66947 13.972C2.44883 14.0124 2.22434 14.0352 2 14.0404V20H5V15H10V20H19V13.972Z"></path>
|
<path d="M21 13.242V20H22V22H2V20H3V13.242C1.79401 12.435 1 11.0602 1 9.5C1 8.67286 1.25027 7.90335 1.67755 7.2612L4.5547 2.36088C4.80513 1.93859 5.26028 1.67578 5.76 1.67578H18.24C18.7397 1.67578 19.1949 1.93859 19.4453 2.36088L22.3225 7.2612C22.7497 7.90335 23 8.67286 23 9.5C23 11.0602 22.206 12.435 21 13.242ZM19 13.972C18.4511 14.0706 17.8794 14.0706 17.3305 13.972C16.1644 13.7566 15.1377 13.0712 14.5 12.1C13.8623 13.0712 12.8356 13.7566 11.6695 13.972C11.1206 14.0706 10.5489 14.0706 10 13.972C9.45108 14.0706 8.87938 14.0706 8.33053 13.972C7.16437 13.7566 6.13771 13.0712 5.5 12.1C4.86229 13.0712 3.83563 13.7566 2.66947 13.972C2.44883 14.0124 2.22434 14.0352 2 14.0404V20H5V15H10V20H19V13.972Z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -164,6 +171,7 @@ export const sidebarConfigList = [
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
|
className="text-blue-500"
|
||||||
>
|
>
|
||||||
<path d="M4.5 7.65311V16.3469L12 20.689L19.5 16.3469V7.65311L12 3.311L4.5 7.65311ZM12 1L21.5 6.5V17.5L12 23L2.5 17.5V6.5L12 1ZM6.49896 9.97065L11 12.5765V17.625H13V12.5765L17.501 9.97066L16.499 8.2398L12 10.8445L7.50104 8.2398L6.49896 9.97065Z"></path>
|
<path d="M4.5 7.65311V16.3469L12 20.689L19.5 16.3469V7.65311L12 3.311L4.5 7.65311ZM12 1L21.5 6.5V17.5L12 23L2.5 17.5V6.5L12 1ZM6.49896 9.97065L11 12.5765V17.625H13V12.5765L17.501 9.97066L16.499 8.2398L12 10.8445L7.50104 8.2398L6.49896 9.97065Z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -304,7 +304,7 @@ export default function KBForm({
|
|||||||
{t('knowledge.noEnginesAvailable')}
|
{t('knowledge.noEnginesAvailable')}
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
href="/home/plugins"
|
href="/home/market?category=KnowledgeEngine"
|
||||||
className="text-sm text-primary hover:underline"
|
className="text-sm text-primary hover:underline"
|
||||||
>
|
>
|
||||||
{t('knowledge.installEngineHint')}
|
{t('knowledge.installEngineHint')}
|
||||||
|
|||||||
@@ -15,8 +15,13 @@ import {
|
|||||||
useSidebarData,
|
useSidebarData,
|
||||||
} from '@/app/home/components/home-sidebar/SidebarDataContext';
|
} from '@/app/home/components/home-sidebar/SidebarDataContext';
|
||||||
import { I18nObject } from '@/app/infra/entities/common';
|
import { I18nObject } from '@/app/infra/entities/common';
|
||||||
import { userInfo, initializeUserInfo } from '@/app/infra/http';
|
import {
|
||||||
import { usePathname } from 'next/navigation';
|
userInfo,
|
||||||
|
systemInfo,
|
||||||
|
initializeUserInfo,
|
||||||
|
initializeSystemInfo,
|
||||||
|
} from '@/app/infra/http';
|
||||||
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { extractI18nObject } from '@/i18n/I18nProvider';
|
import { extractI18nObject } from '@/i18n/I18nProvider';
|
||||||
import { CircleHelp } from 'lucide-react';
|
import { CircleHelp } from 'lucide-react';
|
||||||
@@ -50,6 +55,8 @@ export default function HomeLayout({
|
|||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
// Initialize user info if not already initialized
|
// Initialize user info if not already initialized
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!userInfo) {
|
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 (
|
return (
|
||||||
<SidebarDataProvider>
|
<SidebarDataProvider>
|
||||||
<HomeLayoutInner>{children}</HomeLayoutInner>
|
<HomeLayoutInner>{children}</HomeLayoutInner>
|
||||||
@@ -143,7 +166,9 @@ function HomeLayoutInner({ children }: { children: React.ReactNode }) {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="flex-1 overflow-hidden p-4 pt-0">{mainContent}</div>
|
<div className="flex-1 overflow-hidden p-4 pt-0 min-w-0">
|
||||||
|
{mainContent}
|
||||||
|
</div>
|
||||||
|
|
||||||
<SurveyWidget />
|
<SurveyWidget />
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
|
|||||||
@@ -147,23 +147,16 @@ export default function TrafficChart({
|
|||||||
<h3 className="text-base font-semibold text-foreground mb-4">
|
<h3 className="text-base font-semibold text-foreground mb-4">
|
||||||
{t('monitoring.trafficChart.title')}
|
{t('monitoring.trafficChart.title')}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="h-[300px] flex flex-col items-center justify-center text-muted-foreground">
|
<div className="h-[300px] flex flex-col items-center justify-center text-muted-foreground gap-2">
|
||||||
<svg
|
<svg
|
||||||
className="w-16 h-16 mb-4 text-muted-foreground/30"
|
className="h-[3rem] w-[3rem]"
|
||||||
fill="none"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
fill="currentColor"
|
||||||
>
|
>
|
||||||
<path
|
<path d="M2 13H8V21H2V13ZM16 8H22V21H16V8ZM9 3H15V21H9V3ZM4 15V19H6V15H4ZM11 5V19H13V5H11ZM18 10V19H20V10H18Z"></path>
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
|
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
<p className="text-sm font-medium">
|
<div className="text-sm">{t('monitoring.trafficChart.noData')}</div>
|
||||||
{t('monitoring.trafficChart.noData')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ function MonitoringPageContent() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full overflow-y-auto">
|
<div className="w-full h-full overflow-y-auto overflow-x-hidden">
|
||||||
{/* Filters and Refresh Button - Sticky */}
|
{/* Filters and Refresh Button - Sticky */}
|
||||||
<div className="sticky top-[-1.5rem] z-10 -ml-[2rem] -mr-[1.5rem] -mt-[1.5rem] pt-[1.5rem] pb-4 bg-background">
|
<div className="sticky top-[-1.5rem] z-10 -ml-[2rem] -mr-[1.5rem] -mt-[1.5rem] pt-[1.5rem] pb-4 bg-background">
|
||||||
<div className="ml-[2rem] mr-[1.5rem] px-[0.8rem]">
|
<div className="ml-[2rem] mr-[1.5rem] px-[0.8rem]">
|
||||||
@@ -379,26 +379,18 @@ function MonitoringPageContent() {
|
|||||||
|
|
||||||
{!loading &&
|
{!loading &&
|
||||||
(!data || !data.messages || data.messages.length === 0) && (
|
(!data || !data.messages || data.messages.length === 0) && (
|
||||||
<div className="text-center text-muted-foreground py-16">
|
<div className="flex flex-col items-center justify-center text-muted-foreground py-16 gap-2">
|
||||||
<svg
|
<svg
|
||||||
className="w-16 h-16 mx-auto mb-4 text-muted-foreground/30"
|
className="h-[3rem] w-[3rem]"
|
||||||
fill="none"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
fill="currentColor"
|
||||||
>
|
>
|
||||||
<path
|
<path d="M6.45455 19L2 22.5V4C2 3.44772 2.44772 3 3 3H21C21.5523 3 22 3.44772 22 4V18C22 18.5523 21.5523 19 21 19H6.45455ZM4 18.3851L5.76282 17H20V5H4V18.3851Z"></path>
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"
|
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
<p className="text-base font-medium mb-2">
|
<div className="text-sm">
|
||||||
{t('monitoring.messageList.noMessages')}
|
{t('monitoring.messageList.noMessages')}
|
||||||
</p>
|
</div>
|
||||||
<p className="text-sm">
|
|
||||||
{t('monitoring.messageList.noMessagesDescription')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -600,23 +592,18 @@ function MonitoringPageContent() {
|
|||||||
(!data ||
|
(!data ||
|
||||||
!data.modelCalls ||
|
!data.modelCalls ||
|
||||||
data.modelCalls.length === 0) && (
|
data.modelCalls.length === 0) && (
|
||||||
<div className="text-center text-muted-foreground py-16">
|
<div className="flex flex-col items-center justify-center text-muted-foreground py-16 gap-2">
|
||||||
<svg
|
<svg
|
||||||
className="w-16 h-16 mx-auto mb-4 text-muted-foreground/30"
|
className="h-[3rem] w-[3rem]"
|
||||||
fill="none"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
fill="currentColor"
|
||||||
>
|
>
|
||||||
<path
|
<path d="M10.6144 17.7956C10.277 18.5682 9.20776 18.5682 8.8704 17.7956L7.99275 15.7854C7.21171 13.9966 5.80589 12.5726 4.0523 11.7942L1.63658 10.7219C.868536 10.381.868537 9.26368 1.63658 8.92276L3.97685 7.88394C5.77553 7.08552 7.20657 5.60881 7.97427 3.75892L8.8633 1.61673C9.19319.821767 10.2916.821765 10.6215 1.61673L11.5105 3.75894C12.2782 5.60881 13.7092 7.08552 15.5079 7.88394L17.8482 8.92276C18.6162 9.26368 18.6162 10.381 17.8482 10.7219L15.4325 11.7942C13.6789 12.5726 12.2731 13.9966 11.492 15.7854L10.6144 17.7956ZM19.4014 22.6899 19.6482 22.1242C20.0882 21.1156 20.8807 20.3125 21.8695 19.8732L22.6299 19.5353C23.0412 19.3526 23.0412 18.7549 22.6299 18.5722L21.9121 18.2532C20.8978 17.8026 20.0911 16.9698 19.6586 15.9269L19.4052 15.3156C19.2285 14.8896 18.6395 14.8896 18.4628 15.3156L18.2094 15.9269C17.777 16.9698 16.9703 17.8026 15.956 18.2532L15.2381 18.5722C14.8269 18.7549 14.8269 19.3526 15.2381 19.5353L15.9985 19.8732C16.9874 20.3125 17.7798 21.1156 18.2198 22.1242L18.4667 22.6899C18.6473 23.104 19.2207 23.104 19.4014 22.6899Z"></path>
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
|
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
<p className="text-base font-medium">
|
<div className="text-sm">
|
||||||
{t('monitoring.modelCalls.noData')}
|
{t('monitoring.modelCalls.noData')}
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -775,23 +762,18 @@ function MonitoringPageContent() {
|
|||||||
|
|
||||||
{!loading &&
|
{!loading &&
|
||||||
(!data || !data.errors || data.errors.length === 0) && (
|
(!data || !data.errors || data.errors.length === 0) && (
|
||||||
<div className="text-center text-muted-foreground py-16">
|
<div className="flex flex-col items-center justify-center text-muted-foreground py-16 gap-2">
|
||||||
<svg
|
<svg
|
||||||
className="w-16 h-16 mx-auto mb-4 text-green-300 dark:text-green-600"
|
className="h-[3rem] w-[3rem] text-green-500 dark:text-green-600"
|
||||||
fill="none"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
fill="currentColor"
|
||||||
>
|
>
|
||||||
<path
|
<path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM11.0026 16L6.75999 11.7574L8.17421 10.3431L11.0026 13.1716L16.6595 7.51472L18.0737 8.92893L11.0026 16Z"></path>
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
<p className="text-base font-medium text-green-600 dark:text-green-400">
|
<div className="text-sm text-green-600 dark:text-green-400">
|
||||||
{t('monitoring.errors.noErrors')}
|
{t('monitoring.errors.noErrors')}
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ export default function PluginDetailContent({ id }: { id: string }) {
|
|||||||
onFormSubmit={handleFormSubmit}
|
onFormSubmit={handleFormSubmit}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Divider */}
|
||||||
|
<div className="hidden md:block w-px bg-border shrink-0" />
|
||||||
{/* Right side - Readme */}
|
{/* Right side - Readme */}
|
||||||
<div className="flex-1 overflow-y-auto overflow-x-hidden min-w-0">
|
<div className="flex-1 overflow-y-auto overflow-x-hidden min-w-0">
|
||||||
<PluginReadme pluginAuthor={pluginAuthor} pluginName={pluginName} />
|
<PluginReadme pluginAuthor={pluginAuthor} pluginName={pluginName} />
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect, useCallback, useRef, Suspense } from 'react';
|
import { useState, useEffect, useCallback, useRef, Suspense } from 'react';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
@@ -46,9 +47,24 @@ function MarketPageContent({
|
|||||||
installPlugin: (plugin: PluginV4) => void;
|
installPlugin: (plugin: PluginV4) => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const validCategories = [
|
||||||
|
'Tool',
|
||||||
|
'Command',
|
||||||
|
'EventListener',
|
||||||
|
'KnowledgeEngine',
|
||||||
|
'Parser',
|
||||||
|
];
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [componentFilter, setComponentFilter] = useState<string>('all');
|
const [componentFilter, setComponentFilter] = useState<string>(() => {
|
||||||
|
const category = searchParams.get('category');
|
||||||
|
if (category && validCategories.includes(category)) {
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
return 'all';
|
||||||
|
});
|
||||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||||
const [availableTags, setAvailableTags] = useState<PluginTag[]>([]);
|
const [availableTags, setAvailableTags] = useState<PluginTag[]>([]);
|
||||||
const [tagNames, setTagNames] = useState<Record<string, string>>({});
|
const [tagNames, setTagNames] = useState<Record<string, string>>({});
|
||||||
@@ -284,6 +300,18 @@ function MarketPageContent({
|
|||||||
setComponentFilter(value);
|
setComponentFilter(value);
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
setPlugins([]);
|
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
|
// fetchPlugins will be called by useEffect when componentFilter changes
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -260,6 +260,7 @@ export interface ApiRespSystemInfo {
|
|||||||
allow_modify_login_info: boolean;
|
allow_modify_login_info: boolean;
|
||||||
disable_models_service: boolean;
|
disable_models_service: boolean;
|
||||||
limitation: SystemLimitation;
|
limitation: SystemLimitation;
|
||||||
|
wizard_status: string; // 'none' | 'skipped' | 'completed'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RagMigrationStatusResp {
|
export interface RagMigrationStatusResp {
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export enum DynamicFormItemType {
|
|||||||
KNOWLEDGE_BASE_MULTI_SELECTOR = 'knowledge-base-multi-selector',
|
KNOWLEDGE_BASE_MULTI_SELECTOR = 'knowledge-base-multi-selector',
|
||||||
PLUGIN_SELECTOR = 'plugin-selector',
|
PLUGIN_SELECTOR = 'plugin-selector',
|
||||||
BOT_SELECTOR = 'bot-selector',
|
BOT_SELECTOR = 'bot-selector',
|
||||||
|
WEBHOOK_URL = 'webhook-url',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IFileConfig {
|
export interface IFileConfig {
|
||||||
|
|||||||
@@ -701,6 +701,10 @@ export class BackendClient extends BaseHttpClient {
|
|||||||
return this.get('/api/v1/system/info');
|
return this.get('/api/v1/system/info');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public updateWizardStatus(status: 'skipped' | 'completed'): Promise<void> {
|
||||||
|
return this.post('/api/v1/system/wizard/completed', { status });
|
||||||
|
}
|
||||||
|
|
||||||
public getAsyncTasks(): Promise<ApiRespAsyncTasks> {
|
public getAsyncTasks(): Promise<ApiRespAsyncTasks> {
|
||||||
return this.get('/api/v1/system/tasks');
|
return this.get('/api/v1/system/tasks');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { CloudServiceClient } from './CloudServiceClient';
|
|||||||
import { ApiRespSystemInfo } from '@/app/infra/entities/api';
|
import { ApiRespSystemInfo } from '@/app/infra/entities/api';
|
||||||
|
|
||||||
// 系统信息
|
// 系统信息
|
||||||
export let systemInfo: ApiRespSystemInfo = {
|
export const systemInfo: ApiRespSystemInfo = {
|
||||||
debug: false,
|
debug: false,
|
||||||
version: '',
|
version: '',
|
||||||
edition: 'community',
|
edition: 'community',
|
||||||
@@ -16,6 +16,7 @@ export let systemInfo: ApiRespSystemInfo = {
|
|||||||
max_pipelines: -1,
|
max_pipelines: -1,
|
||||||
max_extensions: -1,
|
max_extensions: -1,
|
||||||
},
|
},
|
||||||
|
wizard_status: 'none',
|
||||||
};
|
};
|
||||||
|
|
||||||
// 用户信息
|
// 用户信息
|
||||||
@@ -50,7 +51,7 @@ if (typeof window !== 'undefined' && systemInfo.cloud_service_url === '') {
|
|||||||
backendClient
|
backendClient
|
||||||
.getSystemInfo()
|
.getSystemInfo()
|
||||||
.then((info) => {
|
.then((info) => {
|
||||||
systemInfo = info;
|
Object.assign(systemInfo, info);
|
||||||
cloudServiceClient.updateBaseURL(info.cloud_service_url);
|
cloudServiceClient.updateBaseURL(info.cloud_service_url);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -65,7 +66,7 @@ if (typeof window !== 'undefined' && systemInfo.cloud_service_url === '') {
|
|||||||
export const getCloudServiceClient = async (): Promise<CloudServiceClient> => {
|
export const getCloudServiceClient = async (): Promise<CloudServiceClient> => {
|
||||||
if (systemInfo.cloud_service_url === '') {
|
if (systemInfo.cloud_service_url === '') {
|
||||||
try {
|
try {
|
||||||
systemInfo = await backendClient.getSystemInfo();
|
Object.assign(systemInfo, await backendClient.getSystemInfo());
|
||||||
// 更新 cloud service client 的 baseURL
|
// 更新 cloud service client 的 baseURL
|
||||||
cloudServiceClient.updateBaseURL(systemInfo.cloud_service_url);
|
cloudServiceClient.updateBaseURL(systemInfo.cloud_service_url);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -90,7 +91,7 @@ export const getCloudServiceClientSync = (): CloudServiceClient => {
|
|||||||
*/
|
*/
|
||||||
export const initializeSystemInfo = async (): Promise<void> => {
|
export const initializeSystemInfo = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
systemInfo = await backendClient.getSystemInfo();
|
Object.assign(systemInfo, await backendClient.getSystemInfo());
|
||||||
cloudServiceClient.updateBaseURL(systemInfo.cloud_service_url);
|
cloudServiceClient.updateBaseURL(systemInfo.cloud_service_url);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize system info:', error);
|
console.error('Failed to initialize system info:', error);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
import {
|
import {
|
||||||
userInfo,
|
userInfo,
|
||||||
|
systemInfo,
|
||||||
initializeUserInfo,
|
initializeUserInfo,
|
||||||
initializeSystemInfo,
|
initializeSystemInfo,
|
||||||
} from '@/app/infra/http';
|
} from '@/app/infra/http';
|
||||||
@@ -29,6 +30,7 @@ import {
|
|||||||
} from '@/app/infra/entities/pipeline';
|
} from '@/app/infra/entities/pipeline';
|
||||||
import {
|
import {
|
||||||
DynamicFormItemConfig,
|
DynamicFormItemConfig,
|
||||||
|
getDefaultValues,
|
||||||
parseDynamicFormItemType,
|
parseDynamicFormItemType,
|
||||||
} from '@/app/home/components/dynamic-form/DynamicFormItemConfig';
|
} from '@/app/home/components/dynamic-form/DynamicFormItemConfig';
|
||||||
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
|
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
|
||||||
@@ -47,63 +49,20 @@ import { LoadingSpinner } from '@/components/ui/loading-spinner';
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { LanguageSelector } from '@/components/ui/language-selector';
|
import { LanguageSelector } from '@/components/ui/language-selector';
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
Dialog,
|
||||||
AlertDialogAction,
|
DialogContent,
|
||||||
AlertDialogContent,
|
DialogDescription,
|
||||||
AlertDialogDescription,
|
DialogFooter,
|
||||||
AlertDialogFooter,
|
DialogHeader,
|
||||||
AlertDialogHeader,
|
DialogTitle,
|
||||||
AlertDialogTitle,
|
} from '@/components/ui/dialog';
|
||||||
} from '@/components/ui/alert-dialog';
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Types
|
// Types
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
interface WizardState {
|
|
||||||
currentStep: number;
|
|
||||||
selectedAdapter: string | null;
|
|
||||||
selectedRunner: string | null;
|
|
||||||
botName: string;
|
|
||||||
botDescription: string;
|
|
||||||
adapterConfig: Record<string, unknown>;
|
|
||||||
runnerConfig: Record<string, unknown>;
|
|
||||||
createdBotUuid: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const WIZARD_STORAGE_KEY = 'langbot_wizard_state';
|
|
||||||
|
|
||||||
const TOTAL_STEPS = 4;
|
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)
|
// Main Wizard Page (full-screen, no sidebar)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -113,30 +72,19 @@ export default function WizardPage() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// ---- Wizard state ----
|
// ---- Wizard state ----
|
||||||
const restoredState = useRef(loadWizardState());
|
const [currentStep, setCurrentStep] = useState(0);
|
||||||
const [currentStep, setCurrentStep] = useState(
|
const [selectedAdapter, setSelectedAdapter] = useState<string | null>(null);
|
||||||
restoredState.current?.currentStep ?? 0,
|
const [selectedRunner, setSelectedRunner] = useState<string | null>(null);
|
||||||
);
|
const [botName, setBotName] = useState('');
|
||||||
const [selectedAdapter, setSelectedAdapter] = useState<string | null>(
|
|
||||||
restoredState.current?.selectedAdapter ?? null,
|
|
||||||
);
|
|
||||||
const [selectedRunner, setSelectedRunner] = useState<string | null>(
|
|
||||||
restoredState.current?.selectedRunner ?? null,
|
|
||||||
);
|
|
||||||
const [botName, setBotName] = useState(restoredState.current?.botName ?? '');
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const [botDescription, _setBotDescription] = useState(
|
const [botDescription, _setBotDescription] = useState('');
|
||||||
restoredState.current?.botDescription ?? '',
|
|
||||||
);
|
|
||||||
const [adapterConfig, setAdapterConfig] = useState<Record<string, unknown>>(
|
const [adapterConfig, setAdapterConfig] = useState<Record<string, unknown>>(
|
||||||
restoredState.current?.adapterConfig ?? {},
|
{},
|
||||||
);
|
|
||||||
const [runnerConfig, setRunnerConfig] = useState<Record<string, unknown>>(
|
|
||||||
restoredState.current?.runnerConfig ?? {},
|
|
||||||
);
|
|
||||||
const [createdBotUuid, setCreatedBotUuid] = useState<string | null>(
|
|
||||||
restoredState.current?.createdBotUuid ?? null,
|
|
||||||
);
|
);
|
||||||
|
const [runnerConfig, setRunnerConfig] = useState<Record<string, unknown>>({});
|
||||||
|
const [createdBotUuid, setCreatedBotUuid] = useState<string | null>(null);
|
||||||
|
const [webhookUrl, setWebhookUrl] = useState<string>('');
|
||||||
|
const [extraWebhookUrl, setExtraWebhookUrl] = useState<string>('');
|
||||||
|
|
||||||
// ---- Remote data ----
|
// ---- Remote data ----
|
||||||
const [adapters, setAdapters] = useState<Adapter[]>([]);
|
const [adapters, setAdapters] = useState<Adapter[]>([]);
|
||||||
@@ -149,29 +97,6 @@ export default function WizardPage() {
|
|||||||
const [isSavingBot, setIsSavingBot] = useState(false);
|
const [isSavingBot, setIsSavingBot] = useState(false);
|
||||||
const [botSaved, setBotSaved] = 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 ----
|
// ---- Fetch remote data ----
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
@@ -300,16 +225,34 @@ export default function WizardPage() {
|
|||||||
: selectedAdapter;
|
: selectedAdapter;
|
||||||
setBotName(defaultName);
|
setBotName(defaultName);
|
||||||
|
|
||||||
|
const defaultConfig = adapter
|
||||||
|
? getDefaultValues(adapter.spec.config)
|
||||||
|
: {};
|
||||||
|
|
||||||
const bot: Bot = {
|
const bot: Bot = {
|
||||||
name: defaultName,
|
name: defaultName,
|
||||||
description: '',
|
description: '',
|
||||||
adapter: selectedAdapter,
|
adapter: selectedAdapter,
|
||||||
adapter_config: {},
|
adapter_config: defaultConfig,
|
||||||
enable: false,
|
enable: false,
|
||||||
};
|
};
|
||||||
const resp = await httpClient.createBot(bot);
|
const resp = await httpClient.createBot(bot);
|
||||||
setCreatedBotUuid(resp.uuid);
|
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<string, unknown>
|
||||||
|
| 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
|
// Advance to Step 1
|
||||||
setCurrentStep(1);
|
setCurrentStep(1);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -338,6 +281,20 @@ export default function WizardPage() {
|
|||||||
enable: true,
|
enable: true,
|
||||||
});
|
});
|
||||||
setBotSaved(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<string, unknown>
|
||||||
|
| undefined;
|
||||||
|
setWebhookUrl((runtimeValues?.webhook_full_url as string) || '');
|
||||||
|
setExtraWebhookUrl(
|
||||||
|
(runtimeValues?.extra_webhook_full_url as string) || '',
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// Non-critical
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const apiErr = err as { msg?: string };
|
const apiErr = err as { msg?: string };
|
||||||
toast.error(
|
toast.error(
|
||||||
@@ -403,7 +360,6 @@ export default function WizardPage() {
|
|||||||
use_pipeline_uuid: pipelineResp.uuid,
|
use_pipeline_uuid: pipelineResp.uuid,
|
||||||
});
|
});
|
||||||
|
|
||||||
toast.success(t('wizard.createSuccess'));
|
|
||||||
setCurrentStep(3);
|
setCurrentStep(3);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const apiErr = err as { msg?: string };
|
const apiErr = err as { msg?: string };
|
||||||
@@ -442,11 +398,24 @@ export default function WizardPage() {
|
|||||||
|
|
||||||
// ---- Skip handler ----
|
// ---- Skip handler ----
|
||||||
const [showSkipConfirm, setShowSkipConfirm] = useState(false);
|
const [showSkipConfirm, setShowSkipConfirm] = useState(false);
|
||||||
|
const [isSkipping, setIsSkipping] = useState(false);
|
||||||
|
|
||||||
const handleSkipConfirm = useCallback(() => {
|
const handleSkipConfirm = useCallback(async () => {
|
||||||
clearWizardState();
|
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.push('/home');
|
||||||
}, [router]);
|
}, [router, t]);
|
||||||
|
|
||||||
// ---- Render ----
|
// ---- Render ----
|
||||||
|
|
||||||
@@ -563,6 +532,8 @@ export default function WizardPage() {
|
|||||||
isSavingBot={isSavingBot}
|
isSavingBot={isSavingBot}
|
||||||
botSaved={botSaved}
|
botSaved={botSaved}
|
||||||
onSaveBot={handleSaveBot}
|
onSaveBot={handleSaveBot}
|
||||||
|
webhookUrl={webhookUrl}
|
||||||
|
extraWebhookUrl={extraWebhookUrl}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{currentStep === 2 && (
|
{currentStep === 2 && (
|
||||||
@@ -623,21 +594,31 @@ export default function WizardPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Skip confirmation dialog */}
|
{/* Skip confirmation dialog */}
|
||||||
<AlertDialog open={showSkipConfirm} onOpenChange={setShowSkipConfirm}>
|
<Dialog open={showSkipConfirm} onOpenChange={setShowSkipConfirm}>
|
||||||
<AlertDialogContent>
|
<DialogContent>
|
||||||
<AlertDialogHeader>
|
<DialogHeader>
|
||||||
<AlertDialogTitle>{t('wizard.skip')}</AlertDialogTitle>
|
<DialogTitle>{t('wizard.skip')}</DialogTitle>
|
||||||
<AlertDialogDescription>
|
<DialogDescription>
|
||||||
{t('wizard.skipConfirmMessage')}
|
{t('wizard.skipConfirmMessage')}
|
||||||
</AlertDialogDescription>
|
</DialogDescription>
|
||||||
</AlertDialogHeader>
|
</DialogHeader>
|
||||||
<AlertDialogFooter>
|
<DialogFooter>
|
||||||
<AlertDialogAction onClick={handleSkipConfirm}>
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setShowSkipConfirm(false)}
|
||||||
|
disabled={isSkipping}
|
||||||
|
>
|
||||||
|
{t('wizard.prev')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSkipConfirm} disabled={isSkipping}>
|
||||||
|
{isSkipping && (
|
||||||
|
<Loader2 className="w-4 h-4 mr-1.5 animate-spin" />
|
||||||
|
)}
|
||||||
{t('wizard.skipConfirmOk')}
|
{t('wizard.skipConfirmOk')}
|
||||||
</AlertDialogAction>
|
</Button>
|
||||||
</AlertDialogFooter>
|
</DialogFooter>
|
||||||
</AlertDialogContent>
|
</DialogContent>
|
||||||
</AlertDialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -722,6 +703,8 @@ function StepBotConfig({
|
|||||||
isSavingBot,
|
isSavingBot,
|
||||||
botSaved,
|
botSaved,
|
||||||
onSaveBot,
|
onSaveBot,
|
||||||
|
webhookUrl,
|
||||||
|
extraWebhookUrl,
|
||||||
}: {
|
}: {
|
||||||
adapterConfigItems: IDynamicFormItemSchema[];
|
adapterConfigItems: IDynamicFormItemSchema[];
|
||||||
adapterConfigValues: Record<string, unknown>;
|
adapterConfigValues: Record<string, unknown>;
|
||||||
@@ -732,6 +715,8 @@ function StepBotConfig({
|
|||||||
isSavingBot: boolean;
|
isSavingBot: boolean;
|
||||||
botSaved: boolean;
|
botSaved: boolean;
|
||||||
onSaveBot: () => void;
|
onSaveBot: () => void;
|
||||||
|
webhookUrl: string;
|
||||||
|
extraWebhookUrl: string;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -787,7 +772,11 @@ function StepBotConfig({
|
|||||||
itemConfigList={adapterConfigItems}
|
itemConfigList={adapterConfigItems}
|
||||||
initialValues={adapterConfigValues as Record<string, object>}
|
initialValues={adapterConfigValues as Record<string, object>}
|
||||||
onSubmit={stableAdapterConfigCb}
|
onSubmit={stableAdapterConfigCb}
|
||||||
systemContext={{ is_wizard: true }}
|
systemContext={{
|
||||||
|
is_wizard: true,
|
||||||
|
webhook_url: webhookUrl,
|
||||||
|
extra_webhook_url: extraWebhookUrl,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -1037,10 +1026,23 @@ function StepDone() {
|
|||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleBack = useCallback(() => {
|
const [isCompleting, setIsCompleting] = useState(false);
|
||||||
clearWizardState();
|
|
||||||
|
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.push('/home/bots');
|
||||||
}, [router]);
|
}, [router, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex flex-col items-center justify-center h-full min-h-[400px]">
|
<div className="relative flex flex-col items-center justify-center h-full min-h-[400px]">
|
||||||
@@ -1065,7 +1067,8 @@ function StepDone() {
|
|||||||
<p className="text-muted-foreground mt-2 text-center max-w-md">
|
<p className="text-muted-foreground mt-2 text-center max-w-md">
|
||||||
{t('wizard.done.description')}
|
{t('wizard.done.description')}
|
||||||
</p>
|
</p>
|
||||||
<Button className="mt-6" onClick={handleBack}>
|
<Button className="mt-6" onClick={handleBack} disabled={isCompleting}>
|
||||||
|
{isCompleting && <Loader2 className="w-4 h-4 mr-1.5 animate-spin" />}
|
||||||
{t('wizard.done.backToWorkbench')}
|
{t('wizard.done.backToWorkbench')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ function SidebarInset({ className, ...props }: React.ComponentProps<'main'>) {
|
|||||||
<main
|
<main
|
||||||
data-slot="sidebar-inset"
|
data-slot="sidebar-inset"
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-background relative flex w-full flex-1 flex-col',
|
'bg-background relative flex w-full flex-1 flex-col min-w-0',
|
||||||
'md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2',
|
'md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2',
|
||||||
'dark:md:peer-data-[variant=inset]:border dark:md:peer-data-[variant=inset]:border-sidebar-border',
|
'dark:md:peer-data-[variant=inset]:border dark:md:peer-data-[variant=inset]:border-sidebar-border',
|
||||||
className,
|
className,
|
||||||
|
|||||||
@@ -1128,6 +1128,8 @@ const enUS = {
|
|||||||
botSaveSuccess: 'Bot configuration saved and enabled!',
|
botSaveSuccess: 'Bot configuration saved and enabled!',
|
||||||
createError: 'Failed to create resources',
|
createError: 'Failed to create resources',
|
||||||
spaceAuthError: 'Failed to initiate Space authorization',
|
spaceAuthError: 'Failed to initiate Space authorization',
|
||||||
|
skipSaveError: 'Failed to save skip status. Please try again.',
|
||||||
|
completeSaveError: 'Failed to save completion status. Please try again.',
|
||||||
step: {
|
step: {
|
||||||
platform: 'Platform',
|
platform: 'Platform',
|
||||||
botConfig: 'Bot Setup',
|
botConfig: 'Bot Setup',
|
||||||
|
|||||||
@@ -1102,6 +1102,8 @@
|
|||||||
botSaveSuccess: 'ボット設定が保存され、有効になりました!',
|
botSaveSuccess: 'ボット設定が保存され、有効になりました!',
|
||||||
createError: 'リソースの作成に失敗しました',
|
createError: 'リソースの作成に失敗しました',
|
||||||
spaceAuthError: 'Space 認証の開始に失敗しました',
|
spaceAuthError: 'Space 認証の開始に失敗しました',
|
||||||
|
skipSaveError: 'スキップ状態の保存に失敗しました。もう一度お試しください。',
|
||||||
|
completeSaveError: '完了状態の保存に失敗しました。もう一度お試しください。',
|
||||||
step: {
|
step: {
|
||||||
platform: 'プラットフォーム',
|
platform: 'プラットフォーム',
|
||||||
botConfig: 'ボット設定',
|
botConfig: 'ボット設定',
|
||||||
|
|||||||
@@ -1075,6 +1075,8 @@ const zhHans = {
|
|||||||
botSaveSuccess: '机器人配置已保存并启用!',
|
botSaveSuccess: '机器人配置已保存并启用!',
|
||||||
createError: '创建资源失败',
|
createError: '创建资源失败',
|
||||||
spaceAuthError: '无法发起 Space 授权',
|
spaceAuthError: '无法发起 Space 授权',
|
||||||
|
skipSaveError: '保存跳过状态失败,请重试。',
|
||||||
|
completeSaveError: '保存完成状态失败,请重试。',
|
||||||
step: {
|
step: {
|
||||||
platform: '平台接入',
|
platform: '平台接入',
|
||||||
botConfig: '机器人配置',
|
botConfig: '机器人配置',
|
||||||
|
|||||||
@@ -1042,6 +1042,8 @@ const zhHant = {
|
|||||||
botSaveSuccess: '機器人配置已儲存並啟用!',
|
botSaveSuccess: '機器人配置已儲存並啟用!',
|
||||||
createError: '建立資源失敗',
|
createError: '建立資源失敗',
|
||||||
spaceAuthError: '無法發起 Space 授權',
|
spaceAuthError: '無法發起 Space 授權',
|
||||||
|
skipSaveError: '儲存跳過狀態失敗,請重試。',
|
||||||
|
completeSaveError: '儲存完成狀態失敗,請重試。',
|
||||||
step: {
|
step: {
|
||||||
platform: '平台接入',
|
platform: '平台接入',
|
||||||
botConfig: '機器人配置',
|
botConfig: '機器人配置',
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
-webkit-text-size-adjust: 100%;
|
-webkit-text-size-adjust: 100%;
|
||||||
color: var(--color-fg-default);
|
color: var(--color-fg-default);
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans',
|
font-family:
|
||||||
Helvetica, Arial, sans-serif;
|
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica,
|
||||||
|
Arial, sans-serif;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
@@ -139,8 +140,9 @@
|
|||||||
font-size: 85%;
|
font-size: 85%;
|
||||||
background-color: var(--color-neutral-muted);
|
background-color: var(--color-neutral-muted);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas,
|
font-family:
|
||||||
'Liberation Mono', monospace;
|
ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono',
|
||||||
|
monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body pre {
|
.markdown-body pre {
|
||||||
|
|||||||
Reference in New Issue
Block a user