This is an automated email from the ASF dual-hosted git repository.

shuai pushed a commit to branch ai-ui
in repository https://gitbox.apache.org/repos/asf/answer.git

commit 8fa8a021cfa994d6bfa9fd7adabccefab142c4ed
Merge: 10c674e0 c509723f
Author: shuai <[email protected]>
AuthorDate: Thu Jan 22 17:01:24 2026 +0800

    feat: add apikeys and mcp pages

 cmd/wire_gen.go                                    |   72 +-
 docs/docs.go                                       | 1007 +++++++++++++-------
 docs/swagger.json                                  |  990 ++++++++++++-------
 docs/swagger.yaml                                  |  528 +++++++---
 go.mod                                             |    2 +-
 i18n/en_US.yaml                                    |   53 +-
 i18n/zh_CN.yaml                                    |   55 +-
 internal/base/constant/site_type.go                |   21 +-
 internal/base/middleware/auth.go                   |    4 +-
 internal/base/middleware/visit_img_auth.go         |    4 +-
 internal/controller/answer_controller.go           |    2 +-
 internal/controller/siteinfo_controller.go         |   22 +-
 internal/controller/template_controller.go         |    2 +-
 internal/controller_admin/siteinfo_controller.go   |  188 +++-
 internal/migrations/init.go                        |   85 +-
 internal/migrations/migrations.go                  |    1 +
 internal/migrations/v30.go                         |  396 ++++++++
 internal/router/answer_api_router.go               |   21 +-
 internal/schema/siteinfo_schema.go                 |  118 ++-
 internal/service/dashboard/dashboard_service.go    |   17 +-
 internal/service/mock/siteinfo_repo_mock.go        |  139 ++-
 internal/service/question_common/question.go       |    2 +-
 internal/service/siteinfo/siteinfo_service.go      |  106 ++-
 .../service/siteinfo_common/siteinfo_service.go    |   72 +-
 internal/service/tag_common/tag_common.go          |    8 +-
 internal/service/uploader/upload.go                |   32 +-
 ui/src/common/constants.ts                         |   63 +-
 ui/src/common/interface.ts                         |   52 +-
 ui/src/components/AccordionNav/index.tsx           |   65 +-
 ui/src/components/AdminSideNav/index.tsx           |   13 +-
 ui/src/components/BubbleAi/index.tsx               |    2 +-
 ui/src/components/SchemaForm/components/Switch.tsx |    3 +-
 ui/src/components/SchemaForm/index.tsx             |    8 +-
 ui/src/components/SchemaForm/types.ts              |    2 +-
 ui/src/components/TabNav/index.tsx                 |   26 +
 ui/src/components/index.ts                         |    2 +
 .../components/Action/index.tsx                    |    2 +-
 .../components/DetailModal/index.tsx               |    4 +-
 .../Admin/{Conversations => AiAssistant}/index.tsx |    2 +-
 ui/src/pages/Admin/AiSettings/index.tsx            |  226 +++--
 ui/src/pages/Admin/Answers/index.tsx               |    8 +-
 .../components/Action/index.tsx                    |   52 +-
 .../Apikeys/components/AddOrEditModal/index.tsx    |  165 ++++
 .../Apikeys/components/CreatedModal/index.tsx      |   36 +
 ui/src/pages/Admin/Apikeys/components/index.ts     |    5 +
 ui/src/pages/Admin/Apikeys/index.tsx               |  119 +++
 ui/src/pages/Admin/Branding/index.tsx              |   16 +-
 ui/src/pages/Admin/CssAndHtml/index.tsx            |   16 +-
 .../Dashboard/components/HealthStatus/index.tsx    |    6 +-
 ui/src/pages/Admin/Files/index.tsx                 |  261 +++++
 ui/src/pages/Admin/General/index.tsx               |   29 +-
 ui/src/pages/Admin/Interface/index.tsx             |   75 +-
 ui/src/pages/Admin/Login/index.tsx                 |   30 +-
 ui/src/pages/Admin/Mcp/index.tsx                   |   94 ++
 ui/src/pages/Admin/Plugins/Config/index.tsx        |   18 +-
 ui/src/pages/Admin/{Legal => Policies}/index.tsx   |   63 +-
 ui/src/pages/Admin/Privileges/index.tsx            |   32 +-
 ui/src/pages/Admin/QaSettings/index.tsx            |  132 +++
 ui/src/pages/Admin/Questions/index.tsx             |    4 +-
 ui/src/pages/Admin/Security/index.tsx              |  145 +++
 ui/src/pages/Admin/Seo/index.tsx                   |   16 +-
 ui/src/pages/Admin/Smtp/index.tsx                  |   16 +-
 ui/src/pages/Admin/TagsSettings/index.tsx          |  170 ++++
 ui/src/pages/Admin/Themes/index.tsx                |   16 +-
 ui/src/pages/Admin/Users/index.tsx                 |    3 +
 ui/src/pages/Admin/UsersSettings/index.tsx         |  132 +++
 ui/src/pages/Admin/Write/index.tsx                 |  473 ---------
 ui/src/pages/Admin/index.scss                      |    4 +
 ui/src/pages/Admin/index.tsx                       |   16 +-
 ui/src/pages/Layout/index.tsx                      |    5 +-
 ui/src/pages/Questions/Ask/index.tsx               |    6 +-
 ui/src/pages/Users/Settings/Profile/index.tsx      |   10 +-
 ui/src/router/routes.ts                            |   54 +-
 ui/src/services/admin/{question.ts => apikeys.ts}  |   27 +-
 ui/src/services/admin/index.ts                     |    3 +
 ui/src/services/admin/mcp.ts                       |   16 +
 ui/src/services/admin/question.ts                  |   10 +
 ui/src/services/admin/settings.ts                  |   38 +-
 .../Admin/index.scss => services/admin/tags.ts}    |   23 +-
 ui/src/services/admin/users.ts                     |   19 +
 ui/src/stores/index.ts                             |    4 +-
 ui/src/stores/interface.ts                         |    4 +-
 ui/src/stores/loginSetting.ts                      |    1 -
 ui/src/stores/siteInfo.ts                          |    3 +-
 ui/src/stores/{siteLegal.ts => siteSecurity.ts}    |   16 +-
 ui/src/stores/writeSetting.ts                      |   12 +-
 ui/src/utils/guard.ts                              |   15 +-
 87 files changed, 4856 insertions(+), 1979 deletions(-)

diff --cc i18n/en_US.yaml
index d8d31e44,ea607366..9a0d198b
--- a/i18n/en_US.yaml
+++ b/i18n/en_US.yaml
@@@ -1826,10 -1816,13 +1826,18 @@@ ui
      plugins: Plugins
      installed_plugins: Installed Plugins
      apperance: Appearance
-     AI: AI
-     conversations: Conversations
-     ai_settings: Settings
+     community: Community
+     advanced: Advanced
+     tags: Tags
+     rules: Rules
+     policies: Policies
+     security: Security
+     files: Files
++    apikeys: API Keys
++    intelligence: Intelligence
++    ai_assistant: AI Assistant
++    ai_settings: AI Settings
 +    mcp: MCP
    website_welcome: Welcome to {{site_name}}
    user_center:
      login: Login
@@@ -2305,37 -2298,6 +2313,70 @@@
        show_logs: Show logs
        status: Status
        title: Badges
++    apikeys:
++      title: API Keys
++      add_api_key: Add API Key
++      desc: Description
++      scope: Scope
++      key: Key
++      created: Created
++      last_used: Last used
++      add_or_edit_modal:
++        add_title: Add API Key
++        edit_title: Edit API Key
++        description: Description
++        description_required: Description is required.
++        scope: Scope
++        global: Global
++        read-only: Read-only
++      created_modal:
++        title: API key created
++        api_key: API key
++        description: This key will not be displayed again. Make sure you take 
a copy before continuing.
++      delete_modal:
++        title: Delete API Key
++        content: Any applications or scripts using this key will no longer be 
able to access the API. This is permanent!
 +    ai_settings:
-       title: Model
 +      enabled:
 +        label: AI enabled
 +        check: Enable AI features
++        text: The AI model must be configured correctly before it can be used.
 +      provider:
 +        label: Provider
 +      api_host:
 +        label: API host
 +        msg: API host is required
 +      api_key:
 +        label: API key
 +        check: Check
 +        check_success: "Connection successful."
 +        msg: API key is required
 +      model:
 +        label: Model
 +        msg: Model is required
 +      add_success: AI settings updated successfully.
 +    conversations:
-       title: Conversations
 +      topic: Topic
 +      helpful: Helpful
 +      unhelpful: Unhelpful
 +      created: Created
 +      action: Action
 +      empty: No conversations found.
 +      delete_modal:
 +        title: Delete conversation
 +        content: Are you sure you want to delete this conversation? This is 
permanent!
 +        delete_success: Conversation deleted successfully.
++    mcp:
++      mcp_server:
++        label: MCP server
++        switch: Enabled
++      type:
++        label: Type
++      url:
++        label: URL
++      http_header:
++        label: HTTP header
++        text: Please replace {key} with the API Key.
    form:
      optional: (optional)
      empty: cannot be empty
diff --cc i18n/zh_CN.yaml
index 0779e3a5,59ef41be..07589872
--- a/i18n/zh_CN.yaml
+++ b/i18n/zh_CN.yaml
@@@ -1788,10 -1778,13 +1788,18 @@@ ui
      plugins: 插件
      installed_plugins: 已安装插件
      apperance: 外观
-     AI: AI
-     conversations: 对话
-     ai_settings: 设置
+     community: 社区
+     advanced: 高级
+     tags: 标签
+     rules: 规则
+     policies: 政策
+     security: 安全
+     files: 文件
++    apikeys: API Keys
++    intelligence: 人工智能
++    ai_assistant: AI 助手
++    ai_settings: AI 设置
 +    mcp: MCP
    website_welcome: 欢迎来到 {{site_name}}
    user_center:
      login: 登录
@@@ -2266,37 -2259,6 +2274,70 @@@
        show_logs: 显示日志
        status: 状态
        title: 徽章
++    apikeys:
++      title: API Keys
++      add_api_key: 添加 API Key
++      desc: 描述
++      scope: 范围
++      key: Key
++      created: 创建时间
++      last_used: 最后使用时间
++      add_or_edit_modal:
++        add_title: 添加 API Key
++        edit_title: 编辑 API Key
++        description: 描述
++        description_required: 描述必填.
++        scope: 范围
++        global: 全局
++        read-only: 只读
++      created_modal:
++        title: API key 创建成功
++        api_key: API key
++        description: 此密钥将不会再显示。请确保在继续之前复制一份。
++      delete_modal:
++        title: 删除 API Key
++        content: 任何使用此密钥的应用程序或脚本将无法再访问 API。这是永久性的!
 +    ai_settings:
-       title: 模型
 +      enabled:
 +        label: AI 启用
 +        check: 启用 AI 功能
++        text: AI 模型必须正确配置才能使用。
 +      provider:
 +        label: 提供者
 +      api_host:
 +        label: API 主机
 +        msg: API 主机必填
 +      api_key:
 +        label: API Key
 +        check: 校验
 +        check_success: "连接成功."
 +        msg: API key 必填
 +      model:
 +        label: 模型
 +        msg: 模型必填
 +      add_success: AI 设置已成功更新.
 +    conversations:
-       title: 对话
 +      topic: 话题
 +      helpful: 有用的
 +      unhelpful: 无用的
 +      created: 创建于
 +      action: 操作
 +      empty: 没有会话记录。
 +      delete_modal:
 +        title: 删除对话
 +        content: 你确定要删除此对话吗?这是永久的!
 +        delete_success: 对话删除成功.
++    mcp:
++      mcp_server:
++        label: MCP 服务
++        switch: 启用
++      type:
++        label: 类型
++      url:
++        label: URL
++      http_header:
++        label: HTTP header
++        text: 请将 {key} 替换为 API Key。
    form:
      optional: (选填)
      empty: 不能为空
diff --cc ui/src/common/constants.ts
index c32b26e3,285da474..2e10f149
--- a/ui/src/common/constants.ts
+++ b/ui/src/common/constants.ts
@@@ -92,23 -97,19 +97,27 @@@ export const ADMIN_NAV_MENUS = 
    {
      name: 'contents',
      icon: 'file-earmark-text-fill',
-     children: [{ name: 'questions' }, { name: 'answers' }],
+     children: [
+       { name: 'questions', path: 'qa/questions', pathPrefix: 'qa/' },
+       { name: 'tags', path: 'tags/settings', pathPrefix: 'tags/' },
+     ],
    },
 +  {
-     name: 'AI',
++    name: 'intelligence',
 +    icon: 'robot',
 +    children: [
-       { name: 'conversations' },
 +      { name: 'ai_settings', path: 'ai-settings' },
++      { name: 'ai_assistant', path: 'ai-assistant' },
 +    ],
 +  },
    {
-     name: 'users',
+     name: 'community',
      icon: 'people-fill',
-   },
-   {
-     name: 'badges',
-     icon: 'award-fill',
+     children: [
+       { name: 'users', pathPrefix: 'users/' },
+       { name: 'badges' },
+       { name: 'rules', path: 'rules/privileges', pathPrefix: 'rules/' },
+     ],
    },
    {
      name: 'apperance',
@@@ -128,14 -130,11 +138,13 @@@
      icon: 'gear-fill',
      children: [
        { name: 'general' },
-       { name: 'interface' },
-       { name: 'smtp' },
-       { name: 'legal' },
-       { name: 'write' },
-       { name: 'seo' },
+       { name: 'security' },
+       { name: 'files' },
        { name: 'login' },
-       { name: 'privileges' },
+       { name: 'seo' },
+       { name: 'smtp' },
++      { name: 'apikeys' },
 +      { name: 'mcp' },
      ],
    },
    {
diff --cc ui/src/common/interface.ts
index 920942d7,aac89cce..308726e8
--- a/ui/src/common/interface.ts
+++ b/ui/src/common/interface.ts
@@@ -416,11 -421,12 +421,13 @@@ export interface SiteSettings 
    theme: AdminSettingsTheme;
    site_seo: AdminSettingsSeo;
    site_users: AdminSettingsUsers;
-   site_write: AdminSettingsWrite;
+   site_advanced: AdminSettingsWrite;
+   site_questions: AdminQuestionSetting;
+   site_tags: AdminTagsSetting;
    version: string;
    revision: string;
-   site_legal: AdminSettingsLegal;
+   site_security: AdminSettingsSecurity;
 +  ai_enabled: boolean;
  }
  
  export interface AdminSettingBranding {
@@@ -811,57 -809,14 +810,84 @@@ export interface BadgeDetailListRes 
    list: BadgeDetailListItem[];
  }
  
++export interface AdminApiKeysItem {
++  access_key: string;
++  created_at: number;
++  description: string;
++  id: number;
++  last_used_at: number;
++  scope: string;
++}
++
++export interface AddOrEditApiKeyParams {
++  description: string;
++  scope?: string;
++  id?: number;
++}
++
 +export interface AiConfig {
 +  enabled: boolean;
 +  chosen_provider: string;
 +  ai_providers: Array<{
 +    provider: string;
 +    api_host: string;
 +    api_key: string;
 +    model: string;
 +  }>;
 +}
 +
 +export interface AiProviderItem {
 +  name: string;
 +  display_name: string;
 +  default_api_host: string;
 +}
 +
 +export interface ConversationListItem {
 +  conversation_id: string;
 +  created_at: number;
 +  topic: string;
 +}
 +
 +export interface AdminConversationListItem {
 +  id: string;
 +  topic: string;
 +  helpful_count: number;
 +  unhelpful_count: number;
 +  created_at: number;
 +  user_info: UserInfoBase;
 +}
 +
 +export interface ConversationDetailItem {
 +  chat_completion_id: string;
 +  content: string;
 +  role: string;
 +  helpful: number;
 +  unhelpful: number;
 +  created_at: number;
 +}
 +
 +export interface ConversationDetail {
 +  conversation_id: string;
 +  created_at: number;
 +  records: ConversationDetailItem[];
 +  topic: string;
 +  updated_at: number;
 +}
 +
 +export interface VoteConversationParams {
 +  cancel: boolean;
 +  chat_completion_id: string;
 +  vote_type: 'helpful' | 'unhelpful';
 +}
++
+ export interface AdminQuestionSetting {
+   min_tags: number;
+   min_content: number;
+   restrict_answer: boolean;
+ }
+ 
+ export interface AdminTagsSetting {
+   recommend_tags: Tag[];
+   required_tag: boolean;
+   reserved_tags: Tag[];
+ }
diff --cc ui/src/components/BubbleAi/index.tsx
index 6c03580c,00000000..52663686
mode 100644,000000..100644
--- a/ui/src/components/BubbleAi/index.tsx
+++ b/ui/src/components/BubbleAi/index.tsx
@@@ -1,240 -1,0 +1,240 @@@
 +import { FC, useEffect, useState, useRef } from 'react';
 +import { Button } from 'react-bootstrap';
 +import { useTranslation } from 'react-i18next';
 +
 +import { marked } from 'marked';
 +import copy from 'copy-to-clipboard';
 +
- import { voteConversation } from '@/enterprise/services';
++import { voteConversation } from '@/services';
 +import { Icon, htmlRender } from '@/components';
 +
 +interface IProps {
 +  canType?: boolean;
 +  chatId: string;
 +  isLast: boolean;
 +  isCompleted: boolean;
 +  content: string;
 +  minHeight?: number;
 +  actionData: {
 +    helpful: number;
 +    unhelpful: number;
 +  };
 +}
 +
 +const BubbleAi: FC<IProps> = ({
 +  canType = false,
 +  isLast,
 +  isCompleted,
 +  content,
 +  chatId = '',
 +  actionData,
 +  minHeight = 0,
 +}) => {
 +  const { t } = useTranslation('translation', { keyPrefix: 'ai_assistant' });
 +  const [displayContent, setDisplayContent] = useState('');
 +  const [copyText, setCopyText] = useState<string>(t('copy'));
 +  const [isHelpful, setIsHelpful] = useState(false);
 +  const [isUnhelpful, setIsUnhelpful] = useState(false);
 +  const [canShowAction, setCanShowAction] = useState(false);
 +  const typewriterRef = useRef<{
 +    timer: NodeJS.Timeout | null;
 +    index: number;
 +    isTyping: boolean;
 +  }>({
 +    timer: null,
 +    index: 0,
 +    isTyping: false,
 +  });
 +  const fmtContainer = useRef<HTMLDivElement>(null);
 +  // 添加ref用于ScrollIntoView
 +  const containerRef = useRef<HTMLDivElement>(null);
 +
 +  const handleCopy = () => {
 +    const res = copy(displayContent);
 +    if (res) {
 +      setCopyText(t('copied', { keyPrefix: 'messages' }));
 +      setTimeout(() => {
 +        setCopyText(t('copy'));
 +      }, 1200);
 +    }
 +  };
 +
 +  const handleVote = (voteType: 'helpful' | 'unhelpful') => {
 +    const isCancel =
 +      (voteType === 'helpful' && isHelpful) ||
 +      (voteType === 'unhelpful' && isUnhelpful);
 +    voteConversation({
 +      chat_completion_id: chatId,
 +      cancel: isCancel,
 +      vote_type: voteType,
 +    }).then(() => {
 +      setIsHelpful(voteType === 'helpful' && !isCancel);
 +      setIsUnhelpful(voteType === 'unhelpful' && !isCancel);
 +    });
 +  };
 +
 +  useEffect(() => {
 +    if ((!canType || !isLast) && content) {
 +      // 如果不是最后一个消息,直接返回,不进行打字效果
 +      if (typewriterRef.current.timer) {
 +        clearInterval(typewriterRef.current.timer);
 +        typewriterRef.current.timer = null;
 +      }
 +      setDisplayContent(content);
 +      setCanShowAction(true);
 +      typewriterRef.current.timer = null;
 +      typewriterRef.current.isTyping = false;
 +      return;
 +    }
 +    // 当内容变化时,清理之前的计时器
 +    if (typewriterRef.current.timer) {
 +      clearInterval(typewriterRef.current.timer);
 +      typewriterRef.current.timer = null;
 +    }
 +
 +    // 如果内容为空,则直接返回
 +    if (!content) {
 +      setDisplayContent('');
 +      return;
 +    }
 +
 +    // 如果内容比当前显示的短,则重置
 +    if (content.length < displayContent.length) {
 +      setDisplayContent('');
 +      typewriterRef.current.index = 0;
 +    }
 +
 +    // 如果内容与显示内容相同,不需要做任何事
 +    if (content === displayContent) {
 +      return;
 +    }
 +
 +    typewriterRef.current.isTyping = true;
 +
 +    // start typing animation
 +    typewriterRef.current.timer = setInterval(() => {
 +      const currentIndex = typewriterRef.current.index;
 +      if (currentIndex < content.length) {
 +        const remainingLength = content.length - currentIndex;
 +        const baseRandomNum = Math.floor(Math.random() * 3) + 2;
 +        let randomNum = Math.min(baseRandomNum, remainingLength);
 +
 +        // 简单的单词边界检查(可选)
 +        const nextChar = content[currentIndex + randomNum];
 +        const prevChar = content[currentIndex + randomNum - 1];
 +
 +        // 如果下一个字符是字母,当前字符也是字母,尝试调整到空格处
 +        if (
 +          nextChar &&
 +          /[a-zA-Z]/.test(nextChar) &&
 +          /[a-zA-Z]/.test(prevChar)
 +        ) {
 +          // 向前找1-2个字符,看看有没有空格
 +          for (
 +            let i = 1;
 +            i <= 2 && currentIndex + randomNum - i > currentIndex;
 +            i += 1
 +          ) {
 +            if (content[currentIndex + randomNum - i] === ' ') {
 +              randomNum = randomNum - i + 1;
 +              break;
 +            }
 +          }
 +          // 向后找1-2个字符,看看有没有空格
 +          for (
 +            let i = 1;
 +            i <= 2 && currentIndex + randomNum + i < content.length;
 +            i += 1
 +          ) {
 +            if (content[currentIndex + randomNum + i] === ' ') {
 +              randomNum = randomNum + i + 1;
 +              break;
 +            }
 +          }
 +        }
 +
 +        const nextIndex = currentIndex + randomNum;
 +        const newContent = content.substring(0, nextIndex);
 +        setDisplayContent(newContent);
 +        typewriterRef.current.index = nextIndex;
 +        setCanShowAction(false);
 +      } else {
 +        clearInterval(typewriterRef.current.timer as NodeJS.Timeout);
 +        typewriterRef.current.timer = null;
 +        typewriterRef.current.isTyping = false;
 +        setCanShowAction(false);
 +      }
 +    }, 30);
 +
 +    // eslint-disable-next-line consistent-return
 +    return () => {
 +      if (typewriterRef.current.timer) {
 +        clearInterval(typewriterRef.current.timer);
 +        typewriterRef.current.timer = null;
 +      }
 +    };
 +  }, [content, isCompleted]);
 +
 +  useEffect(() => {
 +    setIsHelpful(actionData.helpful > 0);
 +    setIsUnhelpful(actionData.unhelpful > 0);
 +  }, [actionData]);
 +
 +  useEffect(() => {
 +    if (fmtContainer.current && isCompleted) {
 +      htmlRender(fmtContainer.current, {
 +        copySuccessText: t('copied', { keyPrefix: 'messages' }),
 +        copyText: t('copy', { keyPrefix: 'messages' }),
 +      });
 +      const links = fmtContainer.current.querySelectorAll('a');
 +      links.forEach((link) => {
 +        link.setAttribute('target', '_blank');
 +      });
 +      setCanShowAction(true);
 +    }
 +  }, [isCompleted, fmtContainer.current]);
 +
 +  return (
 +    <div
 +      className="rounded bubble-ai"
 +      ref={containerRef}
 +      style={{ minHeight: `${minHeight}px`, overflowAnchor: 'none' }}>
 +      <div id={chatId}>
 +        <div
 +          className="fmt text-break text-wrap"
 +          ref={fmtContainer}
 +          style={{ transition: 'all 0.2s ease' }}
 +          dangerouslySetInnerHTML={{ __html: marked.parse(displayContent) }}
 +        />
 +
 +        {canShowAction && (
 +          <div className="action">
 +            <Button
 +              variant="link"
 +              className="p-0 link-secondary small me-3"
 +              onClick={handleCopy}>
 +              <Icon name="copy" />
 +              <span className="ms-1">{copyText}</span>
 +            </Button>
 +            <Button
 +              variant="link"
 +              className={`p-0 small me-3 ${isHelpful ? 'link-primary active' 
: 'link-secondary'}`}
 +              onClick={() => handleVote('helpful')}>
 +              <Icon name="hand-thumbs-up-fill" />
 +              <span className="ms-1">Helpful</span>
 +            </Button>
 +            <Button
 +              variant="link"
 +              className={`p-0 small me-3 ${isUnhelpful ? 'link-primary 
active' : 'link-secondary'}`}
 +              onClick={() => handleVote('unhelpful')}>
 +              <Icon name="hand-thumbs-down-fill" />
 +              <span className="ms-1">Unhelpful</span>
 +            </Button>
 +          </div>
 +        )}
 +      </div>
 +    </div>
 +  );
 +};
 +
 +export default BubbleAi;
diff --cc ui/src/components/index.ts
index e8afa157,416db241..5c81739b
--- a/ui/src/components/index.ts
+++ b/ui/src/components/index.ts
@@@ -64,9 -64,7 +64,10 @@@ import CardBadge from './CardBadge'
  import PinList from './PinList';
  import MobileSideNav from './MobileSideNav';
  import AdminSideNav from './AdminSideNav';
 +import BubbleAi from './BubbleAi';
 +import BubbleUser from './BubbleUser';
 +import Sender from './Sender';
+ import TabNav from './TabNav';
  
  export {
    Avatar,
@@@ -118,8 -116,6 +119,9 @@@
    PinList,
    MobileSideNav,
    AdminSideNav,
 +  BubbleAi,
 +  BubbleUser,
 +  Sender,
+   TabNav,
  };
  export type { EditorRef, JSONSchema, UISchema };
diff --cc ui/src/pages/Admin/AiAssistant/components/Action/index.tsx
index 577d16c3,00000000..c9702ea6
mode 100644,000000..100644
--- a/ui/src/pages/Admin/AiAssistant/components/Action/index.tsx
+++ b/ui/src/pages/Admin/AiAssistant/components/Action/index.tsx
@@@ -1,71 -1,0 +1,71 @@@
 +/*
 + * Licensed to the Apache Software Foundation (ASF) under one
 + * or more contributor license agreements.  See the NOTICE file
 + * distributed with this work for additional information
 + * regarding copyright ownership.  The ASF licenses this file
 + * to you under the Apache License, Version 2.0 (the
 + * "License"); you may not use this file except in compliance
 + * with the License.  You may obtain a copy of the License at
 + *
 + *   http://www.apache.org/licenses/LICENSE-2.0
 + *
 + * Unless required by applicable law or agreed to in writing,
 + * software distributed under the License is distributed on an
 + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 + * KIND, either express or implied.  See the License for the
 + * specific language governing permissions and limitations
 + * under the License.
 + */
 +import { Dropdown } from 'react-bootstrap';
 +import { useTranslation } from 'react-i18next';
 +
 +import { Modal, Icon } from '@/components';
- import { deleteAdminConversation } from '@/enterprise/services';
++import { deleteAdminConversation } from '@/services';
 +import { useToast } from '@/hooks';
 +
 +interface Props {
 +  id: string;
 +  refreshList?: () => void;
 +}
 +const ConversationsOperation = ({ id, refreshList }: Props) => {
 +  const { t } = useTranslation('translation', {
 +    keyPrefix: 'admin.conversations',
 +  });
 +  const toast = useToast();
 +
 +  const handleAction = (eventKey: string | null) => {
 +    if (eventKey === 'delete') {
 +      Modal.confirm({
 +        title: t('delete_modal.title'),
 +        content: t('delete_modal.content'),
 +        cancelBtnVariant: 'link',
 +        confirmBtnVariant: 'danger',
 +        confirmText: t('delete', { keyPrefix: 'btns' }),
 +        onConfirm: () => {
 +          deleteAdminConversation(id).then(() => {
 +            refreshList?.();
 +            toast.onShow({
 +              variant: 'success',
 +              msg: t('delete_modal.delete_success'),
 +            });
 +          });
 +        },
 +      });
 +    }
 +  };
 +
 +  return (
 +    <Dropdown onSelect={handleAction}>
 +      <Dropdown.Toggle variant="link" className="no-toggle p-0 lh-1">
 +        <Icon name="three-dots-vertical" title={t('action')} />
 +      </Dropdown.Toggle>
 +      <Dropdown.Menu align="end">
 +        <Dropdown.Item eventKey="delete">
 +          {t('delete', { keyPrefix: 'btns' })}
 +        </Dropdown.Item>
 +      </Dropdown.Menu>
 +    </Dropdown>
 +  );
 +};
 +
 +export default ConversationsOperation;
diff --cc ui/src/pages/Admin/AiAssistant/components/DetailModal/index.tsx
index 2d408c36,00000000..c308c980
mode 100644,000000..100644
--- a/ui/src/pages/Admin/AiAssistant/components/DetailModal/index.tsx
+++ b/ui/src/pages/Admin/AiAssistant/components/DetailModal/index.tsx
@@@ -1,67 -1,0 +1,67 @@@
 +import { FC, memo } from 'react';
 +import { Button, Modal } from 'react-bootstrap';
 +import { useTranslation } from 'react-i18next';
 +
- import { BubbleAi, BubbleUser } from '@/enterprise/components';
- import { useQueryAdminConversationDetail } from '@/enterprise/services';
++import { BubbleAi, BubbleUser } from '@/components';
++import { useQueryAdminConversationDetail } from '@/services';
 +
 +interface IProps {
 +  visible: boolean;
 +  id: string;
 +  onClose?: () => void;
 +}
 +
 +const Index: FC<IProps> = ({ visible, id, onClose }) => {
 +  const { t } = useTranslation('translation', {
 +    keyPrefix: 'admin.conversations',
 +  });
 +
 +  const { data: conversationDetail } = useQueryAdminConversationDetail(id);
 +
 +  const handleClose = () => {
 +    onClose?.();
 +  };
 +  return (
 +    <Modal show={visible} size="lg" centered onHide={handleClose}>
 +      <Modal.Header closeButton>
 +        <div style={{ maxWidth: '85%' }} className="text-truncate">
 +          {conversationDetail?.topic}
 +        </div>
 +      </Modal.Header>
 +      <Modal.Body className="overflow-y-auto" style={{ maxHeight: '70vh' }}>
 +        {conversationDetail?.records.map((item, index) => {
 +          const isLastMessage =
 +            index === Number(conversationDetail?.records.length) - 1;
 +          return (
 +            <div
 +              key={`${item.chat_completion_id}-${item.role}`}
 +              className={`${isLastMessage ? '' : 'mb-4'}`}>
 +              {item.role === 'user' ? (
 +                <BubbleUser content={item.content} />
 +              ) : (
 +                <BubbleAi
 +                  canType={false}
 +                  chatId={item.chat_completion_id}
 +                  isLast={false}
 +                  isCompleted
 +                  content={item.content}
 +                  actionData={{
 +                    helpful: item.helpful,
 +                    unhelpful: item.unhelpful,
 +                  }}
 +                />
 +              )}
 +            </div>
 +          );
 +        })}
 +      </Modal.Body>
 +      <Modal.Footer>
 +        <Button variant="link" onClick={handleClose}>
 +          {t('close', { keyPrefix: 'btns' })}
 +        </Button>
 +      </Modal.Footer>
 +    </Modal>
 +  );
 +};
 +
 +export default memo(Index);
diff --cc ui/src/pages/Admin/AiAssistant/index.tsx
index ed1ee333,00000000..f3aabb55
mode 100644,000000..100644
--- a/ui/src/pages/Admin/AiAssistant/index.tsx
+++ b/ui/src/pages/Admin/AiAssistant/index.tsx
@@@ -1,111 -1,0 +1,111 @@@
 +import { useState } from 'react';
 +import { Table, Button } from 'react-bootstrap';
 +import { useTranslation } from 'react-i18next';
 +import { useSearchParams } from 'react-router-dom';
 +
 +import { BaseUserCard, FormatTime, Pagination, Empty } from '@/components';
 +import { useQueryAdminConversationList } from '@/services';
 +
 +import DetailModal from './components/DetailModal';
 +import Action from './components/Action';
 +
 +const Index = () => {
 +  const { t } = useTranslation('translation', {
 +    keyPrefix: 'admin.conversations',
 +  });
 +  const [urlSearchParams] = useSearchParams();
 +  const curPage = Number(urlSearchParams.get('page') || '1');
 +  const PAGE_SIZE = 20;
 +  const [detailModal, setDetailModal] = useState({
 +    visible: false,
 +    id: '',
 +  });
 +  const {
 +    data: conversations,
 +    isLoading,
 +    mutate: refreshList,
 +  } = useQueryAdminConversationList({
 +    page: curPage,
 +    page_size: PAGE_SIZE,
 +  });
 +
 +  const handleShowDetailModal = (data) => {
 +    setDetailModal({
 +      visible: true,
 +      id: data.id,
 +    });
 +  };
 +
 +  const handleHideDetailModal = () => {
 +    setDetailModal({
 +      visible: false,
 +      id: '',
 +    });
 +  };
 +
 +  return (
 +    <div className="d-flex flex-column flex-grow-1 position-relative">
-       <h3 className="mb-4">{t('title')}</h3>
++      <h3 className="mb-4">{t('ai_assistant', { keyPrefix: 'nav_menus' 
})}</h3>
 +      <Table responsive="md">
 +        <thead>
 +          <tr>
 +            <th className="min-w-15">{t('topic')}</th>
 +            <th style={{ width: '10%' }}>{t('helpful')}</th>
 +            <th style={{ width: '10%' }}>{t('unhelpful')}</th>
 +            <th style={{ width: '20%' }}>{t('created')}</th>
 +            <th style={{ width: '10%' }} className="text-end">
 +              {t('action')}
 +            </th>
 +          </tr>
 +        </thead>
 +        <tbody className="align-middle">
 +          {conversations?.list.map((item) => {
 +            return (
 +              <tr key={item.id}>
 +                <td>
 +                  <Button
 +                    variant="link"
 +                    className="p-0 text-decoration-none text-truncate 
max-w-30"
 +                    onClick={() => handleShowDetailModal(item)}>
 +                    {item.topic}
 +                  </Button>
 +                </td>
 +                <td>{item.helpful_count}</td>
 +                <td>{item.unhelpful_count}</td>
 +                <td>
 +                  <div className="vstack">
 +                    <BaseUserCard data={item.user_info} avatarSize="20px" />
 +                    <FormatTime
 +                      className="small text-secondary"
 +                      time={item.created_at}
 +                    />
 +                  </div>
 +                </td>
 +                <td className="text-end">
 +                  <Action id={item.id} refreshList={refreshList} />
 +                </td>
 +              </tr>
 +            );
 +          })}
 +        </tbody>
 +      </Table>
 +      {!isLoading && Number(conversations?.count) <= 0 && (
 +        <Empty>{t('empty')}</Empty>
 +      )}
 +
 +      <div className="mt-4 mb-2 d-flex justify-content-center">
 +        <Pagination
 +          currentPage={curPage}
 +          totalSize={conversations?.count || 0}
 +          pageSize={PAGE_SIZE}
 +        />
 +      </div>
 +      <DetailModal
 +        visible={detailModal.visible}
 +        id={detailModal.id}
 +        onClose={handleHideDetailModal}
 +      />
 +    </div>
 +  );
 +};
 +export default Index;
diff --cc ui/src/pages/Admin/AiSettings/index.tsx
index 368b01a0,00000000..2bc30fef
mode 100644,000000..100644
--- a/ui/src/pages/Admin/AiSettings/index.tsx
+++ b/ui/src/pages/Admin/AiSettings/index.tsx
@@@ -1,437 -1,0 +1,467 @@@
 +import { useEffect, useState, useRef } from 'react';
 +import { Form, InputGroup, Button } from 'react-bootstrap';
 +import { useTranslation } from 'react-i18next';
 +
 +import {
 +  getAiConfig,
 +  useQueryAiProvider,
 +  checkAiConfig,
 +  saveAiConfig,
 +} from '@/services';
 +import { aiControlStore } from '@/stores';
 +import { handleFormError } from '@/utils';
 +import { useToast } from '@/hooks';
 +import * as Type from '@/common/interface';
 +
 +const Index = () => {
 +  const { t } = useTranslation('translation', {
 +    keyPrefix: 'admin.ai_settings',
 +  });
 +  const toast = useToast();
 +  const historyConfigRef = useRef<Type.AiConfig>();
 +  // const [historyConfig, setHistoryConfig] = useState<Type.AiConfig>();
 +  const { data: aiProviders } = useQueryAiProvider();
 +
 +  const [formData, setFormData] = useState({
 +    enabled: {
 +      value: false,
 +      isInvalid: false,
 +      errorMsg: '',
 +    },
 +    provider: {
 +      value: '',
 +      isInvalid: false,
 +      errorMsg: '',
 +    },
 +    api_host: {
 +      value: '',
 +      isInvalid: false,
 +      errorMsg: '',
 +    },
 +    api_key: {
 +      value: '',
 +      isInvalid: false,
 +      isValid: false,
 +      errorMsg: '',
 +    },
 +    model: {
 +      value: '',
 +      isInvalid: false,
 +      errorMsg: '',
 +    },
 +  });
 +  const [apiHostPlaceholder, setApiHostPlaceholder] = useState('');
 +  const [modelsData, setModels] = useState<{ id: string }[]>([]);
 +  const [isChecking, setIsChecking] = useState(false);
 +
 +  const getCurrentProviderData = (provider) => {
 +    const findHistoryProvider =
 +      historyConfigRef.current?.ai_providers.find(
 +        (v) => v.provider === provider,
 +      ) || historyConfigRef.current?.ai_providers[0];
 +
 +    return findHistoryProvider;
 +  };
 +
 +  const checkAiConfigData = (data) => {
 +    const params = data || {
 +      api_host: formData.api_host.value || apiHostPlaceholder,
 +      api_key: formData.api_key.value,
 +    };
 +    setIsChecking(true);
 +
 +    checkAiConfig(params)
 +      .then((res) => {
 +        setModels(res);
 +        const findHistoryProvider = getCurrentProviderData(
 +          formData.provider.value,
 +        );
 +
 +        setIsChecking(false);
++
 +        if (!data) {
 +          setFormData({
 +            ...formData,
 +            api_key: {
 +              ...formData.api_key,
 +              errorMsg: t('api_key.check_success'),
 +              isInvalid: false,
 +              isValid: true,
 +            },
 +            model: {
 +              value: findHistoryProvider?.model || res[0].id,
 +              errorMsg: '',
 +              isInvalid: false,
 +            },
 +          });
 +        }
 +      })
 +      .catch((err) => {
 +        console.error('Checking AI config:', err);
 +        setIsChecking(false);
 +      });
 +  };
 +
 +  const handleProviderChange = (value) => {
 +    const findHistoryProvider = getCurrentProviderData(value);
 +    setFormData({
 +      ...formData,
 +      provider: {
 +        value,
 +        isInvalid: false,
 +        errorMsg: '',
 +      },
 +      api_host: {
 +        value: findHistoryProvider?.api_host || '',
 +        isInvalid: false,
 +        errorMsg: '',
 +      },
 +      api_key: {
 +        value: findHistoryProvider?.api_key || '',
 +        isInvalid: false,
 +        isValid: false,
 +        errorMsg: '',
 +      },
 +      model: {
 +        value: findHistoryProvider?.model || '',
 +        isInvalid: false,
 +        errorMsg: '',
 +      },
 +    });
 +    const provider = aiProviders?.find((item) => item.name === value);
 +    const host = findHistoryProvider?.api_host || provider?.default_api_host;
 +    if (findHistoryProvider?.model) {
 +      checkAiConfigData({
 +        api_host: host,
 +        api_key: findHistoryProvider.api_key,
 +      });
 +    } else {
 +      setModels([]);
 +    }
 +  };
 +
 +  const handleValueChange = (value) => {
 +    setFormData((prev) => ({
 +      ...prev,
 +      ...value,
 +    }));
 +  };
 +
 +  const checkValidate = () => {
 +    let bol = true;
 +
 +    const { api_host, api_key, model } = formData;
 +
 +    if (!api_host.value) {
 +      bol = false;
 +      formData.api_host = {
 +        value: '',
 +        isInvalid: true,
 +        errorMsg: t('api_host.msg'),
 +      };
 +    }
 +
 +    if (!api_key.value) {
 +      bol = false;
 +      formData.api_key = {
 +        value: '',
 +        isInvalid: true,
 +        isValid: false,
 +        errorMsg: t('api_key.msg'),
 +      };
 +    }
 +
 +    if (!model.value) {
 +      bol = false;
 +      formData.model = {
 +        value: '',
 +        isInvalid: true,
 +        errorMsg: t('model.msg'),
 +      };
 +    }
 +
 +    setFormData({
 +      ...formData,
 +    });
 +
 +    return bol;
 +  };
 +
 +  const handleSubmit = (e) => {
 +    e.preventDefault();
 +    if (!checkValidate()) {
 +      return;
 +    }
 +    const newProviders = historyConfigRef.current?.ai_providers.map((v) => {
 +      if (v.provider === formData.provider.value) {
 +        return {
 +          provider: formData.provider.value,
 +          api_host: formData.api_host.value,
 +          api_key: formData.api_key.value,
 +          model: formData.model.value,
 +        };
 +      }
 +      return v;
 +    });
 +
 +    const params = {
 +      enabled: formData.enabled.value,
 +      chosen_provider: formData.provider.value,
 +      ai_providers: newProviders,
 +    };
 +    saveAiConfig(params)
 +      .then(() => {
 +        aiControlStore.getState().update({
 +          ai_enabled: formData.enabled.value,
 +        });
 +
 +        historyConfigRef.current = {
 +          ...params,
 +          ai_providers: params.ai_providers || [],
 +        };
 +
 +        toast.onShow({
 +          msg: t('add_success'),
 +          variant: 'success',
 +        });
 +      })
 +      .catch((err) => {
 +        const data = handleFormError(err, formData);
 +        setFormData({ ...data });
 +        const ele = document.getElementById(err.list[0].error_field);
 +        ele?.scrollIntoView({ behavior: 'smooth', block: 'center' });
 +      });
 +  };
 +
 +  const getAiConfigData = async () => {
 +    const aiConfig = await getAiConfig();
 +    historyConfigRef.current = aiConfig;
 +
 +    const currentAiConfig = getCurrentProviderData(aiConfig.chosen_provider);
 +    if (currentAiConfig?.model) {
 +      const provider = aiProviders?.find(
 +        (item) => item.name === formData.provider.value,
 +      );
 +      const host = currentAiConfig.api_host || provider?.default_api_host;
 +      checkAiConfigData({
 +        api_host: host,
 +        api_key: currentAiConfig.api_key,
 +      });
 +    }
 +
 +    setFormData({
 +      enabled: {
 +        value: aiConfig.enabled || false,
 +        isInvalid: false,
 +        errorMsg: '',
 +      },
 +      provider: {
 +        value: currentAiConfig?.provider || '',
 +        isInvalid: false,
 +        errorMsg: '',
 +      },
 +      api_host: {
 +        value: currentAiConfig?.api_host || '',
 +        isInvalid: false,
 +        errorMsg: '',
 +      },
 +      api_key: {
 +        value: currentAiConfig?.api_key || '',
 +        isInvalid: false,
 +        isValid: false,
 +        errorMsg: '',
 +      },
 +      model: {
 +        value: currentAiConfig?.model || '',
 +        isInvalid: false,
 +        errorMsg: '',
 +      },
 +    });
 +  };
 +
 +  useEffect(() => {
 +    getAiConfigData();
 +  }, []);
 +
 +  useEffect(() => {
 +    if (formData.provider.value) {
 +      const provider = aiProviders?.find(
 +        (item) => item.name === formData.provider.value,
 +      );
 +      if (provider) {
 +        setApiHostPlaceholder(provider.default_api_host || '');
 +      }
 +    }
 +    if (!formData.provider.value && aiProviders) {
 +      setFormData((prev) => ({
 +        ...prev,
 +        provider: {
 +          ...prev.provider,
 +          value: aiProviders[0].name,
 +        },
 +      }));
 +    }
 +  }, [aiProviders, formData]);
 +
 +  return (
 +    <div>
-       <h3 className="mb-4">{t('title')}</h3>
-       <Form noValidate onSubmit={handleSubmit}>
-         <Form.Group className="mb-3" controlId="enabled">
-           <Form.Label>{t('enabled.label')}</Form.Label>
-           <Form.Switch
-             type="switch"
-             id="enabled"
-             label={t('enabled.check')}
-             checked={formData.enabled.value}
-             onChange={(e) =>
-               handleValueChange({
-                 enabled: {
-                   value: e.target.checked,
-                   errorMsg: '',
-                   isInvalid: false,
-                 },
-               })
-             }
-           />
-           <Form.Control.Feedback type="invalid">
-             {formData.enabled.errorMsg}
-           </Form.Control.Feedback>
-         </Form.Group>
- 
-         <Form.Group className="mb-3" controlId="provider">
-           <Form.Label>{t('provider.label')}</Form.Label>
-           <Form.Select
-             isInvalid={formData.provider.isInvalid}
-             value={formData.provider.value}
-             onChange={(e) => handleProviderChange(e.target.value)}>
-             {aiProviders?.map((provider) => (
-               <option key={provider.name} value={provider.name}>
-                 {provider.display_name}
-               </option>
-             ))}
-           </Form.Select>
-           <Form.Control.Feedback type="invalid">
-             {formData.provider.errorMsg}
-           </Form.Control.Feedback>
-         </Form.Group>
- 
-         <Form.Group className="mb-3" controlId="api_host">
-           <Form.Label>{t('api_host.label')}</Form.Label>
-           <Form.Control
-             type="text"
-             autoComplete="off"
-             placeholder={apiHostPlaceholder}
-             isInvalid={formData.api_host.isInvalid}
-             value={formData.api_host.value}
-             onChange={(e) =>
-               handleValueChange({
-                 api_host: {
-                   value: e.target.value,
-                   errorMsg: '',
-                   isInvalid: false,
-                 },
-               })
-             }
-           />
-           <Form.Control.Feedback type="invalid">
-             {formData.api_host.errorMsg}
-           </Form.Control.Feedback>
-         </Form.Group>
- 
-         <Form.Group className="mb-3" controlId="api_key">
-           <Form.Label>{t('api_key.label')}</Form.Label>
-           <InputGroup>
++      <h3 className="mb-4">{t('ai_settings', { keyPrefix: 'nav_menus' })}</h3>
++      <div className="max-w-748">
++        <Form noValidate onSubmit={handleSubmit}>
++          <Form.Group className="mb-3" controlId="enabled">
++            <Form.Label>{t('enabled.label')}</Form.Label>
++            <Form.Switch
++              type="switch"
++              id="enabled"
++              label={t('enabled.check')}
++              checked={formData.enabled.value}
++              onChange={(e) =>
++                handleValueChange({
++                  enabled: {
++                    value: e.target.checked,
++                    errorMsg: '',
++                    isInvalid: false,
++                  },
++                })
++              }
++            />
++            <Form.Text className="text-muted">{t('enabled.text')}</Form.Text>
++            <Form.Control.Feedback type="invalid">
++              {formData.enabled.errorMsg}
++            </Form.Control.Feedback>
++          </Form.Group>
++
++          <Form.Group className="mb-3" controlId="provider">
++            <Form.Label>{t('provider.label')}</Form.Label>
++            <Form.Select
++              isInvalid={formData.provider.isInvalid}
++              value={formData.provider.value}
++              onChange={(e) => handleProviderChange(e.target.value)}>
++              {aiProviders?.map((provider) => (
++                <option key={provider.name} value={provider.name}>
++                  {provider.display_name}
++                </option>
++              ))}
++            </Form.Select>
++            <Form.Control.Feedback type="invalid">
++              {formData.provider.errorMsg}
++            </Form.Control.Feedback>
++          </Form.Group>
++
++          <Form.Group className="mb-3" controlId="api_host">
++            <Form.Label>{t('api_host.label')}</Form.Label>
 +            <Form.Control
-               type="password"
-               autoComplete="new-password"
-               isInvalid={formData.api_key.isInvalid}
-               isValid={formData.api_key.isValid}
-               value={formData.api_key.value}
++              type="text"
++              autoComplete="off"
++              placeholder={apiHostPlaceholder}
++              isInvalid={formData.api_host.isInvalid}
++              value={formData.api_host.value}
 +              onChange={(e) =>
 +                handleValueChange({
-                   api_key: {
++                  api_host: {
 +                    value: e.target.value,
 +                    errorMsg: '',
 +                    isInvalid: false,
-                     isValid: false,
 +                  },
 +                })
 +              }
 +            />
-             <Button
-               variant="outline-secondary"
-               className="rounded-end"
-               onClick={() => checkAiConfigData(null)}
-               disabled={isChecking}>
-               {t('api_key.check')}
-             </Button>
-             <Form.Control.Feedback
-               type={formData.api_key.isValid ? 'valid' : 'invalid'}>
-               {formData.api_key.errorMsg}
++            <Form.Control.Feedback type="invalid">
++              {formData.api_host.errorMsg}
 +            </Form.Control.Feedback>
-           </InputGroup>
-         </Form.Group>
- 
-         <Form.Group className="mb-3" controlId="model">
-           <Form.Label>{t('model.label')}</Form.Label>
-           <Form.Select
++          </Form.Group>
++
++          <Form.Group className="mb-3" controlId="api_key">
++            <Form.Label>{t('api_key.label')}</Form.Label>
++            <InputGroup>
++              <Form.Control
++                type="password"
++                autoComplete="new-password"
++                isInvalid={formData.api_key.isInvalid}
++                isValid={formData.api_key.isValid}
++                value={formData.api_key.value}
++                onChange={(e) =>
++                  handleValueChange({
++                    api_key: {
++                      value: e.target.value,
++                      errorMsg: '',
++                      isInvalid: false,
++                      isValid: false,
++                    },
++                  })
++                }
++              />
++              <Button
++                variant="outline-secondary"
++                className="rounded-end"
++                onClick={() => checkAiConfigData(null)}
++                disabled={isChecking}>
++                {t('api_key.check')}
++              </Button>
++              <Form.Control.Feedback
++                type={formData.api_key.isValid ? 'valid' : 'invalid'}>
++                {formData.api_key.errorMsg}
++              </Form.Control.Feedback>
++            </InputGroup>
++          </Form.Group>
++
++          <div className="mb-3">
++            <label htmlFor="model" className="form-label">
++              {t('model.label')}
++            </label>
++            {/* <Form.Select
++            list="datalistOptions"
 +            isInvalid={formData.model.isInvalid}
 +            value={formData.model.value}
 +            onChange={(e) =>
 +              handleValueChange({
 +                model: {
 +                  value: e.target.value,
 +                  errorMsg: '',
 +                  isInvalid: false,
 +                },
 +              })
 +            }>
 +            {modelsData?.map((model) => {
 +              return (
 +                <option key={model.id} value={model.id}>
 +                  {model.id}
 +                </option>
 +              );
 +            })}
-           </Form.Select>
-           <Form.Control.Feedback type="invalid">
-             {formData.model.errorMsg}
-           </Form.Control.Feedback>
-         </Form.Group>
- 
-         <Button type="submit">{t('save', { keyPrefix: 'btns' })}</Button>
-       </Form>
++          </Form.Select> */}
++            <input
++              className="form-control"
++              list="datalistOptions"
++              id="model"
++              value={formData.model.value}
++              onChange={(e) =>
++                handleValueChange({
++                  model: {
++                    value: e.target.value,
++                    errorMsg: '',
++                    isInvalid: false,
++                  },
++                })
++              }
++            />
++            <datalist id="datalistOptions">
++              {modelsData?.map((model) => {
++                return (
++                  <option key={model.id} value={model.id}>
++                    {model.id}
++                  </option>
++                );
++              })}
++            </datalist>
++
++            <div className="invalid-feedback">{formData.model.errorMsg}</div>
++          </div>
++
++          <Button type="submit">{t('save', { keyPrefix: 'btns' })}</Button>
++        </Form>
++      </div>
 +    </div>
 +  );
 +};
 +export default Index;
diff --cc ui/src/pages/Admin/Apikeys/components/Action/index.tsx
index 577d16c3,00000000..f3a7cbcc
mode 100644,000000..100644
--- a/ui/src/pages/Admin/Apikeys/components/Action/index.tsx
+++ b/ui/src/pages/Admin/Apikeys/components/Action/index.tsx
@@@ -1,71 -1,0 +1,77 @@@
 +/*
 + * Licensed to the Apache Software Foundation (ASF) under one
 + * or more contributor license agreements.  See the NOTICE file
 + * distributed with this work for additional information
 + * regarding copyright ownership.  The ASF licenses this file
 + * to you under the Apache License, Version 2.0 (the
 + * "License"); you may not use this file except in compliance
 + * with the License.  You may obtain a copy of the License at
 + *
 + *   http://www.apache.org/licenses/LICENSE-2.0
 + *
 + * Unless required by applicable law or agreed to in writing,
 + * software distributed under the License is distributed on an
 + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 + * KIND, either express or implied.  See the License for the
 + * specific language governing permissions and limitations
 + * under the License.
 + */
++
 +import { Dropdown } from 'react-bootstrap';
 +import { useTranslation } from 'react-i18next';
 +
- import { Modal, Icon } from '@/components';
- import { deleteAdminConversation } from '@/enterprise/services';
- import { useToast } from '@/hooks';
++import { Icon, Modal } from '@/components';
++import { deleteApiKey } from '@/services';
++import { toastStore } from '@/stores';
 +
- interface Props {
-   id: string;
-   refreshList?: () => void;
- }
- const ConversationsOperation = ({ id, refreshList }: Props) => {
++const ApiActions = ({ itemData, refreshList, showModal }) => {
 +  const { t } = useTranslation('translation', {
-     keyPrefix: 'admin.conversations',
++    keyPrefix: 'admin.apikeys.delete_modal',
 +  });
-   const toast = useToast();
 +
-   const handleAction = (eventKey: string | null) => {
-     if (eventKey === 'delete') {
++  const handleAction = (type) => {
++    if (type === 'delete') {
 +      Modal.confirm({
-         title: t('delete_modal.title'),
-         content: t('delete_modal.content'),
++        title: t('title'),
++        content: t('content'),
 +        cancelBtnVariant: 'link',
 +        confirmBtnVariant: 'danger',
 +        confirmText: t('delete', { keyPrefix: 'btns' }),
 +        onConfirm: () => {
-           deleteAdminConversation(id).then(() => {
-             refreshList?.();
-             toast.onShow({
++          deleteApiKey(itemData.id).then(() => {
++            toastStore.getState().show({
++              msg: t('api_key_deleted', { keyPrefix: 'messages' }),
 +              variant: 'success',
-               msg: t('delete_modal.delete_success'),
 +            });
++            refreshList();
 +          });
 +        },
 +      });
 +    }
++
++    if (type === 'edit') {
++      showModal(true);
++    }
 +  };
 +
 +  return (
-     <Dropdown onSelect={handleAction}>
-       <Dropdown.Toggle variant="link" className="no-toggle p-0 lh-1">
-         <Icon name="three-dots-vertical" title={t('action')} />
++    <Dropdown>
++      <Dropdown.Toggle variant="link" className="no-toggle p-0">
++        <Icon
++          name="three-dots-vertical"
++          title={t('action', { keyPrefix: 'admin.answers' })}
++        />
 +      </Dropdown.Toggle>
 +      <Dropdown.Menu align="end">
-         <Dropdown.Item eventKey="delete">
++        <Dropdown.Item onClick={() => handleAction('edit')}>
++          {t('edit', { keyPrefix: 'btns' })}
++        </Dropdown.Item>
++        <Dropdown.Item onClick={() => handleAction('delete')}>
 +          {t('delete', { keyPrefix: 'btns' })}
 +        </Dropdown.Item>
 +      </Dropdown.Menu>
 +    </Dropdown>
 +  );
 +};
 +
- export default ConversationsOperation;
++export default ApiActions;
diff --cc ui/src/pages/Admin/Apikeys/components/AddOrEditModal/index.tsx
index 00000000,00000000..34a3192a
new file mode 100644
--- /dev/null
+++ b/ui/src/pages/Admin/Apikeys/components/AddOrEditModal/index.tsx
@@@ -1,0 -1,0 +1,165 @@@
++import { useState } from 'react';
++import { Modal, Form, Button } from 'react-bootstrap';
++import { useTranslation } from 'react-i18next';
++
++import { handleFormError } from '@/utils';
++import { addApiKey, updateApiKey } from '@/services';
++
++const initFormData = {
++  description: {
++    value: '',
++    isInvalid: false,
++    errorMsg: '',
++  },
++  scope: {
++    value: 'read-only',
++    isInvalid: false,
++    errorMsg: '',
++  },
++};
++
++const Index = ({ data, visible = false, onClose, callback }) => {
++  const { t } = useTranslation('translation', {
++    keyPrefix: 'admin.apikeys.add_or_edit_modal',
++  });
++  const [formData, setFormData] = useState<any>(initFormData);
++
++  const handleValueChange = (value) => {
++    setFormData({
++      ...formData,
++      ...value,
++    });
++  };
++
++  const handleAdd = () => {
++    const { description, scope } = formData;
++    if (!description.value) {
++      setFormData({
++        ...formData,
++        description: {
++          ...description,
++          isInvalid: true,
++          errorMsg: t('description_required'),
++        },
++      });
++      return;
++    }
++    addApiKey({
++      description: description.value,
++      scope: scope.value,
++    })
++      .then((res) => {
++        callback('add', res.access_key);
++        setFormData(initFormData);
++      })
++      .catch((error) => {
++        const obj = handleFormError(error, formData);
++        setFormData({ ...obj });
++      });
++  };
++
++  const handleEdit = () => {
++    const { description } = formData;
++    if (!description.value) {
++      setFormData({
++        ...formData,
++        description: {
++          ...description,
++          isInvalid: true,
++          errorMsg: t('description_required'),
++        },
++      });
++      return;
++    }
++    updateApiKey({
++      description: description.value,
++      id: data?.id,
++    })
++      .then(() => {
++        callback('edit', null);
++        setFormData(initFormData);
++      })
++      .catch((error) => {
++        const obj = handleFormError(error, formData);
++        setFormData({ ...obj });
++      });
++  };
++
++  const handleSubmit = () => {
++    if (data?.id) {
++      handleEdit();
++      return;
++    }
++    handleAdd();
++  };
++
++  const closeModal = () => {
++    setFormData(initFormData);
++    onClose(false, null);
++  };
++  return (
++    <Modal show={visible} onHide={closeModal}>
++      <Modal.Header closeButton>
++        {data?.id ? t('edit_title') : t('add_title')}
++      </Modal.Header>
++      <Modal.Body>
++        <Form>
++          <Form.Group controlId="description" className="mb-3">
++            <Form.Label>{t('description')}</Form.Label>
++            <Form.Control
++              type="text"
++              isInvalid={formData.description.isInvalid}
++              value={formData.description.value}
++              onChange={(e) => {
++                handleValueChange({
++                  description: {
++                    value: e.target.value,
++                    errorMsg: '',
++                    isInvalid: false,
++                  },
++                });
++              }}
++            />
++            <Form.Control.Feedback type="invalid">
++              {formData.description.errorMsg}
++            </Form.Control.Feedback>
++          </Form.Group>
++
++          {!data?.id && visible && (
++            <Form.Group controlId="scope" className="mb-3">
++              <Form.Label>{t('scope')}</Form.Label>
++              <Form.Select
++                isInvalid={formData.scope.isInvalid}
++                value={formData.scope.value}
++                onChange={(e) => {
++                  handleValueChange({
++                    scope: {
++                      value: e.target.value,
++                      errorMsg: '',
++                      isInvalid: false,
++                    },
++                  });
++                }}>
++                <option value="read-only">{t('read-only')}</option>
++                <option value="global">{t('global')}</option>
++              </Form.Select>
++              <Form.Control.Feedback type="invalid">
++                {formData.scope.errorMsg}
++              </Form.Control.Feedback>
++            </Form.Group>
++          )}
++        </Form>
++      </Modal.Body>
++      <Modal.Footer>
++        <Button variant="link" onClick={closeModal}>
++          {t('cancel', { keyPrefix: 'btns' })}
++        </Button>
++        <Button type="button" variant="primary" onClick={handleSubmit}>
++          {t('submit', { keyPrefix: 'btns' })}
++        </Button>
++      </Modal.Footer>
++    </Modal>
++  );
++};
++
++export default Index;
diff --cc ui/src/pages/Admin/Apikeys/components/CreatedModal/index.tsx
index 00000000,00000000..10d16744
new file mode 100644
--- /dev/null
+++ b/ui/src/pages/Admin/Apikeys/components/CreatedModal/index.tsx
@@@ -1,0 -1,0 +1,36 @@@
++import { Modal, Form, Button } from 'react-bootstrap';
++import { useTranslation } from 'react-i18next';
++
++const Index = ({ visible, api_key = '', onClose }) => {
++  const { t } = useTranslation('translation', {
++    keyPrefix: 'admin.apikeys.created_modal',
++  });
++
++  return (
++    <Modal show={visible} onHide={onClose}>
++      <Modal.Header closeButton>{t('title')}</Modal.Header>
++      <Modal.Body>
++        <Form>
++          <Form.Group controlId="api_key" className="mb-3">
++            <Form.Label>{t('api_key')}</Form.Label>
++            <Form.Control
++              type="text"
++              defaultValue={api_key}
++              readOnly
++              disabled
++            />
++          </Form.Group>
++
++          <div className="mb-3">{t('description')}</div>
++        </Form>
++      </Modal.Body>
++      <Modal.Footer>
++        <Button variant="link" onClick={onClose}>
++          {t('close', { keyPrefix: 'btns' })}
++        </Button>
++      </Modal.Footer>
++    </Modal>
++  );
++};
++
++export default Index;
diff --cc ui/src/pages/Admin/Apikeys/components/index.ts
index 00000000,00000000..539b7d79
new file mode 100644
--- /dev/null
+++ b/ui/src/pages/Admin/Apikeys/components/index.ts
@@@ -1,0 -1,0 +1,5 @@@
++import Action from './Action';
++import AddOrEditModal from './AddOrEditModal';
++import CreatedModal from './CreatedModal';
++
++export { Action, AddOrEditModal, CreatedModal };
diff --cc ui/src/pages/Admin/Apikeys/index.tsx
index 00000000,00000000..30a994c7
new file mode 100644
--- /dev/null
+++ b/ui/src/pages/Admin/Apikeys/index.tsx
@@@ -1,0 -1,0 +1,119 @@@
++import { useState } from 'react';
++import { useTranslation } from 'react-i18next';
++import { Button, Table } from 'react-bootstrap';
++
++import dayjs from 'dayjs';
++
++import { useQueryApiKeys } from '@/services';
++
++import { Action, AddOrEditModal, CreatedModal } from './components';
++
++const Index = () => {
++  const { t } = useTranslation('translation', {
++    keyPrefix: 'admin.apikeys',
++  });
++  const [showModal, setShowModal] = useState({
++    visible: false,
++    item: null,
++  });
++  const [showCreatedModal, setShowCreatedModal] = useState({
++    visible: false,
++    api_key: '',
++  });
++  const { data: apiKeysList, mutate: refreshList } = useQueryApiKeys();
++
++  const handleAddModalState = (bol, item) => {
++    setShowModal({
++      visible: bol,
++      item,
++    });
++  };
++
++  const handleCreatedModalState = (visible, api_key) => {
++    setShowCreatedModal({
++      visible,
++      api_key,
++    });
++  };
++
++  const addOrEditCallback = (type, key) => {
++    handleAddModalState(false, null);
++    refreshList();
++    if (type === 'add') {
++      handleCreatedModalState(true, key);
++    }
++  };
++
++  return (
++    <div>
++      <h3 className="mb-4">{t('title')}</h3>
++      <Button
++        variant="outline-primary mb-3"
++        size="sm"
++        onClick={() => handleAddModalState(true, null)}>
++        {t('add_api_key')}
++      </Button>
++      <Table responsive="md">
++        <thead className="c-table">
++          <tr>
++            <th style={{ width: '20%' }}>{t('desc')}</th>
++            <th style={{ width: '11%' }}>{t('scope')}</th>
++            <th style={{ minWidth: '200px' }}>{t('key')}</th>
++            <th style={{ width: '18%' }}>{t('created')}</th>
++            <th style={{ width: '18%' }}>{t('last_used')}</th>
++            <th className="text-end" style={{ width: '10%' }}>
++              {t('action', { keyPrefix: 'admin.questions' })}
++            </th>
++          </tr>
++          {apiKeysList?.map((item) => {
++            return (
++              <tr key={item.id}>
++                <td>{item.description}</td>
++                <td>
++                  {t(item.scope, {
++                    keyPrefix: 'admin.apikeys.add_or_edit_modal',
++                  })}
++                </td>
++                <td>{item.access_key}</td>
++                <td>
++                  {dayjs
++                    .unix(item?.created_at)
++                    .tz()
++                    .format(t('long_date_with_time', { keyPrefix: 'dates' }))}
++                </td>
++                <td>
++                  {item?.last_used_at &&
++                    dayjs
++                      .unix(item?.last_used_at)
++                      .tz()
++                      .format(t('long_date_with_time', { keyPrefix: 'dates' 
}))}
++                </td>
++                <td className="text-end">
++                  <Action
++                    itemData={item}
++                    showModal={() => handleAddModalState(true, item)}
++                    refreshList={refreshList}
++                  />
++                </td>
++              </tr>
++            );
++          })}
++        </thead>
++      </Table>
++
++      <AddOrEditModal
++        data={showModal.item}
++        visible={showModal.visible}
++        onClose={handleAddModalState}
++        callback={addOrEditCallback}
++      />
++      <CreatedModal
++        visible={showCreatedModal.visible}
++        api_key={showCreatedModal.api_key}
++        onClose={() => handleCreatedModalState(false, '')}
++      />
++    </div>
++  );
++};
++
++export default Index;
diff --cc ui/src/pages/Admin/Mcp/index.tsx
index 00000000,00000000..118612b7
new file mode 100644
--- /dev/null
+++ b/ui/src/pages/Admin/Mcp/index.tsx
@@@ -1,0 -1,0 +1,94 @@@
++import { FormEvent, useEffect, useState } from 'react';
++import { useTranslation } from 'react-i18next';
++import { Form, Button } from 'react-bootstrap';
++
++import { useToast } from '@/hooks';
++import { getMcpConfig, saveMcpConfig } from '@/services';
++
++const Mcp = () => {
++  const toast = useToast();
++  const { t } = useTranslation('translation', {
++    keyPrefix: 'admin.mcp',
++  });
++  const [formData, setFormData] = useState({
++    enabled: true,
++    type: '',
++    url: '',
++    http_header: '',
++  });
++  const [isLoading, setIsLoading] = useState(false);
++
++  const handleOnChange = (form) => {
++    setFormData({ ...formData, ...form });
++  };
++  const onSubmit = (evt: FormEvent) => {
++    evt.preventDefault();
++    evt.stopPropagation();
++    saveMcpConfig({ enabled: formData.enabled }).then(() => {
++      toast.onShow({
++        msg: t('update', { keyPrefix: 'toast' }),
++        variant: 'success',
++      });
++    });
++  };
++
++  useEffect(() => {
++    getMcpConfig()
++      .then((resp) => {
++        setIsLoading(false);
++        setFormData(resp);
++      })
++      .catch(() => {
++        setIsLoading(false);
++      });
++  }, []);
++  if (isLoading) {
++    return null;
++  }
++  return (
++    <>
++      <h3 className="mb-4">{t('mcp', { keyPrefix: 'nav_menus' })}</h3>
++      <div className="max-w-748">
++        <Form onSubmit={onSubmit}>
++          <Form.Group className="mb-3" controlId="mcp_server">
++            <Form.Label>{t('mcp_server.label')}</Form.Label>
++            <Form.Check
++              type="switch"
++              label={t('mcp_server.switch')}
++              checked={formData.enabled}
++              onChange={(e) => handleOnChange({ enabled: e.target.checked })}
++            />
++          </Form.Group>
++          {formData.enabled && (
++            <>
++              <Form.Group className="mb-3" controlId="type">
++                <Form.Label>{t('type.label')}</Form.Label>
++                <Form.Control type="text" disabled value={formData.type} />
++              </Form.Group>
++              <Form.Group className="mb-3" controlId="url">
++                <Form.Label>{t('url.label')}</Form.Label>
++                <Form.Control type="text" disabled value={formData.url} />
++              </Form.Group>
++              <Form.Group className="mb-3">
++                <Form.Label>{t('http_header.label')}</Form.Label>
++                <Form.Control
++                  type="text"
++                  disabled
++                  value={formData.http_header}
++                />
++                <Form.Text className="text-muted">
++                  {t('http_header.text')}
++                </Form.Text>
++              </Form.Group>
++            </>
++          )}
++          <Button variant="primary" type="submit">
++            {t('save', { keyPrefix: 'btns' })}
++          </Button>
++        </Form>
++      </div>
++    </>
++  );
++};
++
++export default Mcp;
diff --cc ui/src/router/routes.ts
index 2328d526,7d7003a1..8423fb7a
--- a/ui/src/router/routes.ts
+++ b/ui/src/router/routes.ts
@@@ -429,14 -445,6 +445,22 @@@ const routes: RouteNode[] = 
              path: 'badges',
              page: 'pages/Admin/Badges',
            },
 +          {
-             path: 'conversations',
-             page: '@/enterprise/pages/Admin/Conversations',
++            path: 'ai-assistant',
++            page: 'pages/Admin/AiAssistant',
 +          },
 +          {
 +            path: 'ai-settings',
-             page: '@/enterprise/pages/Admin/AiSettings',
++            page: 'pages/Admin/AiSettings',
++          },
++          {
++            path: 'apikeys',
++            page: 'pages/Admin/Apikeys',
++          },
++          {
++            path: 'mcp',
++            page: 'pages/Admin/Mcp',
 +          },
          ],
        },
        {
@@@ -459,26 -467,6 +483,26 @@@
          path: '50x',
          page: 'pages/50X',
        },
 +      // ai
 +      {
 +        page: 'pages/SideNavLayoutWithoutFooter',
 +        children: [
 +          {
 +            path: '/ai-assistant',
-             page: '@/enterprise/pages/AiAssistant',
++            page: 'pages/AiAssistant',
 +            guard: () => {
 +              return guard.logged();
 +            },
 +          },
 +          {
 +            path: '/ai-assistant/:id',
-             page: '@/enterprise/pages/AiAssistant',
++            page: 'pages/AiAssistant',
 +            guard: () => {
 +              return guard.logged();
 +            },
 +          },
 +        ],
 +      },
      ],
    },
    {
diff --cc ui/src/services/admin/apikeys.ts
index 670534e2,1ea0e991..8271e024
--- a/ui/src/services/admin/apikeys.ts
+++ b/ui/src/services/admin/apikeys.ts
@@@ -17,32 -17,11 +17,35 @@@
   * under the License.
   */
  
- import qs from 'qs';
 -import Head from './Head';
 -import SearchItem from './SearchItem';
 -import Tips from './Tips';
 -import Empty from './Empty';
 -import SearchHead from './SearchHead';
 -import ListLoader from './ListLoader';
 +import useSWR from 'swr';
  
 -export { Head, SearchItem, Tips, Empty, SearchHead, ListLoader };
 +import request from '@/utils/request';
 +import type * as Type from '@/common/interface';
 +
- export const useQuestionSearch = (params: Type.AdminContentsReq) => {
-   const apiUrl = `/answer/admin/api/question/page?${qs.stringify(params)}`;
-   const { data, error, mutate } = useSWR<Type.ListResult, Error>(
-     [apiUrl],
++export const useQueryApiKeys = () => {
++  const apiUrl = `/answer/admin/api/api-key/all`;
++  const { data, error, mutate } = useSWR<Type.AdminApiKeysItem[], Error>(
++    apiUrl,
 +    request.instance.get,
 +  );
 +  return {
 +    data,
 +    isLoading: !data && !error,
 +    error,
 +    mutate,
 +  };
 +};
 +
- export const changeQuestionStatus = (
-   question_id: string,
-   status: Type.AdminQuestionStatus,
- ) => {
-   return request.put('/answer/admin/api/question/status', {
-     question_id,
-     status,
++export const addApiKey = (params: Type.AddOrEditApiKeyParams) => {
++  return request.post('/answer/admin/api/api-key', params);
++};
++
++export const updateApiKey = (params: Type.AddOrEditApiKeyParams) => {
++  return request.put('/answer/admin/api/api-key', params);
++};
++
++export const deleteApiKey = (id: string) => {
++  return request.delete('/answer/admin/api/api-key', {
++    id,
 +  });
 +};
diff --cc ui/src/services/admin/index.ts
index c2c630cf,3fa211ee..76d6ef90
--- a/ui/src/services/admin/index.ts
+++ b/ui/src/services/admin/index.ts
@@@ -25,4 -25,4 +25,7 @@@ export * from './users'
  export * from './dashboard';
  export * from './plugins';
  export * from './badges';
 +export * from './ai';
+ export * from './tags';
++export * from './apikeys';
++export * from './mcp';
diff --cc ui/src/services/admin/mcp.ts
index 00000000,00000000..ab86e6db
new file mode 100644
--- /dev/null
+++ b/ui/src/services/admin/mcp.ts
@@@ -1,0 -1,0 +1,16 @@@
++import request from '@/utils/request';
++
++type McpConfig = {
++  enabled: boolean;
++  type: string;
++  url: string;
++  http_header: string;
++};
++
++export const getMcpConfig = () => {
++  return request.get<McpConfig>(`/answer/admin/api/mcp-config`);
++};
++
++export const saveMcpConfig = (params: { enabled: boolean }) => {
++  return request.put(`/answer/admin/api/mcp-config`, params);
++};
diff --cc ui/src/stores/index.ts
index 1e3aa25d,1bd2fece..41af8d55
--- a/ui/src/stores/index.ts
+++ b/ui/src/stores/index.ts
@@@ -33,8 -33,7 +33,8 @@@ import loginToContinueStore from './log
  import errorCodeStore from './errorCode';
  import sideNavStore from './sideNav';
  import commentReplyStore from './commentReply';
- import siteLealStore from './siteLegal';
+ import siteSecurityStore from './siteSecurity';
 +import aiControlStore from './aiControl';
  
  export {
    toastStore,
@@@ -53,6 -52,5 +53,6 @@@
    sideNavStore,
    commentReplyStore,
    writeSettingStore,
-   siteLealStore,
+   siteSecurityStore,
 +  aiControlStore,
  };
diff --cc ui/src/utils/guard.ts
index e1d2ceb5,6e6a6d80..fc78fa12
--- a/ui/src/utils/guard.ts
+++ b/ui/src/utils/guard.ts
@@@ -30,8 -30,7 +30,8 @@@ import 
    loginToContinueStore,
    pageTagStore,
    writeSettingStore,
-   siteLealStore,
+   siteSecurityStore,
 +  aiControlStore,
  } from '@/stores';
  import { RouteAlias } from '@/router/alias';
  import {
@@@ -383,15 -382,11 +383,14 @@@ export const initAppSettingsStore = asy
      themeSettingStore.getState().update(appSettings.theme);
      seoSettingStore.getState().update(appSettings.site_seo);
      writeSettingStore.getState().update({
-       restrict_answer: appSettings.site_write.restrict_answer,
-       ...appSettings.site_write,
-     });
-     siteLealStore.getState().update({
-       external_content_display: 
appSettings.site_legal.external_content_display,
+       ...appSettings.site_advanced,
+       ...appSettings.site_questions,
+       ...appSettings.site_tags,
      });
 +    aiControlStore.getState().update({
 +      ai_enabled: appSettings.ai_enabled,
 +    });
+     siteSecurityStore.getState().update(appSettings.site_security);
    }
  };
  

Reply via email to