From ad81d60986ceb923fbc0e9f89e165ba579a699f8 Mon Sep 17 00:00:00 2001 From: Decksweeper20 <3569912451@qq.com> Date: Wed, 10 Dec 2025 22:14:51 +0800 Subject: [PATCH 01/36] Add user testimonials to memorial wall documentation Add an optional extended description --- doc/docs/zh/opensource-memorial-wall.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/docs/zh/opensource-memorial-wall.md b/doc/docs/zh/opensource-memorial-wall.md index 7c4a7cc91..0e8053109 100644 --- a/doc/docs/zh/opensource-memorial-wall.md +++ b/doc/docs/zh/opensource-memorial-wall.md @@ -623,3 +623,4 @@ Nexent开发者加油 ::: tip 开源新手 - 2025-12-05 感谢 Nexent 让我踏上了开源之旅! ::: +很开心能接触到这个平台,让我有机会踏上开源之旅 From 13317265510a109b1ed265a6aa90ab1eb364a9d0 Mon Sep 17 00:00:00 2001 From: xuyaqi Date: Fri, 12 Dec 2025 15:42:19 +0800 Subject: [PATCH 02/36] refactoring TopNavbar & SideNavigation by using antd default style --- .../components/navigation/SideNavigation.tsx | 258 ++++-------------- frontend/components/navigation/TopNavbar.tsx | 127 +++++---- frontend/const/layoutConstants.ts | 9 + 3 files changed, 126 insertions(+), 268 deletions(-) diff --git a/frontend/components/navigation/SideNavigation.tsx b/frontend/components/navigation/SideNavigation.tsx index 2de970754..04c7db783 100644 --- a/frontend/components/navigation/SideNavigation.tsx +++ b/frontend/components/navigation/SideNavigation.tsx @@ -22,7 +22,7 @@ import { } from "lucide-react"; import type { MenuProps } from "antd"; import { useAuth } from "@/hooks/useAuth"; -import { HEADER_CONFIG, FOOTER_CONFIG } from "@/const/layoutConstants"; +import { HEADER_CONFIG, FOOTER_CONFIG, SIDER_CONFIG } from "@/const/layoutConstants"; const { Sider } = Layout; @@ -75,10 +75,6 @@ export function SideNavigation({ const pathname = usePathname(); const [collapsed, setCollapsed] = useState(false); const [selectedKey, setSelectedKey] = useState("0"); - const expandedWidth = 277; - const collapsedWidth = 64; - const siderWidth = collapsed ? collapsedWidth : expandedWidth; - // Update selected key when pathname or currentView changes useEffect(() => { // If we have a currentView from parent, use it to determine the key @@ -105,197 +101,61 @@ export function SideNavigation({ } }, [pathname, currentView]); + // Helper function to create menu item with consistent icon styling + const createMenuItem = (key: string, Icon: any, labelKey: string, view: string, requiresAuth = false, requiresAdmin = false) => ({ + key, + icon: , + label: t(labelKey), + onClick: () => { + if (!isSpeedMode && requiresAdmin && user?.role !== "admin") { + onAdminRequired?.(); + } else if (!isSpeedMode && requiresAuth && !user) { + onAuthRequired?.(); + } else { + onViewChange?.(view); + } + }, + }); + // Menu items configuration const menuItems: MenuProps["items"] = [ - { - key: "0", - icon: , - label: t("sidebar.homePage"), - onClick: () => { - onViewChange?.("home"); - }, - }, - { - key: "1", - icon: , - label: t("sidebar.startChat"), - onClick: () => { - if (!isSpeedMode && !user) { - onAuthRequired?.(); - } else { - onViewChange?.("chat"); - } - }, - }, - { - key: "2", - icon: , - label: t("sidebar.quickConfig"), - onClick: () => { - if (!isSpeedMode && user?.role !== "admin") { - onAdminRequired?.(); - } else { - onViewChange?.("setup"); - } - }, - }, - { - key: "3", - icon: , - label: t("sidebar.agentSpace"), - onClick: () => { - if (!isSpeedMode && !user) { - onAuthRequired?.(); - } else { - onViewChange?.("space"); - } - }, - }, - { - key: "4", - icon: , - label: t("sidebar.agentMarket"), - onClick: () => { - if (!isSpeedMode && !user) { - onAuthRequired?.(); - } else { - onViewChange?.("market"); - } - }, - }, - { - key: "5", - icon: , - label: t("sidebar.agentDev"), - onClick: () => { - if (!isSpeedMode && user?.role !== "admin") { - onAdminRequired?.(); - } else { - onViewChange?.("agents"); - } - }, - }, - { - key: "6", - icon: , - label: t("sidebar.knowledgeBase"), - onClick: () => { - if (!isSpeedMode && !user) { - onAuthRequired?.(); - } else { - onViewChange?.("knowledges"); - } - }, - }, - { - key: "10", - icon: , - label: t("sidebar.mcpToolsManagement"), - onClick: () => { - if (!isSpeedMode && user?.role !== "admin") { - onAdminRequired?.(); - } else { - onViewChange?.("mcpTools"); - } - }, - }, - { - key: "11", - icon: , - label: t("sidebar.monitoringManagement"), - onClick: () => { - if (!isSpeedMode && user?.role !== "admin") { - onAdminRequired?.(); - } else { - onViewChange?.("monitoring"); - } - }, - }, - { - key: "7", - icon: , - label: t("sidebar.modelManagement"), - onClick: () => { - if (!isSpeedMode && user?.role !== "admin") { - onAdminRequired?.(); - } else { - onViewChange?.("models"); - } - }, - }, - { - key: "8", - icon: , - label: t("sidebar.memoryManagement"), - onClick: () => { - if (!isSpeedMode && user?.role !== "admin") { - onAdminRequired?.(); - } else { - onViewChange?.("memory"); - } - }, - }, - { - key: "9", - icon: , - label: t("sidebar.userManagement"), - onClick: () => { - if (!isSpeedMode && user?.role !== "admin") { - onAdminRequired?.(); - } else { - onViewChange?.("users"); - } - }, - }, + createMenuItem("0", Home, "sidebar.homePage", "home"), + createMenuItem("1", Bot, "sidebar.startChat", "chat", true), + createMenuItem("2", Zap, "sidebar.quickConfig", "setup", false, true), + createMenuItem("3", Globe, "sidebar.agentSpace", "space", true), + createMenuItem("4", ShoppingBag, "sidebar.agentMarket", "market", true), + createMenuItem("5", Code, "sidebar.agentDev", "agents", false, true), + createMenuItem("6", BookOpen, "sidebar.knowledgeBase", "knowledges", true), + createMenuItem("10", Puzzle, "sidebar.mcpToolsManagement", "mcpTools", false, true), + createMenuItem("11", Activity, "sidebar.monitoringManagement", "monitoring", false, true), + createMenuItem("7", Settings, "sidebar.modelManagement", "models", false, true), + createMenuItem("8", Database, "sidebar.memoryManagement", "memory", false, true), + createMenuItem("9", Users, "sidebar.userManagement", "users", false, true), ]; - // Calculate sidebar height dynamically based on header and footer reserved heights - const headerReservedHeight = parseInt(HEADER_CONFIG.RESERVED_HEIGHT); - const footerReservedHeight = parseInt(FOOTER_CONFIG.RESERVED_HEIGHT); - const sidebarHeight = `calc(100vh - ${headerReservedHeight}px - ${footerReservedHeight}px)`; - const sidebarTop = `${headerReservedHeight}px`; + // Calculate sidebar height and position dynamically + const sidebarHeight = `calc(100vh - ${HEADER_CONFIG.RESERVED_HEIGHT} - ${FOOTER_CONFIG.RESERVED_HEIGHT})`; + const sidebarTop = HEADER_CONFIG.RESERVED_HEIGHT; return ( - -
-
+ +
+
@@ -304,11 +164,7 @@ export function SideNavigation({ selectedKeys={[selectedKey]} items={menuItems} onClick={({ key }) => setSelectedKey(key)} - className="!bg-transparent !border-r-0" - style={{ - height: "100%", - borderRight: 0, - }} + className="bg-transparent border-r-0 h-full" />
@@ -320,30 +176,14 @@ export function SideNavigation({ shape="circle" size="small" onClick={() => setCollapsed(!collapsed)} - className="shadow-md hover:shadow-lg transition-all" + className="fixed top-1/2 -translate-y-1/2 w-6 h-6 min-w-6 p-0 border-2 border-white shadow-md hover:shadow-lg transition-all z-[800]" style={{ - position: "fixed", - left: collapsed ? "52px" : "264px", - top: "50vh", - transform: "translateY(-50%)", - width: "24px", - height: "24px", - minWidth: "24px", - padding: 0, - display: "flex", - alignItems: "center", - justifyContent: "center", - border: "2px solid white", - zIndex: 800, + left: collapsed + ? `${SIDER_CONFIG.COLLAPSED_WIDTH - 12}px` + : `${SIDER_CONFIG.EXPANDED_WIDTH - 13}px`, transition: "left 0.2s ease", }} - icon={ - collapsed ? ( - - ) : ( - - ) - } + icon={collapsed ? : } />
diff --git a/frontend/components/navigation/TopNavbar.tsx b/frontend/components/navigation/TopNavbar.tsx index 0062750b6..61553ac1c 100644 --- a/frontend/components/navigation/TopNavbar.tsx +++ b/frontend/components/navigation/TopNavbar.tsx @@ -8,10 +8,12 @@ import { Globe } from "lucide-react"; import { Dropdown } from "antd"; import { DownOutlined } from "@ant-design/icons"; import Link from "next/link"; -import { HEADER_CONFIG } from "@/const/layoutConstants"; +import { HEADER_CONFIG, SIDER_CONFIG } from "@/const/layoutConstants"; import { languageOptions } from "@/const/constants"; import { useLanguageSwitch } from "@/lib/language"; import React from "react"; +import { Flex, Layout } from 'antd'; +const { Header } = Layout; interface TopNavbarProps { /** Additional title text to display after logo (separated by |) */ @@ -30,48 +32,49 @@ export function TopNavbar({ additionalTitle, additionalRightContent }: TopNavbar const { user, isLoading: userLoading, isSpeedMode } = useAuth(); const { currentLanguage, handleLanguageChange } = useLanguageSwitch(); - // Left content - Logo + optional additional title + // Left content - Logo + optional additional title (aligned with sidebar width) const leftContent = ( -
+ + {/* Logo section - matches sidebar width */} -

+ ModelEngine {t("assistant.name")} -

+
- {/* Additional title with separator */} + {/* Additional title with separator - outside of sidebar width */} {additionalTitle && ( - <> -
+ +
{additionalTitle}
- +
)} -
+ ); // Right content - Additional content + default navigation items const rightContent = ( -
+ {/* Additional right content (e.g., status badge) */} {additionalRightContent} @@ -80,18 +83,20 @@ export function TopNavbar({ additionalTitle, additionalRightContent }: TopNavbar href="https://github.com/ModelEngine-Group/nexent" target="_blank" rel="noopener noreferrer" - className="text-xs font-medium text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white transition-colors flex items-center gap-1" + className="text-xs font-medium text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white transition-colors" > - - Github + + + Github + {/* ModelEngine link */} @@ -112,17 +117,19 @@ export function TopNavbar({ additionalTitle, additionalRightContent }: TopNavbar onClick: ({ key }) => handleLanguageChange(key as string), }} > - - - {languageOptions.find((o) => o.value === currentLanguage)?.label || - currentLanguage} - + + + + {languageOptions.find((o) => o.value === currentLanguage)?.label || + currentLanguage} + + {/* User status - only shown in full version */} {!isSpeedMode && ( - <> + {userLoading ? ( {t("common.loading")}... @@ -133,42 +140,44 @@ export function TopNavbar({ additionalTitle, additionalRightContent }: TopNavbar ) : null} - + )} -
+ ); return ( -
- {/* Left section - Logo + additional title */} - {leftContent} + + {/* Left section - Logo + additional title */} + {leftContent} - {/* Right section - Additional content + default navigation */} - {rightContent} + {/* Right section - Additional content + default navigation */} + {rightContent} - {/* Mobile hamburger menu button */} - -
+ {/* Mobile hamburger menu button */} + + + ); } diff --git a/frontend/const/layoutConstants.ts b/frontend/const/layoutConstants.ts index 77b16df09..1c209617f 100644 --- a/frontend/const/layoutConstants.ts +++ b/frontend/const/layoutConstants.ts @@ -18,6 +18,15 @@ export const HEADER_CONFIG = { HORIZONTAL_PADDING: "24px", // px-6 } as const; +// Sidebar configuration +export const SIDER_CONFIG = { + // Sidebar width when expanded + EXPANDED_WIDTH: 280, + + // Sidebar width when collapsed + COLLAPSED_WIDTH: 64, +} as const; + // Footer configuration export const FOOTER_CONFIG = { // Actual displayed height (including padding) From aebf020b11543886b202595eb21aceee43d8899e Mon Sep 17 00:00:00 2001 From: panyehong <2655992392@qq.com> Date: Sat, 13 Dec 2025 17:19:37 +0800 Subject: [PATCH 03/36] =?UTF-8?q?=F0=9F=90=9B=20The=20prompt=20includes=20?= =?UTF-8?q?the=20thought=20process=20#2038=20[Specification=20Details]=201?= =?UTF-8?q?.Modify=20the=20tokens=20processing=20logic=20in=20llm=5Futils?= =?UTF-8?q?=20to=20remove=20the=20fields=20before=20.=202.Add=20te?= =?UTF-8?q?st=20cases.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/utils/llm_utils.py | 61 +++- test/backend/utils/test_llm_utils.py | 507 ++++++++++++++++++++++++++- 2 files changed, 558 insertions(+), 10 deletions(-) diff --git a/backend/utils/llm_utils.py b/backend/utils/llm_utils.py index 2e1590498..e74ef91b3 100644 --- a/backend/utils/llm_utils.py +++ b/backend/utils/llm_utils.py @@ -18,16 +18,37 @@ def _process_thinking_tokens( ) -> bool: """ Process tokens to filter out thinking content between and tags. + Handles cases where providers only send a closing tag or mix reasoning_content. """ - if is_thinking: - return THINK_END_PATTERN not in new_token + # Check for end tag first, as it might appear in the same token as start tag + if THINK_END_PATTERN in new_token: + # If we were never in think mode, treat everything accumulated so far as reasoning and clear it + if not is_thinking: + token_join.clear() + if callback: + callback("") # clear any previously streamed reasoning content + + # Exit thinking mode and only keep content after + _, _, after_end = new_token.partition(THINK_END_PATTERN) + is_thinking = False + new_token = after_end + # Continue processing the remaining content in this token + # Check for start tag (after processing end tag, in case both are in the same token) if THINK_START_PATTERN in new_token: + # Drop any content before and switch to thinking mode + _, _, after_start = new_token.partition(THINK_START_PATTERN) + new_token = after_start + is_thinking = True + + if is_thinking: + # Still inside thinking content; ignore until we exit return True - token_join.append(new_token) - if callback: - callback("".join(token_join)) + if new_token: + token_join.append(new_token) + if callback: + callback("".join(token_join)) return False @@ -46,8 +67,8 @@ def call_llm_for_system_prompt( llm = OpenAIModel( model_id=get_model_name_from_config(llm_model_config) if llm_model_config else "", - api_base=llm_model_config.get("base_url", ""), - api_key=llm_model_config.get("api_key", ""), + api_base=llm_model_config.get("base_url", "") if llm_model_config else "", + api_key=llm_model_config.get("api_key", "") if llm_model_config else "", temperature=0.3, top_p=0.95, ) @@ -65,16 +86,38 @@ def call_llm_for_system_prompt( current_request = llm.client.chat.completions.create(stream=True, **completion_kwargs) token_join: List[str] = [] is_thinking = False + reasoning_content_seen = False + content_tokens_seen = 0 for chunk in current_request: - new_token = chunk.choices[0].delta.content + delta = chunk.choices[0].delta + reasoning_content = getattr(delta, "reasoning_content", None) + new_token = delta.content + + # Note: reasoning_content is separate metadata and doesn't affect content filtering + # We only filter content based on tags in delta.content + if reasoning_content: + reasoning_content_seen = True + logger.debug("Received reasoning_content (metadata only, not filtering content)") + + # Process content token if it exists if new_token is not None: + content_tokens_seen += 1 is_thinking = _process_thinking_tokens( new_token, is_thinking, token_join, callback, ) - return "".join(token_join) + + result = "".join(token_join) + if not result and content_tokens_seen > 0: + logger.warning( + "Generated prompt is empty but %d content tokens were processed. " + "This suggests all content was filtered out.", + content_tokens_seen + ) + + return result except Exception as exc: logger.error("Failed to generate prompt from LLM: %s", str(exc)) raise diff --git a/test/backend/utils/test_llm_utils.py b/test/backend/utils/test_llm_utils.py index 50857e91b..ce4ed98d0 100644 --- a/test/backend/utils/test_llm_utils.py +++ b/test/backend/utils/test_llm_utils.py @@ -266,9 +266,514 @@ def mock_callback(text): is_thinking = _process_thinking_tokens("", False, token_join, mock_callback) self.assertFalse(is_thinking) - self.assertEqual(token_join, [""]) + self.assertEqual(token_join, []) + self.assertEqual(callback_calls, []) + + def test_process_thinking_tokens_end_tag_without_starting(self): + """Test end tag when never in thinking mode - should clear token_join""" + token_join = ["Some", "content"] + callback_calls = [] + + def mock_callback(text): + callback_calls.append(text) + + is_thinking = _process_thinking_tokens("", False, token_join, mock_callback) + + self.assertFalse(is_thinking) + self.assertEqual(token_join, []) self.assertEqual(callback_calls, [""]) + def test_process_thinking_tokens_end_tag_without_starting_no_callback(self): + """Test end tag when never in thinking mode without callback""" + token_join = ["Some", "content"] + + is_thinking = _process_thinking_tokens("", False, token_join, None) + + self.assertFalse(is_thinking) + self.assertEqual(token_join, []) + + def test_process_thinking_tokens_end_tag_with_content_after(self): + """Test end tag followed by content in the same token""" + token_join = ["Hello"] + callback_calls = [] + + def mock_callback(text): + callback_calls.append(text) + + is_thinking = _process_thinking_tokens("World", True, token_join, mock_callback) + + self.assertFalse(is_thinking) + self.assertEqual(token_join, ["Hello", "World"]) + self.assertEqual(callback_calls, ["HelloWorld"]) + + def test_process_thinking_tokens_start_tag_with_content_after(self): + """Test start tag followed by content in the same token""" + token_join = ["Hello"] + callback_calls = [] + + def mock_callback(text): + callback_calls.append(text) + + is_thinking = _process_thinking_tokens("thinking", False, token_join, mock_callback) + + self.assertTrue(is_thinking) + self.assertEqual(token_join, ["Hello"]) + self.assertEqual(callback_calls, []) + + def test_process_thinking_tokens_both_tags_in_same_token(self): + """Test both start and end tags in the same token""" + token_join = ["Hello"] + callback_calls = [] + + def mock_callback(text): + callback_calls.append(text) + + # When both tags are in the same token, end tag is processed first + # End tag clears token_join (since is_thinking=False), sets is_thinking=False, + # new_token becomes "World" (content after ) + # Then start tag check happens on "World", no match, so is_thinking stays False + # Then is_thinking check returns False, so "World" is added to token_join + is_thinking = _process_thinking_tokens( + "thinkingWorld", + False, + token_join, + mock_callback, + ) + + # After processing end tag: token_join cleared, is_thinking=False, new_token="World" + # Start tag check on "World": no match, is_thinking stays False + # Then "World" is added to token_join + # Note: When end tag clears token_join, callback("") is called, but empty string is not added to token_join + self.assertFalse(is_thinking) + self.assertEqual(token_join, ["World"]) + self.assertEqual(callback_calls, ["", "World"]) + + def test_process_thinking_tokens_new_token_empty_after_processing(self): + """Test when new_token becomes empty after processing tags""" + token_join = ["Hello"] + callback_calls = [] + + def mock_callback(text): + callback_calls.append(text) + + # End tag with no content after + is_thinking = _process_thinking_tokens("", True, token_join, mock_callback) + + self.assertFalse(is_thinking) + self.assertEqual(token_join, ["Hello"]) + self.assertEqual(callback_calls, []) + + +class TestCallLLMForSystemPromptExtended(unittest.TestCase): + """Extended tests for call_llm_for_system_prompt to achieve 100% coverage""" + + def setUp(self): + self.test_model_id = 1 + + @patch('backend.utils.llm_utils.OpenAIModel') + @patch('backend.utils.llm_utils.get_model_name_from_config') + @patch('backend.utils.llm_utils.get_model_by_model_id') + def test_call_llm_for_system_prompt_with_callback( + self, + mock_get_model_by_id, + mock_get_model_name, + mock_openai, + ): + """Test call_llm_for_system_prompt with callback""" + mock_model_config = { + "base_url": "http://example.com", + "api_key": "fake-key", + } + mock_get_model_by_id.return_value = mock_model_config + mock_get_model_name.return_value = "gpt-4" + + mock_llm_instance = mock_openai.return_value + mock_chunk = MagicMock() + mock_chunk.choices = [MagicMock()] + mock_chunk.choices[0].delta.content = "Generated prompt" + + mock_llm_instance.client = MagicMock() + mock_llm_instance.client.chat.completions.create.return_value = [mock_chunk] + mock_llm_instance._prepare_completion_kwargs.return_value = {} + + callback_calls = [] + + def mock_callback(text): + callback_calls.append(text) + + result = call_llm_for_system_prompt( + self.test_model_id, + "user prompt", + "system prompt", + callback=mock_callback, + ) + + self.assertEqual(result, "Generated prompt") + self.assertEqual(len(callback_calls), 1) + self.assertEqual(callback_calls[0], "Generated prompt") + + @patch('backend.utils.llm_utils.OpenAIModel') + @patch('backend.utils.llm_utils.get_model_name_from_config') + @patch('backend.utils.llm_utils.get_model_by_model_id') + def test_call_llm_for_system_prompt_with_reasoning_content( + self, + mock_get_model_by_id, + mock_get_model_name, + mock_openai, + ): + """Test call_llm_for_system_prompt with reasoning_content""" + mock_model_config = { + "base_url": "http://example.com", + "api_key": "fake-key", + } + mock_get_model_by_id.return_value = mock_model_config + mock_get_model_name.return_value = "gpt-4" + + mock_llm_instance = mock_openai.return_value + mock_chunk = MagicMock() + mock_chunk.choices = [MagicMock()] + mock_chunk.choices[0].delta.content = "Generated prompt" + mock_chunk.choices[0].delta.reasoning_content = "Some reasoning" + + mock_llm_instance.client = MagicMock() + mock_llm_instance.client.chat.completions.create.return_value = [mock_chunk] + mock_llm_instance._prepare_completion_kwargs.return_value = {} + + result = call_llm_for_system_prompt( + self.test_model_id, + "user prompt", + "system prompt", + ) + + self.assertEqual(result, "Generated prompt") + + @patch('backend.utils.llm_utils.OpenAIModel') + @patch('backend.utils.llm_utils.get_model_name_from_config') + @patch('backend.utils.llm_utils.get_model_by_model_id') + def test_call_llm_for_system_prompt_multiple_chunks( + self, + mock_get_model_by_id, + mock_get_model_name, + mock_openai, + ): + """Test call_llm_for_system_prompt with multiple chunks""" + mock_model_config = { + "base_url": "http://example.com", + "api_key": "fake-key", + } + mock_get_model_by_id.return_value = mock_model_config + mock_get_model_name.return_value = "gpt-4" + + mock_llm_instance = mock_openai.return_value + mock_chunk1 = MagicMock() + mock_chunk1.choices = [MagicMock()] + mock_chunk1.choices[0].delta.content = "Generated " + mock_chunk1.choices[0].delta.reasoning_content = None + + mock_chunk2 = MagicMock() + mock_chunk2.choices = [MagicMock()] + mock_chunk2.choices[0].delta.content = "prompt" + mock_chunk2.choices[0].delta.reasoning_content = None + + mock_llm_instance.client = MagicMock() + mock_llm_instance.client.chat.completions.create.return_value = [mock_chunk1, mock_chunk2] + mock_llm_instance._prepare_completion_kwargs.return_value = {} + + result = call_llm_for_system_prompt( + self.test_model_id, + "user prompt", + "system prompt", + ) + + self.assertEqual(result, "Generated prompt") + + @patch('backend.utils.llm_utils.OpenAIModel') + @patch('backend.utils.llm_utils.get_model_name_from_config') + @patch('backend.utils.llm_utils.get_model_by_model_id') + def test_call_llm_for_system_prompt_with_none_content( + self, + mock_get_model_by_id, + mock_get_model_name, + mock_openai, + ): + """Test call_llm_for_system_prompt with delta.content as None""" + mock_model_config = { + "base_url": "http://example.com", + "api_key": "fake-key", + } + mock_get_model_by_id.return_value = mock_model_config + mock_get_model_name.return_value = "gpt-4" + + mock_llm_instance = mock_openai.return_value + mock_chunk = MagicMock() + mock_chunk.choices = [MagicMock()] + mock_chunk.choices[0].delta.content = None + mock_chunk.choices[0].delta.reasoning_content = "Some reasoning" + + mock_llm_instance.client = MagicMock() + mock_llm_instance.client.chat.completions.create.return_value = [mock_chunk] + mock_llm_instance._prepare_completion_kwargs.return_value = {} + + result = call_llm_for_system_prompt( + self.test_model_id, + "user prompt", + "system prompt", + ) + + self.assertEqual(result, "") + + @patch('backend.utils.llm_utils.OpenAIModel') + @patch('backend.utils.llm_utils.get_model_name_from_config') + @patch('backend.utils.llm_utils.get_model_by_model_id') + def test_call_llm_for_system_prompt_with_thinking_tags( + self, + mock_get_model_by_id, + mock_get_model_name, + mock_openai, + ): + """Test call_llm_for_system_prompt with thinking tags""" + mock_model_config = { + "base_url": "http://example.com", + "api_key": "fake-key", + } + mock_get_model_by_id.return_value = mock_model_config + mock_get_model_name.return_value = "gpt-4" + + mock_llm_instance = mock_openai.return_value + mock_chunk1 = MagicMock() + mock_chunk1.choices = [MagicMock()] + mock_chunk1.choices[0].delta.content = "Start " + mock_chunk1.choices[0].delta.reasoning_content = None + + mock_chunk2 = MagicMock() + mock_chunk2.choices = [MagicMock()] + mock_chunk2.choices[0].delta.content = "thinking" + mock_chunk2.choices[0].delta.reasoning_content = None + + mock_chunk3 = MagicMock() + mock_chunk3.choices = [MagicMock()] + mock_chunk3.choices[0].delta.content = " End" + mock_chunk3.choices[0].delta.reasoning_content = None + + mock_llm_instance.client = MagicMock() + mock_llm_instance.client.chat.completions.create.return_value = [ + mock_chunk1, + mock_chunk2, + mock_chunk3, + ] + mock_llm_instance._prepare_completion_kwargs.return_value = {} + + result = call_llm_for_system_prompt( + self.test_model_id, + "user prompt", + "system prompt", + ) + + # chunk1: "Start " -> added to token_join + # chunk2: "thinking" -> + # end tag clears token_join (since is_thinking=False), new_token becomes "" + # chunk3: " End" -> added to token_join + # Final result should be " End" (chunk1 content was cleared by chunk2's end tag) + self.assertEqual(result, " End") + + @patch('backend.utils.llm_utils.OpenAIModel') + @patch('backend.utils.llm_utils.get_model_name_from_config') + @patch('backend.utils.llm_utils.get_model_by_model_id') + @patch('backend.utils.llm_utils.logger') + def test_call_llm_for_system_prompt_empty_result_with_tokens( + self, + mock_logger, + mock_get_model_by_id, + mock_get_model_name, + mock_openai, + ): + """Test call_llm_for_system_prompt with empty result but processed tokens""" + mock_model_config = { + "base_url": "http://example.com", + "api_key": "fake-key", + } + mock_get_model_by_id.return_value = mock_model_config + mock_get_model_name.return_value = "gpt-4" + + mock_llm_instance = mock_openai.return_value + mock_chunk = MagicMock() + mock_chunk.choices = [MagicMock()] + # Content that will be filtered out by thinking tags + mock_chunk.choices[0].delta.content = "all content" + mock_chunk.choices[0].delta.reasoning_content = None + + mock_llm_instance.client = MagicMock() + mock_llm_instance.client.chat.completions.create.return_value = [mock_chunk] + mock_llm_instance._prepare_completion_kwargs.return_value = {} + + result = call_llm_for_system_prompt( + self.test_model_id, + "user prompt", + "system prompt", + ) + + self.assertEqual(result, "") + # Verify warning was logged + mock_logger.warning.assert_called_once() + call_args = mock_logger.warning.call_args[0][0] + self.assertIn("empty but", call_args) + self.assertIn("content tokens were processed", call_args) + + @patch('backend.utils.llm_utils.OpenAIModel') + @patch('backend.utils.llm_utils.get_model_name_from_config') + @patch('backend.utils.llm_utils.get_model_by_model_id') + def test_call_llm_for_system_prompt_with_tenant_id( + self, + mock_get_model_by_id, + mock_get_model_name, + mock_openai, + ): + """Test call_llm_for_system_prompt with tenant_id""" + mock_model_config = { + "base_url": "http://example.com", + "api_key": "fake-key", + } + mock_get_model_by_id.return_value = mock_model_config + mock_get_model_name.return_value = "gpt-4" + + mock_llm_instance = mock_openai.return_value + mock_chunk = MagicMock() + mock_chunk.choices = [MagicMock()] + mock_chunk.choices[0].delta.content = "Generated prompt" + + mock_llm_instance.client = MagicMock() + mock_llm_instance.client.chat.completions.create.return_value = [mock_chunk] + mock_llm_instance._prepare_completion_kwargs.return_value = {} + + result = call_llm_for_system_prompt( + self.test_model_id, + "user prompt", + "system prompt", + tenant_id="test-tenant", + ) + + self.assertEqual(result, "Generated prompt") + mock_get_model_by_id.assert_called_once_with( + model_id=self.test_model_id, + tenant_id="test-tenant", + ) + + @patch('backend.utils.llm_utils.OpenAIModel') + @patch('backend.utils.llm_utils.get_model_name_from_config') + @patch('backend.utils.llm_utils.get_model_by_model_id') + def test_call_llm_for_system_prompt_with_none_model_config( + self, + mock_get_model_by_id, + mock_get_model_name, + mock_openai, + ): + """Test call_llm_for_system_prompt with None model config""" + mock_get_model_by_id.return_value = None + mock_get_model_name.return_value = "gpt-4" + + mock_llm_instance = mock_openai.return_value + mock_chunk = MagicMock() + mock_chunk.choices = [MagicMock()] + mock_chunk.choices[0].delta.content = "Generated prompt" + + mock_llm_instance.client = MagicMock() + mock_llm_instance.client.chat.completions.create.return_value = [mock_chunk] + mock_llm_instance._prepare_completion_kwargs.return_value = {} + + result = call_llm_for_system_prompt( + self.test_model_id, + "user prompt", + "system prompt", + ) + + self.assertEqual(result, "Generated prompt") + # Verify OpenAIModel was called with empty strings when model_config is None + mock_openai.assert_called_once_with( + model_id="", + api_base="", + api_key="", + temperature=0.3, + top_p=0.95, + ) + + @patch('backend.utils.llm_utils.OpenAIModel') + @patch('backend.utils.llm_utils.get_model_name_from_config') + @patch('backend.utils.llm_utils.get_model_by_model_id') + @patch('backend.utils.llm_utils.logger') + def test_call_llm_for_system_prompt_reasoning_content_logging( + self, + mock_logger, + mock_get_model_by_id, + mock_get_model_name, + mock_openai, + ): + """Test call_llm_for_system_prompt logs when reasoning_content is received""" + mock_model_config = { + "base_url": "http://example.com", + "api_key": "fake-key", + } + mock_get_model_by_id.return_value = mock_model_config + mock_get_model_name.return_value = "gpt-4" + + mock_llm_instance = mock_openai.return_value + mock_chunk = MagicMock() + mock_chunk.choices = [MagicMock()] + mock_chunk.choices[0].delta.content = "Generated prompt" + mock_chunk.choices[0].delta.reasoning_content = "Some reasoning" + + mock_llm_instance.client = MagicMock() + mock_llm_instance.client.chat.completions.create.return_value = [mock_chunk] + mock_llm_instance._prepare_completion_kwargs.return_value = {} + + result = call_llm_for_system_prompt( + self.test_model_id, + "user prompt", + "system prompt", + ) + + self.assertEqual(result, "Generated prompt") + # Verify debug log was called for reasoning_content + mock_logger.debug.assert_called_once() + call_args = mock_logger.debug.call_args[0][0] + self.assertIn("reasoning_content", call_args) + + @patch('backend.utils.llm_utils.OpenAIModel') + @patch('backend.utils.llm_utils.get_model_name_from_config') + @patch('backend.utils.llm_utils.get_model_by_model_id') + @patch('backend.utils.llm_utils.logger') + def test_call_llm_for_system_prompt_exception_logging( + self, + mock_logger, + mock_get_model_by_id, + mock_get_model_name, + mock_openai, + ): + """Test call_llm_for_system_prompt exception handling and logging""" + mock_model_config = { + "base_url": "http://example.com", + "api_key": "fake-key", + } + mock_get_model_by_id.return_value = mock_model_config + mock_get_model_name.return_value = "gpt-4" + + mock_llm_instance = mock_openai.return_value + mock_llm_instance.client = MagicMock() + mock_llm_instance.client.chat.completions.create.side_effect = Exception("LLM error") + mock_llm_instance._prepare_completion_kwargs.return_value = {} + + with self.assertRaises(Exception) as context: + call_llm_for_system_prompt( + self.test_model_id, + "user prompt", + "system prompt", + ) + + self.assertIn("LLM error", str(context.exception)) + # Verify error was logged + mock_logger.error.assert_called_once() + call_args = mock_logger.error.call_args[0][0] + self.assertIn("Failed to generate prompt", call_args) + if __name__ == '__main__': unittest.main() From d55cbf135608cf6145ccd7d0c1578e7dc128a973 Mon Sep 17 00:00:00 2001 From: Chen Kaiquan <92064677+Magic026@users.noreply.github.com> Date: Sun, 14 Dec 2025 20:47:54 +0800 Subject: [PATCH 04/36] =?UTF-8?q?Add=20new=20tip=20for=20=E5=BC=80?= =?UTF-8?q?=E6=BA=90=E6=96=B0=E6=89=8B=20on=20Nexent=20platform?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/docs/zh/opensource-memorial-wall.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/docs/zh/opensource-memorial-wall.md b/doc/docs/zh/opensource-memorial-wall.md index 7c4a7cc91..e5d1c9f62 100644 --- a/doc/docs/zh/opensource-memorial-wall.md +++ b/doc/docs/zh/opensource-memorial-wall.md @@ -623,3 +623,7 @@ Nexent开发者加油 ::: tip 开源新手 - 2025-12-05 感谢 Nexent 让我踏上了开源之旅! ::: + +::: tip 开源新手 - 2025-12-14 +开放原子大赛接触到了Nexent平台,祝越来越好! +::: From 53bab91249cc5377b77fd4ee13e4f672cefb55f8 Mon Sep 17 00:00:00 2001 From: xuyaqi Date: Mon, 15 Dec 2025 16:16:06 +0800 Subject: [PATCH 05/36] Bugfix workspace root detection for multiple lockfiles --- frontend/next.config.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs index a44561b9c..c136acce8 100644 --- a/frontend/next.config.mjs +++ b/frontend/next.config.mjs @@ -22,6 +22,8 @@ const nextConfig = { parallelServerCompiles: true, }, compress: true, + // Fix workspace root detection for multiple lockfiles + outputFileTracingRoot: process.cwd(), } mergeConfig(nextConfig, userConfig) From cbc12a2b4de622037c466f69229ee6f5d282782a Mon Sep 17 00:00:00 2001 From: xuyaqi Date: Mon, 15 Dec 2025 18:22:14 +0800 Subject: [PATCH 06/36] Unify global modals: step1: delete unused code --- .../[locale]/chat/components/chatHeader.tsx | 13 +- .../internal/memory/memoryDeleteModal.tsx | 55 -- .../internal/memory/memoryManageModal.tsx | 595 ------------------ 3 files changed, 2 insertions(+), 661 deletions(-) delete mode 100644 frontend/app/[locale]/chat/internal/memory/memoryDeleteModal.tsx delete mode 100644 frontend/app/[locale]/chat/internal/memory/memoryManageModal.tsx diff --git a/frontend/app/[locale]/chat/components/chatHeader.tsx b/frontend/app/[locale]/chat/components/chatHeader.tsx index f9578bedf..4aff3f40d 100644 --- a/frontend/app/[locale]/chat/components/chatHeader.tsx +++ b/frontend/app/[locale]/chat/components/chatHeader.tsx @@ -14,8 +14,6 @@ import { useAuth } from "@/hooks/useAuth"; import { USER_ROLES } from "@/const/modelConfig"; import { saveView } from "@/lib/viewPersistence"; -import MemoryManageModal from "../internal/memory/memoryManageModal"; - interface ChatHeaderProps { title: string; onRename?: (newTitle: string) => void; @@ -25,8 +23,7 @@ export function ChatHeader({ title, onRename }: ChatHeaderProps) { const router = useRouter(); const [isEditing, setIsEditing] = useState(false); const [editTitle, setEditTitle] = useState(title); - const [memoryModalVisible, setMemoryModalVisible] = useState(false); - const [embeddingConfigured, setEmbeddingConfigured] = useState(true); + const [showConfigPrompt, setShowConfigPrompt] = useState(false); const [showAutoOffPrompt, setShowAutoOffPrompt] = useState(false); const inputRef = useRef(null); @@ -62,9 +59,8 @@ export function ChatHeader({ title, onRename }: ChatHeaderProps) { const modelConfig = configStore.getModelConfig(); const configured = Boolean( modelConfig?.embedding?.modelName || - modelConfig?.multiEmbedding?.modelName + modelConfig?.multiEmbedding?.modelName ); - setEmbeddingConfigured(configured); if (!configured) { // If memory switch is on, turn it off automatically and notify the user @@ -85,7 +81,6 @@ export function ChatHeader({ title, onRename }: ChatHeaderProps) { }); } } catch (e) { - setEmbeddingConfigured(false); log.error("Failed to read model config for embedding check", e); } }, []); @@ -227,10 +222,6 @@ export function ChatHeader({ title, onRename }: ChatHeaderProps) {
- setMemoryModalVisible(false)} - /> ); } diff --git a/frontend/app/[locale]/chat/internal/memory/memoryDeleteModal.tsx b/frontend/app/[locale]/chat/internal/memory/memoryDeleteModal.tsx deleted file mode 100644 index 007e00270..000000000 --- a/frontend/app/[locale]/chat/internal/memory/memoryDeleteModal.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from "react"; -import { useTranslation, Trans } from "react-i18next"; -import { Modal } from "antd"; -import { WarningFilled } from "@ant-design/icons"; -import { MemoryDeleteModalProps } from "@/types/memory"; - -/** - * Hosts the "Clear Memory" secondary confirmation popup window and is responsible only for UI display. - * The upper-level component is responsible for controlling visible and callback logic. - */ -const MemoryDeleteModal: React.FC = ({ - visible, - targetTitle, - onOk, - onCancel, -}) => { - const { t } = useTranslation(); - return ( - - {t("memoryDeleteModal.title")} -
- } - onOk={onOk} - onCancel={onCancel} - okText={t("memoryDeleteModal.clear")} - cancelText={t("common.cancel")} - okButtonProps={{ danger: true }} - destroyOnClose - > -
- -
-

- }} - /> -

-

- {t("memoryDeleteModal.prompt")} -

-
-
- - ); -}; - -export default MemoryDeleteModal; diff --git a/frontend/app/[locale]/chat/internal/memory/memoryManageModal.tsx b/frontend/app/[locale]/chat/internal/memory/memoryManageModal.tsx deleted file mode 100644 index 424bf75f7..000000000 --- a/frontend/app/[locale]/chat/internal/memory/memoryManageModal.tsx +++ /dev/null @@ -1,595 +0,0 @@ -import React from "react"; -import { useTranslation } from "react-i18next"; -import { - Modal, - Tabs, - Collapse, - List, - Button, - Switch, - Pagination, - Dropdown, - Input, - App, -} from "antd"; -import { CaretRightOutlined, DownOutlined } from "@ant-design/icons"; -import { USER_ROLES, MEMORY_TAB_KEYS, MemoryTabKey } from "@/const/modelConfig"; -import { MEMORY_SHARE_STRATEGY, MemoryShareStrategy } from "@/const/memoryConfig"; -import { - MessageSquarePlus, - Eraser, - MessageSquareOff, - UsersRound, - UserRound, - Bot, - Share2, - Settings, - MessageSquareDashed, - Check, - X, -} from "lucide-react"; - -import { useAuth } from "@/hooks/useAuth"; -import { useMemory } from "@/hooks/useMemory"; - -import MemoryDeleteModal from "./memoryDeleteModal"; -import { MemoryManageModalProps, LabelWithIconFunction } from "@/types/memory"; - -/** - * Memory management popup, responsible only for UI rendering. - * Complex state logic is managed by hooks/useMemory.ts. - */ -const MemoryManageModal: React.FC = ({ - visible, - onClose, - userRole, -}) => { - // Get user role from authentication context - const { user, isSpeedMode } = useAuth(); - const { message } = App.useApp(); - const role: (typeof USER_ROLES)[keyof typeof USER_ROLES] = (userRole ?? - (isSpeedMode || user?.role === USER_ROLES.ADMIN ? USER_ROLES.ADMIN : USER_ROLES.USER)) as (typeof USER_ROLES)[keyof typeof USER_ROLES]; - - // Get user role from other hooks / context - const currentUserId = "user1"; - const currentTenantId = "tenant1"; - - const memory = useMemory({ - visible, - currentUserId, - currentTenantId, - message, - }); - const { t } = useTranslation("common"); - - // ====================== Clear memory confirmation popup ====================== - const [clearConfirmVisible, setClearConfirmVisible] = React.useState(false); - const [clearTarget, setClearTarget] = React.useState<{ - key: string; - title: string; - } | null>(null); - - const handleClearConfirm = (groupKey: string, groupTitle: string) => { - setClearTarget({ key: groupKey, title: groupTitle }); - setClearConfirmVisible(true); - }; - - const handleClearConfirmOk = async () => { - if (clearTarget) { - await memory.handleClearMemory(clearTarget.key, clearTarget.title); - setClearConfirmVisible(false); - setClearTarget(null); - } - }; - - const handleClearConfirmCancel = () => { - setClearConfirmVisible(false); - setClearTarget(null); - }; - - // ====================== UI rendering functions ====================== - const renderBaseSettings = () => { - const shareOptionLabels: Record = { - [MEMORY_SHARE_STRATEGY.ALWAYS]: t("memoryManageModal.shareOption.always"), - [MEMORY_SHARE_STRATEGY.ASK]: t("memoryManageModal.shareOption.ask"), - [MEMORY_SHARE_STRATEGY.NEVER]: t("memoryManageModal.shareOption.never"), - }; - const dropdownItems = [ - { label: shareOptionLabels[MEMORY_SHARE_STRATEGY.ALWAYS], key: MEMORY_SHARE_STRATEGY.ALWAYS }, - { label: shareOptionLabels[MEMORY_SHARE_STRATEGY.ASK], key: MEMORY_SHARE_STRATEGY.ASK }, - { label: shareOptionLabels[MEMORY_SHARE_STRATEGY.NEVER], key: MEMORY_SHARE_STRATEGY.NEVER }, - ]; - - const handleMenuClick = ({ key }: { key: string }) => { - memory.setShareOption(key as MemoryShareStrategy); - }; - - return ( -
-
- - {t("memoryManageModal.memoryAbility")} - -
- -
-
- - {memory.memoryEnabled && ( -
- - {t("memoryManageModal.agentMemoryShare")} - - - - {shareOptionLabels[memory.shareOption]} - - - -
- )} -
- ); - }; - - // Render add memory input box - const renderAddMemoryInput = (groupKey: string) => { - if (memory.addingMemoryKey !== groupKey) return null; - - return ( -
- memory.setNewMemoryContent(e.target.value)} - placeholder={t("memoryManageModal.inputPlaceholder")} - maxLength={500} - showCount - onPressEnter={memory.confirmAddingMemory} - disabled={memory.isAddingMemory} - className="flex-1" - autoSize={{ minRows: 2, maxRows: 5 }} - /> -
- ); - }; - - // Render empty state - const renderEmptyState = (groupKey: string) => { - const groups = memory.getGroupsForTab(memory.activeTabKey); - const currentGroup = groups.find((g) => g.key === groupKey); - - if (currentGroup && currentGroup.items.length === 0) { - return ( -
- -

{t("memoryManageModal.noMemory")}

-
- ); - } - return null; - }; - - const renderCollapseGroups = ( - groups: { title: string; key: string; items: any[] }[], - showSwitch = false, - tabKey?: MemoryTabKey - ) => { - const paginated = tabKey === MEMORY_TAB_KEYS.AGENT_SHARED || tabKey === MEMORY_TAB_KEYS.USER_AGENT; - const currentPage = paginated ? memory.pageMap[tabKey!] || 1 : 1; - const startIdx = (currentPage - 1) * memory.pageSize; - const sliceGroups = paginated - ? groups.slice(startIdx, startIdx + memory.pageSize) - : groups; - - // If no groups have been loaded (e.g. interface is still in request), directly render empty state to avoid white screen - if (sliceGroups.length === 0) { - return ( -
- -

{t("memoryManageModal.noMemory")}

-
- ); - } - - // Single group scenario, cannot be collapsed (tenant shared, user personal tab) - const isFixedSingle = - sliceGroups.length === 1 && - (tabKey === MEMORY_TAB_KEYS.TENANT || tabKey === MEMORY_TAB_KEYS.USER_PERSONAL); - - if (isFixedSingle) { - return ( -
- {sliceGroups.map((g) => ( -
-
- {g.title} -
-
-
- {g.items.length === 0 && memory.addingMemoryKey !== g.key ? ( -
- -

- {t("memoryManageModal.noMemory")} -

-
- ) : ( -
- {memory.addingMemoryKey === g.key && ( -
- {renderAddMemoryInput(g.key)} -
- )} - ( - } - onClick={() => - memory.handleDeleteMemory(item.id, g.key) - } - />, - ]} - > -
- {item.memory} -
-
- )} - /> -
- )} -
- ))} -
- ); - } - - return ( - <> - ( - - )} - activeKey={memory.openKey} - onChange={(key) => { - if (Array.isArray(key)) { - memory.setOpenKey(key[0] as string); - } else if (key) { - memory.setOpenKey(key as string); - } - }} - style={{ maxHeight: "70vh", overflow: "auto" }} - items={sliceGroups.map((g) => { - const isPlaceholder = /-placeholder$/.test(g.key); - const disabled = !isPlaceholder && !!memory.disabledGroups[g.key]; - return { - key: g.key, - label: ( -
- {g.title} -
e.stopPropagation()} - > - {showSwitch && !isPlaceholder && ( - memory.toggleGroup(g.key, val)} - /> - )} - {/* If the group has no data, hide the "clear memory" button to keep the interface simple */} -
-
- ), - collapsible: disabled ? "disabled" : undefined, - children: ( -
- {memory.addingMemoryKey === g.key && ( -
- {renderAddMemoryInput(g.key)} -
- )} - ( - } - disabled={disabled} - onClick={() => - memory.handleDeleteMemory(item.id, g.key) - } - />, - ]} - > -
- {item.memory} -
-
- )} - /> -
- ), - showArrow: true, - className: "memory-modal-panel", - }; - })} - /> - {paginated && groups.length > memory.pageSize && ( -
- - memory.setPageMap((prev) => ({ ...prev, [tabKey!]: page })) - } - showSizeChanger={false} - /> -
- )} - - ); - }; - - const labelWithIcon: LabelWithIconFunction = (Icon: React.ElementType, text: string) => ( - - - {text} - - ); - - const tabItems = [ - { - key: MEMORY_TAB_KEYS.BASE, - label: labelWithIcon(Settings, t("memoryManageModal.baseSettings")), - children: renderBaseSettings(), - }, - ...(role === USER_ROLES.ADMIN - ? [ - { - key: MEMORY_TAB_KEYS.TENANT, - label: labelWithIcon( - UsersRound, - t("memoryManageModal.tenantShareTab") - ), - children: renderCollapseGroups( - [memory.tenantSharedGroup], - false, - MEMORY_TAB_KEYS.TENANT - ), - disabled: !memory.memoryEnabled, - }, - { - key: MEMORY_TAB_KEYS.AGENT_SHARED, - label: labelWithIcon(Share2, t("memoryManageModal.agentShareTab")), - children: renderCollapseGroups( - memory.agentSharedGroups, - true, - MEMORY_TAB_KEYS.AGENT_SHARED - ), - disabled: !memory.memoryEnabled || memory.shareOption === MEMORY_SHARE_STRATEGY.NEVER, - }, - ] - : []), - { - key: MEMORY_TAB_KEYS.USER_PERSONAL, - label: labelWithIcon(UserRound, t("memoryManageModal.userPersonalTab")), - children: renderCollapseGroups( - [memory.userPersonalGroup], - false, - MEMORY_TAB_KEYS.USER_PERSONAL - ), - disabled: !memory.memoryEnabled, - }, - { - key: MEMORY_TAB_KEYS.USER_AGENT, - label: labelWithIcon(Bot, t("memoryManageModal.userAgentTab")), - children: renderCollapseGroups(memory.userAgentGroups, true, MEMORY_TAB_KEYS.USER_AGENT), - disabled: !memory.memoryEnabled, - }, - ]; - - return ( - <> - - memory.setActiveTabKey(key)} - /> - - - {/* Clear memory confirmation popup */} - - - ); -}; - -export default MemoryManageModal; From 4719c4bee464cbaf1019665fc819cbd66b583543 Mon Sep 17 00:00:00 2001 From: wmc1112 <759659013@qq.com> Date: Mon, 15 Dec 2025 19:06:12 +0800 Subject: [PATCH 07/36] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20The=20agent=20import?= =?UTF-8?q?=20performance=20improvement:=20add=20existing=20tool=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/agent/AgentImportWizard.tsx | 276 +++++++++++++++--- frontend/public/locales/en/common.json | 8 + frontend/public/locales/zh/common.json | 8 + 3 files changed, 254 insertions(+), 38 deletions(-) diff --git a/frontend/components/agent/AgentImportWizard.tsx b/frontend/components/agent/AgentImportWizard.tsx index 06159be23..aa1cc027c 100644 --- a/frontend/components/agent/AgentImportWizard.tsx +++ b/frontend/components/agent/AgentImportWizard.tsx @@ -2,14 +2,14 @@ import React, { useState, useEffect, useRef } from "react"; import { Modal, Steps, Button, Select, Input, Form, Tag, Space, Spin, App, Collapse, Radio } from "antd"; -import { DownloadOutlined, CheckCircleOutlined, CloseCircleOutlined, PlusOutlined } from "@ant-design/icons"; +import { DownloadOutlined, CheckCircleOutlined, CloseCircleOutlined, PlusOutlined, ToolOutlined } from "@ant-design/icons"; import { useTranslation } from "react-i18next"; import { ModelOption } from "@/types/modelConfig"; import { modelService } from "@/services/modelService"; import { getMcpServerList, addMcpServer, updateToolList } from "@/services/mcpService"; import { McpServer, AgentRefreshEvent } from "@/types/agentConfig"; import { ImportAgentData } from "@/hooks/useAgentImport"; -import { importAgent, checkAgentNameConflictBatch, regenerateAgentNameBatch } from "@/services/agentConfigService"; +import { importAgent, checkAgentNameConflictBatch, regenerateAgentNameBatch, fetchTools } from "@/services/agentConfigService"; import log from "@/lib/logger"; export interface AgentImportWizardProps { @@ -125,6 +125,9 @@ export default function AgentImportWizard({ const [loadingMcpServers, setLoadingMcpServers] = useState(false); const [installingMcp, setInstallingMcp] = useState>({}); const [isImporting, setIsImporting] = useState(false); + const [availableTools, setAvailableTools] = useState>([]); + const [missingTools, setMissingTools] = useState>([]); + const [loadingTools, setLoadingTools] = useState(false); // Name conflict checking and renaming // Structure: agentKey -> { hasConflict, conflictAgents, renamedName, renamedDisplayName } @@ -171,6 +174,7 @@ export default function AgentImportWizard({ useEffect(() => { if (visible) { loadLLMModels(); + loadAvailableTools(); } }, [visible]); @@ -196,9 +200,17 @@ export default function AgentImportWizard({ parseConfigFields(); parseMcpServers(); initializeModelSelection(); + computeMissingTools(); } }, [visible, initialData]); + // Recompute missing tools when available tool list changes + useEffect(() => { + if (visible) { + computeMissingTools(); + } + }, [visible, availableTools]); + // Initialize model selection for individual mode const initializeModelSelection = () => { if (!initialData?.agent_info) return; @@ -265,12 +277,12 @@ export default function AgentImportWizard({ }, []); const hasConflict = hasNameConflict || hasDisplayNameConflict; - conflicts[agentKey] = { - hasConflict, - conflictAgents, - renamedName: item.name, - renamedDisplayName: item.display_name || "", - }; + conflicts[agentKey] = { + hasConflict, + conflictAgents, + renamedName: item.name, + renamedDisplayName: item.display_name || "", + }; }); setAgentNameConflicts(conflicts); @@ -457,6 +469,24 @@ export default function AgentImportWizard({ } }; + const loadAvailableTools = async () => { + setLoadingTools(true); + try { + const result = await fetchTools(); + if (result.success) { + setAvailableTools(result.data || []); + } else { + log.warn("Skip tool availability check due to fetch failure"); + setAvailableTools([]); + } + } catch (error) { + log.error("Failed to load available tools:", error); + setAvailableTools([]); + } finally { + setLoadingTools(false); + } + }; + const parseConfigFields = () => { if (!initialData?.agent_info) { setConfigFields([]); @@ -549,6 +579,58 @@ export default function AgentImportWizard({ setConfigValues(initialValues); }; + // Detect missing tools in imported agents compared to available tools + const computeMissingTools = () => { + if (!initialData?.agent_info) { + setMissingTools([]); + return; + } + + const availableNameSet = new Set(); + availableTools.forEach((tool) => { + if (tool.name) { + availableNameSet.add(tool.name.toLowerCase()); + } + if (tool.origin_name) { + availableNameSet.add(tool.origin_name.toLowerCase()); + } + }); + + const missingMap: Record }> = {}; + + Object.entries(initialData.agent_info).forEach(([agentKey, agentInfo]) => { + const agentDisplayName = (agentInfo as any)?.display_name || (agentInfo as any)?.name || `${t("market.install.agent.defaultName", "Agent")} ${agentKey}`; + if (Array.isArray((agentInfo as any)?.tools)) { + (agentInfo as any).tools.forEach((tool: any) => { + const rawName = tool?.name || tool?.origin_name || tool?.class_name; + const name = typeof rawName === "string" ? rawName.trim() : ""; + if (!name) return; + const key = name.toLowerCase(); + if (availableNameSet.has(key)) return; + + if (!missingMap[key]) { + missingMap[key] = { + name, + source: tool?.source, + usage: tool?.usage, + agents: new Set(), + }; + } + missingMap[key].agents.add(agentDisplayName); + }); + } + }); + + const missingList = Object.values(missingMap).map((item) => ({ + name: item.name, + source: item.source, + usage: item.usage, + agents: Array.from(item.agents), + })); + + setMissingTools(missingList); + }; + const parseMcpServers = async () => { // Use mcp_info as the source of truth if (!initialData?.mcp_info || initialData.mcp_info.length === 0) { @@ -833,7 +915,13 @@ export default function AgentImportWizard({ // OR if any agent was successfully renamed (meaning it had a conflict that was resolved) successfullyRenamedAgents.size > 0 ); + const hasMissingTools = !loadingTools && missingTools.length > 0; + // Tools check should be the first step when there are missing tools const steps = [ + hasMissingTools && { + key: "tools", + title: t("market.install.step.missingTools", "Missing Tools"), + }, hasAnyAgentsWithConflicts && { key: "rename", title: t("market.install.step.rename", "Rename Agent"), @@ -863,6 +951,8 @@ export default function AgentImportWizard({ if (currentStepKey === "rename") { return true; + } else if (currentStepKey === "tools") { + return true; } else if (currentStepKey === "model") { if (modelSelectionMode === "unified") { return selectedModelId !== null && selectedModelName !== ""; @@ -926,16 +1016,17 @@ export default function AgentImportWizard({ // Get agents that still have conflicts const agentsWithConflicts = allAgentsWithConflicts.filter( - ([agentKey, conflict]) => conflict.hasConflict + ([, conflict]) => conflict.hasConflict ); - // If no agents had conflicts at all, don't show rename step + // If no agents had conflicts at all, do not show rename step content if (allAgentsWithConflicts.length === 0) { return null; } // Check if all conflicts are resolved - const allConflictsResolved = agentsWithConflicts.length === 0 && allAgentsWithConflicts.length > 0; + const allConflictsResolved = + agentsWithConflicts.length === 0 && allAgentsWithConflicts.length > 0; const hasResolvedAgents = allAgentsWithConflicts.some( ([agentKey]) => successfullyRenamedAgents.has(agentKey) ); @@ -947,7 +1038,10 @@ export default function AgentImportWizard({

- {t("market.install.rename.success", "All agent name conflicts have been resolved. You can proceed to the next step.")} + {t( + "market.install.rename.success", + "All agent name conflicts have been resolved. You can proceed to the next step." + )}

@@ -958,19 +1052,31 @@ export default function AgentImportWizard({

- {t("market.install.rename.partialSuccess", "Some agents have been successfully renamed.")} + {t( + "market.install.rename.partialSuccess", + "Some agents have been successfully renamed." + )}

)}

- {t("market.install.rename.warning", "The agent name or display name conflicts with existing agents. Please rename to proceed.")} + {t( + "market.install.rename.warning", + "The agent name or display name conflicts with existing agents. Please rename to proceed." + )}

- {t("market.install.rename.oneClickDesc", "You can manually edit the names, or click one-click rename to let the selected model regenerate names for all conflicted agents.")} + {t( + "market.install.rename.oneClickDesc", + "You can manually edit the names, or click one-click rename to let the selected model regenerate names for all conflicted agents." + )}

- {t("market.install.rename.note", "Note: If you proceed without renaming, the agent will be created but marked as unavailable due to name conflicts. You can rename it later in the agent list.")} + {t( + "market.install.rename.note", + "Note: If you proceed without renaming, the agent will be created but marked as unavailable due to name conflicts. You can rename it later in the agent list." + )}

- - - - ); } diff --git a/frontend/app/[locale]/chat/internal/chatAttachment.tsx b/frontend/app/[locale]/chat/internal/chatAttachment.tsx index c08ece8f7..ab144ff94 100644 --- a/frontend/app/[locale]/chat/internal/chatAttachment.tsx +++ b/frontend/app/[locale]/chat/internal/chatAttachment.tsx @@ -1,30 +1,31 @@ import { chatConfig } from "@/const/chatConfig"; import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { ExternalLink } from "lucide-react"; -import { storageService, convertImageUrlToApiUrl, extractObjectNameFromUrl } from "@/services/storageService"; -import { message } from "antd"; -import log from "@/lib/logger"; import { - AiFillFileImage, - AiFillFilePdf, - AiFillFileWord, - AiFillFileExcel, - AiFillFilePpt, - AiFillFileZip, - AiFillFileText, - AiFillFileMarkdown, - AiFillHtml5, - AiFillCode, - AiFillFileUnknown, -} from "react-icons/ai"; - + Download, + FileImage, + FileText, + File, + Archive, + Code, + FileSpreadsheet, + Presentation, + FileCode, +} from "lucide-react"; +import { + FilePdfOutlined, + FileWordOutlined, + Html5Outlined, +} from "@ant-design/icons"; import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; + storageService, + convertImageUrlToApiUrl, + extractObjectNameFromUrl, +} from "@/services/storageService"; + +import log from "@/lib/logger"; + +import { Modal, App } from "antd"; import { cn } from "@/lib/utils"; import { AttachmentItem, ChatAttachmentProps } from "@/types/chat"; @@ -40,23 +41,22 @@ const ImageViewer = ({ }) => { if (!isOpen) return null; const { t } = useTranslation("common"); - + // Convert image URL to backend API URL const imageUrl = convertImageUrlToApiUrl(url); return ( - - - - - {t("chatAttachment.imagePreview")} - - -
- Full size -
-
-
+ +
+ img +
+
); }; @@ -78,15 +78,15 @@ const FileViewer = ({ }) => { if (!isOpen) return null; const { t } = useTranslation("common"); + const { message } = App.useApp(); const [isDownloading, setIsDownloading] = useState(false); - // Handle file download const handleDownload = async (e: React.MouseEvent) => { // Prevent dialog from closing immediately e.preventDefault(); e.stopPropagation(); - + // Check if URL is a direct http/https URL that can be accessed directly // Exclude backend API endpoints (containing /api/file/download/) if ( @@ -104,16 +104,18 @@ const FileViewer = ({ setTimeout(() => { document.body.removeChild(link); }, 100); - message.success(t("chatAttachment.downloadSuccess", "File download started")); + message.success( + t("chatAttachment.downloadSuccess", "File download started") + ); setTimeout(() => { onClose(); }, 500); return; } - + // Try to get object_name from props or extract from URL let finalObjectName: string | undefined = objectName; - + if (!finalObjectName && url) { finalObjectName = extractObjectNameFromUrl(url) || undefined; } @@ -131,10 +133,17 @@ const FileViewer = ({ setTimeout(() => { document.body.removeChild(link); }, 100); - message.success(t("chatAttachment.downloadSuccess", "File download started")); + message.success( + t("chatAttachment.downloadSuccess", "File download started") + ); return; } else { - message.error(t("chatAttachment.downloadError", "File object name or URL is missing")); + message.error( + t( + "chatAttachment.downloadError", + "File object name or URL is missing" + ) + ); return; } } @@ -144,7 +153,9 @@ const FileViewer = ({ // Start download (non-blocking, browser handles it) await storageService.downloadFile(finalObjectName, name); // Show success message immediately after triggering download - message.success(t("chatAttachment.downloadSuccess", "File download started")); + message.success( + t("chatAttachment.downloadSuccess", "File download started") + ); // Keep dialog open for a moment to show the message, then close setTimeout(() => { setIsDownloading(false); @@ -165,56 +176,68 @@ const FileViewer = ({ setTimeout(() => { document.body.removeChild(link); }, 100); - message.success(t("chatAttachment.downloadSuccess", "File download started")); + message.success( + t("chatAttachment.downloadSuccess", "File download started") + ); setTimeout(() => { onClose(); }, 500); } catch (fallbackError) { message.error( - t("chatAttachment.downloadError", "Failed to download file. Please try again.") + t( + "chatAttachment.downloadError", + "Failed to download file. Please try again." + ) ); } } else { message.error( - t("chatAttachment.downloadError", "Failed to download file. Please try again.") + t( + "chatAttachment.downloadError", + "Failed to download file. Please try again." + ) ); } } }; return ( - - - - + + {getFileIcon(name, contentType)} + + {name} + + + } + > +
+
+
{getFileIcon(name, contentType)} - {name} - - - -
-
-
- {getFileIcon(name, contentType)} -
-

- {t("chatAttachment.previewNotSupported")} -

-
+

+ {t("chatAttachment.previewNotSupported")} +

+
- -
+ + ); }; @@ -231,56 +254,68 @@ const getFileIcon = (name: string, contentType?: string) => { const fileType = contentType || ""; const iconSize = 32; - // Image file + // Image file - using lucide-react if ( fileType.startsWith("image/") || ["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp"].includes(extension) ) { - return ; + return ; } // Identify by extension name - // Document file + // Document file if (chatConfig.fileIcons.pdf.includes(extension)) { - return ; + // PDF - using ant-design (lucide doesn't have a specific PDF icon) + return ; } if (chatConfig.fileIcons.word.includes(extension)) { - return ; + // Word - using ant-design (lucide doesn't have a specific Word icon) + return ( + + ); } if (chatConfig.fileIcons.text.includes(extension)) { - return ; + // Text - using lucide-react + return ; } if (chatConfig.fileIcons.markdown.includes(extension)) { - return ; + // Markdown - using lucide-react FileText + return ; } // Table file if (chatConfig.fileIcons.excel.includes(extension)) { - return ; + // Excel - using lucide-react + return ; } // Presentation file if (chatConfig.fileIcons.powerpoint.includes(extension)) { - return ; + // PowerPoint - using lucide-react + return ; } // Code file if (chatConfig.fileIcons.html.includes(extension)) { - return ; + // HTML - using ant-design (more specific) + return ; } if (chatConfig.fileIcons.code.includes(extension)) { - return ; + // Code - using lucide-react + return ; } if (chatConfig.fileIcons.json.includes(extension)) { - return ; + // JSON - using lucide-react FileCode + return ; } // Compressed file if (chatConfig.fileIcons.compressed.includes(extension)) { - return ; + // Archive - using lucide-react + return ; } - // Default file icon - return ; - }; + // Default file icon - using lucide-react + return ; +}; // Format file size const formatFileSize = (size: number): string => { @@ -307,12 +342,12 @@ export function ChatAttachment({ // Handle image click const handleImageClick = (url: string) => { + // Use internal preview + setSelectedImage(url); + + // Also call external callback if provided (for compatibility) if (onImageClick) { - // Call external callback onImageClick(url); - } else { - // Use internal preview when there is no external callback - setSelectedImage(url); } }; diff --git a/frontend/app/[locale]/chat/internal/chatInterface.tsx b/frontend/app/[locale]/chat/internal/chatInterface.tsx index f07fdb80c..b0412d550 100644 --- a/frontend/app/[locale]/chat/internal/chatInterface.tsx +++ b/frontend/app/[locale]/chat/internal/chatInterface.tsx @@ -1450,7 +1450,6 @@ export function ChatInterface() { onKeyDown={handleKeyDown} onSelectMessage={handleMessageSelect} selectedMessageId={selectedMessageId} - onImageClick={handleImageClick} attachments={attachments} onAttachmentsChange={handleAttachmentsChange} onFileUpload={handleFileUpload} @@ -1490,38 +1489,6 @@ export function ChatInterface() { - - {/* Image preview */} - {viewingImage && ( -
setViewingImage(null)} - > -
e.stopPropagation()} - > - {t("chatInterface.imagePreview")} { - handleImageError(viewingImage); - }} - /> - -
-
- )} ); } diff --git a/frontend/components/ui/dialog.tsx b/frontend/components/ui/dialog.tsx deleted file mode 100644 index 6c6a9c2b9..000000000 --- a/frontend/components/ui/dialog.tsx +++ /dev/null @@ -1,114 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as DialogPrimitive from "@radix-ui/react-dialog"; -import { X } from "lucide-react"; - -import { cn } from "@/lib/utils"; - -const Dialog = DialogPrimitive.Root; - -const DialogPortal = DialogPrimitive.Portal; - -const DialogOverlay = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; - -const DialogContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - - - {children} - - - Close - - - -)); -DialogContent.displayName = DialogPrimitive.Content.displayName; - -const DialogHeader = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
-); -DialogHeader.displayName = "DialogHeader"; - -const DialogFooter = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
-); -DialogFooter.displayName = "DialogFooter"; - -const DialogTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -DialogTitle.displayName = DialogPrimitive.Title.displayName; - -const DialogDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -DialogDescription.displayName = DialogPrimitive.Description.displayName; - -export { - Dialog, - DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, - DialogDescription, -}; From b683865adc7d200b9de29a9991d79523bdf322b7 Mon Sep 17 00:00:00 2001 From: xuyaqi Date: Tue, 16 Dec 2025 10:22:12 +0800 Subject: [PATCH 13/36] Unify code style (cherry picked from commit b61edc0bc14a32befbfe7f33f01c0553bd49b5b0) --- .editorconfig | 14 ++++++++++++++ frontend/.eslintrc.json | 9 +++++---- frontend/.prettierignore | 39 +++++++++++++++++++++++++++++++++++++++ frontend/.prettierrc | 16 ++++++++++++++++ frontend/package.json | 12 +++++++++--- 5 files changed, 83 insertions(+), 7 deletions(-) create mode 100644 .editorconfig create mode 100644 frontend/.prettierignore create mode 100644 frontend/.prettierrc diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..c4796f5b8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{js,jsx,ts,tsx,json}] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false \ No newline at end of file diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json index 6b10a5b73..e6ee73d8c 100644 --- a/frontend/.eslintrc.json +++ b/frontend/.eslintrc.json @@ -1,6 +1,7 @@ { - "extends": [ - "next/core-web-vitals", - "next/typescript" - ] + "extends": ["next/core-web-vitals", "next/typescript", "prettier"], + "plugins": ["prettier"], + "rules": { + "prettier/prettier": "error" + } } diff --git a/frontend/.prettierignore b/frontend/.prettierignore new file mode 100644 index 000000000..75902765a --- /dev/null +++ b/frontend/.prettierignore @@ -0,0 +1,39 @@ +# Dependencies +node_modules/ + +# Build outputs +.next/ +out/ +dist/ +build/ + +# Environment files +.env* + +# Logs +*.log + +# Package manager files +package-lock.json +yarn.lock +pnpm-lock.yaml + +# IDE files +.vscode/ +.idea/ + +# OS files +.DS_Store +Thumbs.db + +# Generated files +*.d.ts +!src/**/*.d.ts + +# Public assets (usually don't need formatting) +public/ + +# Config files that should maintain specific formatting +tailwind.config.* +next.config.* +postcss.config.* \ No newline at end of file diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 000000000..66853947d --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,16 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": false, + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "always", + "endOfLine": "auto", + "jsxSingleQuote": false, + "htmlWhitespaceSensitivity": "css", + "proseWrap": "preserve", + "quoteProps": "as-needed" +} diff --git a/frontend/package.json b/frontend/package.json index 9fceddc44..ca6aa7194 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,8 +7,11 @@ "build": "next build", "start": "NODE_ENV=production node server.js", "lint": "next lint", + "lint:fix": "next lint --fix", + "format": "prettier --write .", + "format:check": "prettier --check .", "type-check": "tsc --noEmit", - "check-all": "npm run type-check && npm run lint && npm run build" + "check-all": "npm run type-check && npm run lint && npm run format:check && npm run build" }, "dependencies": { "@ant-design/icons": "^6.0.0", @@ -61,7 +64,7 @@ "katex": "^0.16.11", "lucide-react": "^0.454.0", "mermaid": "^11.12.0", - "next": "15.5.7", + "next": "^15.5.9", "next-i18next": "^15.4.2", "next-themes": "^0.4.4", "react": "18.2.0", @@ -75,7 +78,7 @@ "react-markdown": "^8.0.7", "react-organizational-chart": "^2.2.1", "react-resizable-panels": "^2.1.7", - "react-syntax-highlighter": "^15.5.0", + "react-syntax-highlighter": "^16.1.0", "reactflow": "^11.11.4", "recharts": "2.15.0", "rehype-katex": "^6.0.3", @@ -98,7 +101,10 @@ "@types/react-dom": "18.3.6", "eslint": "^9.34.0", "eslint-config-next": "15.5.7", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", "postcss": "^8", + "prettier": "^3.2.5", "tailwindcss": "^3.4.17", "typescript": "5.8.3" } From f0c70ecfe063ffc630f4ce743bf997e3aef72a02 Mon Sep 17 00:00:00 2001 From: xuyaqi Date: Tue, 16 Dec 2025 10:22:12 +0800 Subject: [PATCH 14/36] Unify code style (cherry picked from commit b61edc0bc14a32befbfe7f33f01c0553bd49b5b0) --- .editorconfig | 14 ++++++++++++++ frontend/.eslintrc.json | 9 +++++---- frontend/.prettierignore | 39 +++++++++++++++++++++++++++++++++++++++ frontend/.prettierrc | 16 ++++++++++++++++ frontend/package.json | 12 +++++++++--- 5 files changed, 83 insertions(+), 7 deletions(-) create mode 100644 .editorconfig create mode 100644 frontend/.prettierignore create mode 100644 frontend/.prettierrc diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..c4796f5b8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{js,jsx,ts,tsx,json}] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false \ No newline at end of file diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json index 6b10a5b73..e6ee73d8c 100644 --- a/frontend/.eslintrc.json +++ b/frontend/.eslintrc.json @@ -1,6 +1,7 @@ { - "extends": [ - "next/core-web-vitals", - "next/typescript" - ] + "extends": ["next/core-web-vitals", "next/typescript", "prettier"], + "plugins": ["prettier"], + "rules": { + "prettier/prettier": "error" + } } diff --git a/frontend/.prettierignore b/frontend/.prettierignore new file mode 100644 index 000000000..75902765a --- /dev/null +++ b/frontend/.prettierignore @@ -0,0 +1,39 @@ +# Dependencies +node_modules/ + +# Build outputs +.next/ +out/ +dist/ +build/ + +# Environment files +.env* + +# Logs +*.log + +# Package manager files +package-lock.json +yarn.lock +pnpm-lock.yaml + +# IDE files +.vscode/ +.idea/ + +# OS files +.DS_Store +Thumbs.db + +# Generated files +*.d.ts +!src/**/*.d.ts + +# Public assets (usually don't need formatting) +public/ + +# Config files that should maintain specific formatting +tailwind.config.* +next.config.* +postcss.config.* \ No newline at end of file diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 000000000..66853947d --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,16 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": false, + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "always", + "endOfLine": "auto", + "jsxSingleQuote": false, + "htmlWhitespaceSensitivity": "css", + "proseWrap": "preserve", + "quoteProps": "as-needed" +} diff --git a/frontend/package.json b/frontend/package.json index 9fceddc44..ca6aa7194 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,8 +7,11 @@ "build": "next build", "start": "NODE_ENV=production node server.js", "lint": "next lint", + "lint:fix": "next lint --fix", + "format": "prettier --write .", + "format:check": "prettier --check .", "type-check": "tsc --noEmit", - "check-all": "npm run type-check && npm run lint && npm run build" + "check-all": "npm run type-check && npm run lint && npm run format:check && npm run build" }, "dependencies": { "@ant-design/icons": "^6.0.0", @@ -61,7 +64,7 @@ "katex": "^0.16.11", "lucide-react": "^0.454.0", "mermaid": "^11.12.0", - "next": "15.5.7", + "next": "^15.5.9", "next-i18next": "^15.4.2", "next-themes": "^0.4.4", "react": "18.2.0", @@ -75,7 +78,7 @@ "react-markdown": "^8.0.7", "react-organizational-chart": "^2.2.1", "react-resizable-panels": "^2.1.7", - "react-syntax-highlighter": "^15.5.0", + "react-syntax-highlighter": "^16.1.0", "reactflow": "^11.11.4", "recharts": "2.15.0", "rehype-katex": "^6.0.3", @@ -98,7 +101,10 @@ "@types/react-dom": "18.3.6", "eslint": "^9.34.0", "eslint-config-next": "15.5.7", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", "postcss": "^8", + "prettier": "^3.2.5", "tailwindcss": "^3.4.17", "typescript": "5.8.3" } From 6cdaba321da81a79976dbd86ff2c3a1551f3b996 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=9B=E9=94=90?= Date: Tue, 16 Dec 2025 10:46:07 +0800 Subject: [PATCH 15/36] uersguide docs change --- doc/docs/.vitepress/config.mts | 4 ++-- doc/docs/en/user-guide/memory-management.md | 6 +++--- doc/docs/zh/user-guide/agent-development.md | 2 +- doc/docs/zh/user-guide/agent-space.md | 2 +- doc/docs/zh/user-guide/knowledge-base.md | 2 +- doc/docs/zh/user-guide/memory-management.md | 2 +- doc/docs/zh/user-guide/model-management.md | 2 +- doc/docs/zh/user-guide/quick-setup.md | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/doc/docs/.vitepress/config.mts b/doc/docs/.vitepress/config.mts index 47ad56417..884fa28bb 100644 --- a/doc/docs/.vitepress/config.mts +++ b/doc/docs/.vitepress/config.mts @@ -90,7 +90,7 @@ export default defineConfig({ link: "/en/user-guide/knowledge-base", }, { text: "MCP Tools", link: "/en/user-guide/mcp-tools" }, - { text: "Monitoring & Operations", link: "/en/user-guide/monitor" }, + { text: "Monitoring & Ops", link: "/en/user-guide/monitor" }, { text: "Model Management", link: "/en/user-guide/model-management", @@ -269,7 +269,7 @@ export default defineConfig({ items: [ { text: "首页", link: "/zh/user-guide/home-page" }, { text: "开始问答", link: "/zh/user-guide/start-chat" }, - { text: "快速设置", link: "/zh/user-guide/quick-setup" }, + { text: "快速配置", link: "/zh/user-guide/quick-setup" }, { text: "智能体空间", link: "/zh/user-guide/agent-space" }, { text: "智能体市场", link: "/zh/user-guide/agent-market" }, { diff --git a/doc/docs/en/user-guide/memory-management.md b/doc/docs/en/user-guide/memory-management.md index 6e1330b78..0caffb7e1 100644 --- a/doc/docs/en/user-guide/memory-management.md +++ b/doc/docs/en/user-guide/memory-management.md @@ -84,9 +84,9 @@ When an agent retrieves memory it follows this order (high ➝ low): The system takes care of most work for you: -- **Smart extraction:** Detects important facts in conversations and stores them automatically. -- **Context injection:** Retrieves the most relevant memories and adds them to prompts silently. -- **Incremental updates:** Refreshes or removes outdated memories so the store stays clean. +- **Smart extraction:** Detects key facts in conversations, creates memory entries automatically, and stores them at the right level—no manual input needed. +- **Automatic context embedding:** Retrieves the most relevant memories and implicitly injects them into the conversation context so agents respond with better accuracy. +- **Incremental updates:** Gradually refreshes or removes outdated memories to keep the store clean, timely, and reliable. ## ✋ Manual Memory Operations diff --git a/doc/docs/zh/user-guide/agent-development.md b/doc/docs/zh/user-guide/agent-development.md index 885931c1e..af3832ed8 100644 --- a/doc/docs/zh/user-guide/agent-development.md +++ b/doc/docs/zh/user-guide/agent-development.md @@ -161,4 +161,4 @@ Nexent 支持您快速便捷地使用第三方 MCP 工具,丰富 Agent 能力 2. 在 **[开始问答](./start-chat)** 中与智能体进行交互 3. 在 **[记忆管理](./memory-management)** 配置记忆以提升智能体的个性化能力 -如果您在智能体开发过程中遇到任何问题,请参考我们的 **[常见问题](../quick-start/faq)** 或在 [GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions) 中进行提问获取支持。 +如果您在使用程中遇到任何问题,请参考我们的 **[常见问题](../quick-start/faq)** 或在 [GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions) 中进行提问获取支持。 diff --git a/doc/docs/zh/user-guide/agent-space.md b/doc/docs/zh/user-guide/agent-space.md index 1ceede622..ff9cc7219 100644 --- a/doc/docs/zh/user-guide/agent-space.md +++ b/doc/docs/zh/user-guide/agent-space.md @@ -67,4 +67,4 @@ 2. 继续 **[智能体开发](./agent-development)** 创建更多智能体 3. 配置 **[记忆管理](./memory-management)** 以提升智能体的记忆能力 -如果您在配置过程中遇到任何问题,请参考我们的 **[常见问题](../quick-start/faq)** 或在[GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions)中进行提问获取支持。 +如果您在使用过程中遇到任何问题,请参考我们的 **[常见问题](../quick-start/faq)** 或在[GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions)中进行提问获取支持。 diff --git a/doc/docs/zh/user-guide/knowledge-base.md b/doc/docs/zh/user-guide/knowledge-base.md index 7277032fd..c28f878e1 100644 --- a/doc/docs/zh/user-guide/knowledge-base.md +++ b/doc/docs/zh/user-guide/knowledge-base.md @@ -79,4 +79,4 @@ Nexent支持多种文件格式,包括: 1. **[智能体开发](./agent-development)** - 创建和配置智能体 2. **[开始问答](./start-chat)** - 与智能体进行交互 -如果您在知识库配置过程中遇到任何问题,请参考我们的 **[常见问题](../quick-start/faq)** 或在[GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions)中进行提问获取支持。 \ No newline at end of file +如果您在使用过程中遇到任何问题,请参考我们的 **[常见问题](../quick-start/faq)** 或在[GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions)中进行提问获取支持。 \ No newline at end of file diff --git a/doc/docs/zh/user-guide/memory-management.md b/doc/docs/zh/user-guide/memory-management.md index 5f745ad1f..b8b9915e2 100644 --- a/doc/docs/zh/user-guide/memory-management.md +++ b/doc/docs/zh/user-guide/memory-management.md @@ -157,4 +157,4 @@ Nexent采用四层记忆存储架构,不同层级的记忆有不同的作用 2. 在 **[智能体空间](./agent-space)** 中管理您的智能体 3. 继续 **[智能体开发](./agent-development)** 创建更多智能体 -如果您在配置过程中遇到任何问题,请参考我们的 **[常见问题](../quick-start/faq)** 或在[GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions)中进行提问获取支持。 +如果您在使用过程中遇到任何问题,请参考我们的 **[常见问题](../quick-start/faq)** 或在[GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions)中进行提问获取支持。 diff --git a/doc/docs/zh/user-guide/model-management.md b/doc/docs/zh/user-guide/model-management.md index a57ab32f1..3e23342ca 100644 --- a/doc/docs/zh/user-guide/model-management.md +++ b/doc/docs/zh/user-guide/model-management.md @@ -244,4 +244,4 @@ Nexent 支持任何 **遵循OpenAI API规范** 的大语言模型供应商,包 1. **[知识库](./knowledge-base)** - 创建和管理知识库。 2. **[智能体开发](./agent-development)** - 创建和配置智能体。 -如果您在模型配置过程中遇到任何问题,请参考我们的 **[常见问题](../quick-start/faq)** 或在[GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions)中进行提问获取支持。 +如果您在使用过程中遇到任何问题,请参考我们的 **[常见问题](../quick-start/faq)** 或在[GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions)中进行提问获取支持。 diff --git a/doc/docs/zh/user-guide/quick-setup.md b/doc/docs/zh/user-guide/quick-setup.md index 736b55a19..44d00b335 100644 --- a/doc/docs/zh/user-guide/quick-setup.md +++ b/doc/docs/zh/user-guide/quick-setup.md @@ -50,4 +50,4 @@ 2. 在 **[开始问答](./start-chat)** 中与智能体进行交互 3. 配置 **[记忆管理](./memory-management)** 以提升智能体的记忆能力 -如果您在配置过程中遇到任何问题,请参考我们的 **[常见问题](../quick-start/faq)** 或在[GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions)中进行提问获取支持。 +如果您在使用过程中遇到任何问题,请参考我们的 **[常见问题](../quick-start/faq)** 或在[GitHub Discussions](https://github.com/ModelEngine-Group/nexent/discussions)中进行提问获取支持。 From 478d775d8b71d10c4ec92b00548efab4245e15d4 Mon Sep 17 00:00:00 2001 From: xuyaqi Date: Tue, 16 Dec 2025 11:48:57 +0800 Subject: [PATCH 16/36] Revert "Unify code style" This reverts commit b683865adc7d200b9de29a9991d79523bdf322b7. --- .editorconfig | 14 -------------- frontend/.eslintrc.json | 9 ++++----- frontend/.prettierignore | 39 --------------------------------------- frontend/.prettierrc | 16 ---------------- frontend/package.json | 12 +++--------- 5 files changed, 7 insertions(+), 83 deletions(-) delete mode 100644 .editorconfig delete mode 100644 frontend/.prettierignore delete mode 100644 frontend/.prettierrc diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index c4796f5b8..000000000 --- a/.editorconfig +++ /dev/null @@ -1,14 +0,0 @@ -root = true - -[*] -charset = utf-8 -end_of_line = lf -insert_final_newline = true -trim_trailing_whitespace = true - -[*.{js,jsx,ts,tsx,json}] -indent_style = space -indent_size = 2 - -[*.md] -trim_trailing_whitespace = false \ No newline at end of file diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json index e6ee73d8c..6b10a5b73 100644 --- a/frontend/.eslintrc.json +++ b/frontend/.eslintrc.json @@ -1,7 +1,6 @@ { - "extends": ["next/core-web-vitals", "next/typescript", "prettier"], - "plugins": ["prettier"], - "rules": { - "prettier/prettier": "error" - } + "extends": [ + "next/core-web-vitals", + "next/typescript" + ] } diff --git a/frontend/.prettierignore b/frontend/.prettierignore deleted file mode 100644 index 75902765a..000000000 --- a/frontend/.prettierignore +++ /dev/null @@ -1,39 +0,0 @@ -# Dependencies -node_modules/ - -# Build outputs -.next/ -out/ -dist/ -build/ - -# Environment files -.env* - -# Logs -*.log - -# Package manager files -package-lock.json -yarn.lock -pnpm-lock.yaml - -# IDE files -.vscode/ -.idea/ - -# OS files -.DS_Store -Thumbs.db - -# Generated files -*.d.ts -!src/**/*.d.ts - -# Public assets (usually don't need formatting) -public/ - -# Config files that should maintain specific formatting -tailwind.config.* -next.config.* -postcss.config.* \ No newline at end of file diff --git a/frontend/.prettierrc b/frontend/.prettierrc deleted file mode 100644 index 66853947d..000000000 --- a/frontend/.prettierrc +++ /dev/null @@ -1,16 +0,0 @@ -{ - "semi": true, - "trailingComma": "es5", - "singleQuote": false, - "printWidth": 80, - "tabWidth": 2, - "useTabs": false, - "bracketSpacing": true, - "bracketSameLine": false, - "arrowParens": "always", - "endOfLine": "auto", - "jsxSingleQuote": false, - "htmlWhitespaceSensitivity": "css", - "proseWrap": "preserve", - "quoteProps": "as-needed" -} diff --git a/frontend/package.json b/frontend/package.json index ca6aa7194..9fceddc44 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,11 +7,8 @@ "build": "next build", "start": "NODE_ENV=production node server.js", "lint": "next lint", - "lint:fix": "next lint --fix", - "format": "prettier --write .", - "format:check": "prettier --check .", "type-check": "tsc --noEmit", - "check-all": "npm run type-check && npm run lint && npm run format:check && npm run build" + "check-all": "npm run type-check && npm run lint && npm run build" }, "dependencies": { "@ant-design/icons": "^6.0.0", @@ -64,7 +61,7 @@ "katex": "^0.16.11", "lucide-react": "^0.454.0", "mermaid": "^11.12.0", - "next": "^15.5.9", + "next": "15.5.7", "next-i18next": "^15.4.2", "next-themes": "^0.4.4", "react": "18.2.0", @@ -78,7 +75,7 @@ "react-markdown": "^8.0.7", "react-organizational-chart": "^2.2.1", "react-resizable-panels": "^2.1.7", - "react-syntax-highlighter": "^16.1.0", + "react-syntax-highlighter": "^15.5.0", "reactflow": "^11.11.4", "recharts": "2.15.0", "rehype-katex": "^6.0.3", @@ -101,10 +98,7 @@ "@types/react-dom": "18.3.6", "eslint": "^9.34.0", "eslint-config-next": "15.5.7", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-prettier": "^5.1.3", "postcss": "^8", - "prettier": "^3.2.5", "tailwindcss": "^3.4.17", "typescript": "5.8.3" } From cdd75dea1bcb7fb3394823ea923b382390c7b71a Mon Sep 17 00:00:00 2001 From: xuyaqi Date: Tue, 16 Dec 2025 11:49:40 +0800 Subject: [PATCH 17/36] Delete radix-ui/react-dialog, replace it with ant design modal --- frontend/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/package.json b/frontend/package.json index 9fceddc44..4bd21b972 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,7 +23,6 @@ "@radix-ui/react-checkbox": "^1.1.3", "@radix-ui/react-collapsible": "^1.1.2", "@radix-ui/react-context-menu": "^2.2.4", - "@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-dropdown-menu": "^2.1.4", "@radix-ui/react-hover-card": "^1.1.4", "@radix-ui/react-label": "^2.1.1", From 94a8097255d00a37138b47252e5062782ffcf727 Mon Sep 17 00:00:00 2001 From: wmc1112 <759659013@qq.com> Date: Tue, 16 Dec 2025 11:26:27 +0800 Subject: [PATCH 18/36] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20The=20tool=20status?= =?UTF-8?q?=20is=20not=20synchronized=20when=20the=20service=20starts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/config_service.py | 20 +- backend/runtime_service.py | 21 +-- .../services/tool_configuration_service.py | 68 +------ .../test_tool_configuration_service.py | 171 ------------------ 4 files changed, 6 insertions(+), 274 deletions(-) diff --git a/backend/config_service.py b/backend/config_service.py index f98c7b155..961c59b91 100644 --- a/backend/config_service.py +++ b/backend/config_service.py @@ -12,30 +12,14 @@ from apps.config_app import app from utils.logging_utils import configure_logging, configure_elasticsearch_logging -from services.tool_configuration_service import initialize_tools_on_startup + configure_logging(logging.INFO) configure_elasticsearch_logging() logger = logging.getLogger("config_service") -async def startup_initialization(): - """ - Perform initialization tasks during server startup - """ +if __name__ == "__main__": logger.info("Starting server initialization...") logger.info(f"APP version is: {APP_VERSION}") - try: - # Initialize tools on startup - service layer handles detailed logging - await initialize_tools_on_startup() - logger.info("Server initialization completed successfully!") - - except Exception as e: - logger.error(f"Server initialization failed: {str(e)}") - # Don't raise the exception to allow server to start even if initialization fails - logger.warning("Server will continue to start despite initialization issues") - - -if __name__ == "__main__": - asyncio.run(startup_initialization()) uvicorn.run(app, host="0.0.0.0", port=5010, log_level="info") diff --git a/backend/runtime_service.py b/backend/runtime_service.py index faa3d2981..eb065aa3d 100644 --- a/backend/runtime_service.py +++ b/backend/runtime_service.py @@ -12,31 +12,16 @@ from apps.runtime_app import app from utils.logging_utils import configure_logging, configure_elasticsearch_logging -from services.tool_configuration_service import initialize_tools_on_startup + configure_logging(logging.INFO) configure_elasticsearch_logging() -logger = logging.getLogger("runtime_service") +logger = logging.getLogger("config_service") -async def startup_initialization(): - """ - Perform initialization tasks during server startup - """ +if __name__ == "__main__": logger.info("Starting server initialization...") logger.info(f"APP version is: {APP_VERSION}") - try: - # Initialize tools on startup - service layer handles detailed logging - await initialize_tools_on_startup() - logger.info("Server initialization completed successfully!") - except Exception as e: - logger.error(f"Server initialization failed: {str(e)}") - # Don't raise the exception to allow server to start even if initialization fails - logger.warning("Server will continue to start despite initialization issues") - - -if __name__ == "__main__": - asyncio.run(startup_initialization()) uvicorn.run(app, host="0.0.0.0", port=5014, log_level="info") diff --git a/backend/services/tool_configuration_service.py b/backend/services/tool_configuration_service.py index 66298c8c5..97a386a29 100644 --- a/backend/services/tool_configuration_service.py +++ b/backend/services/tool_configuration_service.py @@ -11,7 +11,7 @@ import jsonref from mcpadapt.smolagents_adapter import _sanitize_function_name -from consts.const import DEFAULT_USER_ID, LOCAL_MCP_SERVER, DATA_PROCESS_SERVICE +from consts.const import LOCAL_MCP_SERVER, DATA_PROCESS_SERVICE from consts.exceptions import MCPConnectionError, ToolExecutionException, NotFoundException from consts.model import ToolInstanceInfoRequest, ToolInfo, ToolSourceEnum, ToolValidateRequest from database.remote_mcp_db import get_mcp_records_by_tenant, get_mcp_server_by_name_and_tenant @@ -22,7 +22,6 @@ update_tool_table_from_scan_tool_list, search_last_tool_instance_by_tool_id, ) -from database.user_tenant_db import get_all_tenant_ids from services.file_management_service import get_llm_model from services.vectordatabase_service import get_embedding_model, get_vector_db_core from services.tenant_config_service import get_selected_knowledge_list, build_knowledge_name_mapping @@ -367,71 +366,6 @@ async def list_all_tools(tenant_id: str): return formatted_tools -async def initialize_tools_on_startup(): - """ - Initialize and scan all tools during server startup for all tenants - - This function scans all available tools (local, LangChain, and MCP) - and updates the database with the latest tool information for all tenants. - """ - - logger.info("Starting tool initialization on server startup...") - - try: - # Get all tenant IDs from the database - tenant_ids = get_all_tenant_ids() - - if not tenant_ids: - logger.warning("No tenants found in database, skipping tool initialization") - return - - logger.info(f"Found {len(tenant_ids)} tenants: {tenant_ids}") - - total_tools = 0 - successful_tenants = 0 - failed_tenants = [] - - # Process each tenant - for tenant_id in tenant_ids: - try: - logger.info(f"Initializing tools for tenant: {tenant_id}") - - # Add timeout to prevent hanging during startup - try: - await asyncio.wait_for( - update_tool_list(tenant_id=tenant_id, user_id=DEFAULT_USER_ID), - timeout=60.0 # 60 seconds timeout per tenant - ) - - # Get the count of tools for this tenant - tools_info = query_all_tools(tenant_id) - tenant_tool_count = len(tools_info) - total_tools += tenant_tool_count - successful_tenants += 1 - - logger.info(f"Tenant {tenant_id}: {tenant_tool_count} tools initialized") - - except asyncio.TimeoutError: - logger.error(f"Tool initialization timed out for tenant {tenant_id}") - failed_tenants.append(f"{tenant_id} (timeout)") - - except Exception as e: - logger.error(f"Tool initialization failed for tenant {tenant_id}: {str(e)}") - failed_tenants.append(f"{tenant_id} (error: {str(e)})") - - # Log final results - logger.info("Tool initialization completed!") - logger.info(f"Total tools available across all tenants: {total_tools}") - logger.info(f"Successfully processed: {successful_tenants}/{len(tenant_ids)} tenants") - - if failed_tenants: - logger.warning(f"Failed tenants: {', '.join(failed_tenants)}") - - except Exception as e: - logger.error(f"❌ Tool initialization failed: {str(e)}") - raise - - def load_last_tool_config_impl(tool_id: int, tenant_id: str, user_id: str): """ Load the last tool configuration for a given tool ID diff --git a/test/backend/services/test_tool_configuration_service.py b/test/backend/services/test_tool_configuration_service.py index cf12c9805..b59d18449 100644 --- a/test/backend/services/test_tool_configuration_service.py +++ b/test/backend/services/test_tool_configuration_service.py @@ -1290,177 +1290,6 @@ def __init__(self): assert mock_build_tool_info.call_count == 2 -class TestInitializeToolsOnStartup: - """Test cases for initialize_tools_on_startup function""" - - @patch('backend.services.tool_configuration_service.get_all_tenant_ids') - @patch('backend.services.tool_configuration_service.update_tool_list') - @patch('backend.services.tool_configuration_service.query_all_tools') - @patch('backend.services.tool_configuration_service.logger') - async def test_initialize_tools_on_startup_no_tenants(self, mock_logger, mock_query_tools, mock_update_tool_list, mock_get_tenants): - """Test initialize_tools_on_startup when no tenants are found""" - # Mock get_all_tenant_ids to return empty list - mock_get_tenants.return_value = [] - - # Import and call the function - from backend.services.tool_configuration_service import initialize_tools_on_startup - await initialize_tools_on_startup() - - # Verify warning was logged - mock_logger.warning.assert_called_with( - "No tenants found in database, skipping tool initialization") - mock_update_tool_list.assert_not_called() - - @patch('backend.services.tool_configuration_service.get_all_tenant_ids') - @patch('backend.services.tool_configuration_service.update_tool_list') - @patch('backend.services.tool_configuration_service.query_all_tools') - @patch('backend.services.tool_configuration_service.logger') - async def test_initialize_tools_on_startup_success(self, mock_logger, mock_query_tools, mock_update_tool_list, mock_get_tenants): - """Test successful tool initialization for all tenants""" - # Mock tenant IDs - tenant_ids = ["tenant_1", "tenant_2", "default_tenant"] - mock_get_tenants.return_value = tenant_ids - - # Mock update_tool_list to succeed - mock_update_tool_list.return_value = None - - # Mock query_all_tools to return mock tools - mock_tools = [ - {"tool_id": "tool_1", "name": "Test Tool 1"}, - {"tool_id": "tool_2", "name": "Test Tool 2"} - ] - mock_query_tools.return_value = mock_tools - - # Import and call the function - from backend.services.tool_configuration_service import initialize_tools_on_startup - await initialize_tools_on_startup() - - # Verify update_tool_list was called for each tenant - assert mock_update_tool_list.call_count == len(tenant_ids) - - # Verify success logging - mock_logger.info.assert_any_call("Tool initialization completed!") - mock_logger.info.assert_any_call( - "Total tools available across all tenants: 6") # 2 tools * 3 tenants - mock_logger.info.assert_any_call("Successfully processed: 3/3 tenants") - - @patch('backend.services.tool_configuration_service.get_all_tenant_ids') - @patch('backend.services.tool_configuration_service.update_tool_list') - @patch('backend.services.tool_configuration_service.logger') - async def test_initialize_tools_on_startup_timeout(self, mock_logger, mock_update_tool_list, mock_get_tenants): - """Test tool initialization timeout scenario""" - tenant_ids = ["tenant_1", "tenant_2"] - mock_get_tenants.return_value = tenant_ids - - # Mock update_tool_list to timeout - mock_update_tool_list.side_effect = asyncio.TimeoutError() - - # Import and call the function - from backend.services.tool_configuration_service import initialize_tools_on_startup - await initialize_tools_on_startup() - - # Verify timeout error was logged for each tenant - assert mock_logger.error.call_count == len(tenant_ids) - for call in mock_logger.error.call_args_list: - assert "timed out" in str(call) - - # Verify failed tenants were logged - mock_logger.warning.assert_called_once() - warning_call = mock_logger.warning.call_args[0][0] - assert "Failed tenants:" in warning_call - assert "tenant_1 (timeout)" in warning_call - assert "tenant_2 (timeout)" in warning_call - - @patch('backend.services.tool_configuration_service.get_all_tenant_ids') - @patch('backend.services.tool_configuration_service.update_tool_list') - @patch('backend.services.tool_configuration_service.logger') - async def test_initialize_tools_on_startup_exception(self, mock_logger, mock_update_tool_list, mock_get_tenants): - """Test tool initialization with exception during processing""" - tenant_ids = ["tenant_1", "tenant_2"] - mock_get_tenants.return_value = tenant_ids - - # Mock update_tool_list to raise exception - mock_update_tool_list.side_effect = Exception( - "Database connection failed") - - # Import and call the function - from backend.services.tool_configuration_service import initialize_tools_on_startup - await initialize_tools_on_startup() - - # Verify exception error was logged for each tenant - assert mock_logger.error.call_count == len(tenant_ids) - for call in mock_logger.error.call_args_list: - assert "Tool initialization failed" in str(call) - assert "Database connection failed" in str(call) - - # Verify failed tenants were logged - mock_logger.warning.assert_called_once() - warning_call = mock_logger.warning.call_args[0][0] - assert "Failed tenants:" in warning_call - assert "tenant_1 (error: Database connection failed)" in warning_call - assert "tenant_2 (error: Database connection failed)" in warning_call - - @patch('backend.services.tool_configuration_service.get_all_tenant_ids') - @patch('backend.services.tool_configuration_service.logger') - async def test_initialize_tools_on_startup_critical_exception(self, mock_logger, mock_get_tenants): - """Test tool initialization when get_all_tenant_ids raises exception""" - # Mock get_all_tenant_ids to raise exception - mock_get_tenants.side_effect = Exception("Database connection failed") - - # Import and call the function - from backend.services.tool_configuration_service import initialize_tools_on_startup - - # Should raise the exception - with pytest.raises(Exception, match="Database connection failed"): - await initialize_tools_on_startup() - - # Verify critical error was logged - mock_logger.error.assert_called_with( - "❌ Tool initialization failed: Database connection failed") - - @patch('backend.services.tool_configuration_service.get_all_tenant_ids') - @patch('backend.services.tool_configuration_service.update_tool_list') - @patch('backend.services.tool_configuration_service.query_all_tools') - @patch('backend.services.tool_configuration_service.logger') - async def test_initialize_tools_on_startup_mixed_results(self, mock_logger, mock_query_tools, mock_update_tool_list, mock_get_tenants): - """Test tool initialization with mixed success and failure results""" - tenant_ids = ["tenant_1", "tenant_2", "tenant_3"] - mock_get_tenants.return_value = tenant_ids - - # Mock update_tool_list with mixed results - def side_effect(*args, **kwargs): - tenant_id = kwargs.get('tenant_id') - if tenant_id == "tenant_1": - return None # Success - elif tenant_id == "tenant_2": - raise asyncio.TimeoutError() # Timeout - else: # tenant_3 - raise Exception("Connection error") # Exception - - mock_update_tool_list.side_effect = side_effect - - # Mock query_all_tools for successful tenant - mock_tools = [{"tool_id": "tool_1", "name": "Test Tool"}] - mock_query_tools.return_value = mock_tools - - # Import and call the function - from backend.services.tool_configuration_service import initialize_tools_on_startup - await initialize_tools_on_startup() - - # Verify mixed results logging - mock_logger.info.assert_any_call("Tool initialization completed!") - mock_logger.info.assert_any_call( - "Total tools available across all tenants: 1") - mock_logger.info.assert_any_call("Successfully processed: 1/3 tenants") - - # Verify failed tenants were logged - mock_logger.warning.assert_called_once() - warning_call = mock_logger.warning.call_args[0][0] - assert "Failed tenants:" in warning_call - assert "tenant_2 (timeout)" in warning_call - assert "tenant_3 (error: Connection error)" in warning_call - - class TestLoadLastToolConfigImpl: """Test load_last_tool_config_impl function""" From 4a705063569522e3f5d3d55c32d284ff4b05daa0 Mon Sep 17 00:00:00 2001 From: xuyaqi Date: Tue, 16 Dec 2025 14:35:47 +0800 Subject: [PATCH 19/36] Unify global modals: step4: Update useConfirmModals, make sure every modal.confirm use same style --- .../components/AgentSetupOrchestrator.tsx | 6 +--- .../agents/components/McpConfigModal.tsx | 10 +++--- .../chat/components/chatLeftSidebar.tsx | 10 ++---- .../knowledges/KnowledgeBaseConfiguration.tsx | 4 +-- .../[locale]/space/components/AgentCard.tsx | 9 +++--- frontend/components/auth/avatarDropdown.tsx | 11 +++---- frontend/components/auth/sessionListeners.tsx | 7 ++++ frontend/hooks/useConfirmModal.ts | 32 ++++++++++++++----- frontend/types/setupConfig.ts | 13 -------- 9 files changed, 49 insertions(+), 53 deletions(-) delete mode 100644 frontend/types/setupConfig.ts diff --git a/frontend/app/[locale]/agents/components/AgentSetupOrchestrator.tsx b/frontend/app/[locale]/agents/components/AgentSetupOrchestrator.tsx index 225b20980..b1a01bfe9 100644 --- a/frontend/app/[locale]/agents/components/AgentSetupOrchestrator.tsx +++ b/frontend/app/[locale]/agents/components/AgentSetupOrchestrator.tsx @@ -2003,7 +2003,7 @@ export default function AgentSetupOrchestrator({ content: t("agentConfig.agents.copyConfirmContent", { name: agent?.display_name || agent?.name || "", }), - onConfirm: () => handleCopyAgentFromList(agent), + onOk: () => handleCopyAgentFromList(agent), }); }; @@ -2326,10 +2326,6 @@ export default function AgentSetupOrchestrator({ >
-
{t("businessLogic.config.modal.deleteContent", { diff --git a/frontend/app/[locale]/agents/components/McpConfigModal.tsx b/frontend/app/[locale]/agents/components/McpConfigModal.tsx index 4561c4625..d6405f11c 100644 --- a/frontend/app/[locale]/agents/components/McpConfigModal.tsx +++ b/frontend/app/[locale]/agents/components/McpConfigModal.tsx @@ -34,6 +34,7 @@ import { checkMcpServerHealth, } from "@/services/mcpService"; import { McpServer, McpTool } from "@/types/agentConfig"; +import { useConfirmModal } from "@/hooks/useConfirmModal"; import log from "@/lib/logger"; const { Text, Title } = Typography; @@ -43,7 +44,8 @@ export default function McpConfigModal({ onCancel, }: McpConfigModalProps) { const { t } = useTranslation("common"); - const { message, modal } = App.useApp(); + const { message } = App.useApp(); + const { confirm } = useConfirmModal(); const [serverList, setServerList] = useState([]); const [loading, setLoading] = useState(false); const [addingServer, setAddingServer] = useState(false); @@ -159,16 +161,12 @@ export default function McpConfigModal({ // Delete MCP server const handleDeleteServer = async (server: McpServer) => { - modal.confirm({ + confirm({ title: t("mcpConfig.delete.confirmTitle"), content: t("mcpConfig.delete.confirmContent", { name: server.service_name, }), okText: t("common.delete", "Delete"), - cancelText: t("common.cancel", "Cancel"), - okType: "danger", - cancelButtonProps: { disabled: updatingTools }, - okButtonProps: { disabled: updatingTools, loading: updatingTools }, onOk: async () => { try { const result = await deleteMcpServer( diff --git a/frontend/app/[locale]/chat/components/chatLeftSidebar.tsx b/frontend/app/[locale]/chat/components/chatLeftSidebar.tsx index d14467131..c9b2b3aaf 100644 --- a/frontend/app/[locale]/chat/components/chatLeftSidebar.tsx +++ b/frontend/app/[locale]/chat/components/chatLeftSidebar.tsx @@ -28,6 +28,7 @@ import { import { StaticScrollArea } from "@/components/ui/scrollArea"; import { USER_ROLES } from "@/const/modelConfig"; import { useTranslation } from "react-i18next"; +import { useConfirmModal } from "@/hooks/useConfirmModal"; import { ConversationListItem, ChatSidebarProps } from "@/types/chat"; // conversation status indicator component @@ -114,7 +115,7 @@ export function ChatSidebar({ userRole = USER_ROLES.USER, }: ChatSidebarProps) { const { t } = useTranslation(); - const { modal } = App.useApp(); + const { confirm } = useConfirmModal(); const router = useRouter(); const { today, week, older } = categorizeDialogs(conversationList); const [editingId, setEditingId] = useState(null); @@ -179,14 +180,9 @@ export function ChatSidebar({ onDropdownOpenChange(false, null); // Show confirmation modal - modal.confirm({ + confirm({ title: t("chatLeftSidebar.confirmDeletionTitle"), content: t("chatLeftSidebar.confirmDeletionDescription"), - okText: t("common.confirm"), - cancelText: t("common.cancel"), - okType: "danger", - centered: true, - maskClosable: false, onOk: () => { onDelete(dialogId); }, diff --git a/frontend/app/[locale]/knowledges/KnowledgeBaseConfiguration.tsx b/frontend/app/[locale]/knowledges/KnowledgeBaseConfiguration.tsx index 014664450..10943ef01 100644 --- a/frontend/app/[locale]/knowledges/KnowledgeBaseConfiguration.tsx +++ b/frontend/app/[locale]/knowledges/KnowledgeBaseConfiguration.tsx @@ -497,7 +497,7 @@ function DataConfig({ isActive }: DataConfigProps) { okText: t("common.confirm"), cancelText: t("common.cancel"), danger: true, - onConfirm: async () => { + onOk: async () => { try { await deleteKnowledgeBase(id); @@ -554,7 +554,7 @@ function DataConfig({ isActive }: DataConfigProps) { okText: t("common.confirm"), cancelText: t("common.cancel"), danger: true, - onConfirm: async () => { + onOk: async () => { try { await deleteDocument(kbId, docId); message.success(t("document.message.deleteSuccess")); diff --git a/frontend/app/[locale]/space/components/AgentCard.tsx b/frontend/app/[locale]/space/components/AgentCard.tsx index b174218bd..eb5c183c6 100644 --- a/frontend/app/[locale]/space/components/AgentCard.tsx +++ b/frontend/app/[locale]/space/components/AgentCard.tsx @@ -23,6 +23,7 @@ import { } from "@/services/agentConfigService"; import { generateAvatarFromName } from "@/lib/avatar"; import { useAuth } from "@/hooks/useAuth"; +import { useConfirmModal } from "@/hooks/useConfirmModal"; import { USER_ROLES } from "@/const/modelConfig"; import log from "@/lib/logger"; @@ -45,8 +46,9 @@ interface AgentCardProps { export default function AgentCard({ agent, onRefresh, onChat, onEdit }: AgentCardProps) { const { t } = useTranslation("common"); - const { message, modal } = App.useApp(); + const { message } = App.useApp(); const { user, isSpeedMode } = useAuth(); + const { confirm } = useConfirmModal(); const [isDeleting, setIsDeleting] = useState(false); const [isExporting, setIsExporting] = useState(false); @@ -63,15 +65,12 @@ export default function AgentCard({ agent, onRefresh, onChat, onEdit }: AgentCar // Handle delete agent const handleDelete = () => { - modal.confirm({ + confirm({ title: t("space.deleteConfirm.title", "Delete Agent"), content: t( "space.deleteConfirm.content", `Are you sure you want to delete agent "${agent.display_name}"? This action cannot be undone.` ), - okText: t("common.confirm", "Confirm"), - cancelText: t("common.cancel", "Cancel"), - okButtonProps: { danger: true }, onOk: async () => { setIsDeleting(true); try { diff --git a/frontend/components/auth/avatarDropdown.tsx b/frontend/components/auth/avatarDropdown.tsx index f5050b22c..06ff95742 100644 --- a/frontend/components/auth/avatarDropdown.tsx +++ b/frontend/components/auth/avatarDropdown.tsx @@ -14,6 +14,7 @@ import { import type { ItemType } from "antd/es/menu/interface"; import { useAuth } from "@/hooks/useAuth"; +import { useConfirmModal } from "@/hooks/useConfirmModal"; import { getRoleColor } from "@/lib/auth"; export function AvatarDropdown() { @@ -22,6 +23,7 @@ export function AvatarDropdown() { const [dropdownOpen, setDropdownOpen] = useState(false); const { t } = useTranslation("common"); const { modal } = App.useApp(); + const { confirm } = useConfirmModal(); if (isLoading) { return ; @@ -115,11 +117,9 @@ export function AvatarDropdown() { icon: , label: t("auth.logout"), onClick: () => { - modal.confirm({ + confirm({ title: t("auth.confirmLogout"), content: t("auth.confirmLogoutPrompt"), - okText: t("auth.confirm"), - cancelText: t("auth.cancel"), onOk: () => { logout(); }, @@ -141,13 +141,10 @@ export function AvatarDropdown() { okText: t("auth.confirm"), }); } else { - modal.confirm({ + confirm({ title: t("auth.confirmRevoke"), content: t("auth.confirmRevokePrompt"), - icon: , okText: t("auth.confirmRevokeOk"), - cancelText: t("auth.cancel"), - okButtonProps: { danger: true }, onOk: () => { revoke(); }, diff --git a/frontend/components/auth/sessionListeners.tsx b/frontend/components/auth/sessionListeners.tsx index 2897ffe6b..3bc3413c2 100644 --- a/frontend/components/auth/sessionListeners.tsx +++ b/frontend/components/auth/sessionListeners.tsx @@ -7,6 +7,7 @@ import { App, Modal } from "antd"; import { ExclamationCircleOutlined } from "@ant-design/icons"; import { useAuth } from "@/hooks/useAuth"; +import { useConfirmModal } from "@/hooks/useConfirmModal"; import { authService } from "@/services/authService"; import { sessionService } from "@/services/sessionService"; import { getSessionFromStorage } from "@/lib/auth"; @@ -29,6 +30,7 @@ export function SessionListeners() { const { openLoginModal, setIsFromSessionExpired, clearLocalSession, isSpeedMode } = useAuth(); const { modal } = App.useApp(); + const { confirm } = useConfirmModal(); const modalShownRef = useRef(false); const isLocaleHomePath = (path?: string | null) => { @@ -52,7 +54,12 @@ export function SessionListeners() { content: t("login.expired.content"), okText: t("login.expired.okText"), cancelText: t("login.expired.cancelText"), + centered: true, closable: false, + okButtonProps: { + danger: true, + type: "primary" + }, onOk() { // Clear local session state (session already expired on backend) clearLocalSession(); diff --git a/frontend/hooks/useConfirmModal.ts b/frontend/hooks/useConfirmModal.ts index 1a2b6e12a..1a12734da 100644 --- a/frontend/hooks/useConfirmModal.ts +++ b/frontend/hooks/useConfirmModal.ts @@ -1,7 +1,18 @@ +import { App } from "antd"; +import { ExclamationCircleFilled } from "@ant-design/icons"; + +import React from "react"; import i18next from "i18next"; -import { App } from "antd"; -import { StaticConfirmProps } from "@/types/setupConfig"; +interface ConfirmProps { + title: string; + content: React.ReactNode; + okText?: string; + cancelText?: string; + danger?: boolean; // 默认为 true,使用 danger 样式 + onOk?: () => void; + onCancel?: () => void; +} export const useConfirmModal = () => { const { modal } = App.useApp(); @@ -11,20 +22,25 @@ export const useConfirmModal = () => { content, okText, cancelText, - danger = false, - onConfirm, + danger = true, + onOk, onCancel, - }: StaticConfirmProps) => { + }: ConfirmProps) => { return modal.confirm({ title, content, + centered: true, + icon: React.createElement(ExclamationCircleFilled), okText: okText || i18next.t("common.confirm"), cancelText: cancelText || i18next.t("common.cancel"), - okButtonProps: { danger }, - onOk: onConfirm, + okButtonProps: { + danger, + type: "primary" + }, + onOk: onOk, onCancel, }); }; return { confirm }; -}; +}; \ No newline at end of file diff --git a/frontend/types/setupConfig.ts b/frontend/types/setupConfig.ts deleted file mode 100644 index a244e081f..000000000 --- a/frontend/types/setupConfig.ts +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; - -interface StaticConfirmProps { - title: string; - content: React.ReactNode; - okText?: string; - cancelText?: string; - danger?: boolean; - onConfirm?: () => void; - onCancel?: () => void; -} - -export type { StaticConfirmProps }; From f8b73bd559ed57e234a4a808488e3ad837c2cba3 Mon Sep 17 00:00:00 2001 From: wmc1112 <759659013@qq.com> Date: Tue, 16 Dec 2025 15:01:23 +0800 Subject: [PATCH 20/36] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20The=20tool=20status?= =?UTF-8?q?=20is=20not=20synchronized=20when=20the=20service=20starts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/config_service.py | 1 - backend/runtime_service.py | 3 +- test/backend/test_config_service.py | 302 --------------------------- test/backend/test_runtime_service.py | 282 ------------------------- 4 files changed, 1 insertion(+), 587 deletions(-) diff --git a/backend/config_service.py b/backend/config_service.py index 961c59b91..ebe5b7593 100644 --- a/backend/config_service.py +++ b/backend/config_service.py @@ -1,7 +1,6 @@ import uvicorn import logging import warnings -import asyncio from consts.const import APP_VERSION diff --git a/backend/runtime_service.py b/backend/runtime_service.py index eb065aa3d..9fd42d9a7 100644 --- a/backend/runtime_service.py +++ b/backend/runtime_service.py @@ -1,7 +1,6 @@ import uvicorn import logging import warnings -import asyncio from consts.const import APP_VERSION @@ -16,7 +15,7 @@ configure_logging(logging.INFO) configure_elasticsearch_logging() -logger = logging.getLogger("config_service") +logger = logging.getLogger("runtime_service") if __name__ == "__main__": diff --git a/test/backend/test_config_service.py b/test/backend/test_config_service.py index 9935797cc..0f25b9530 100644 --- a/test/backend/test_config_service.py +++ b/test/backend/test_config_service.py @@ -128,308 +128,6 @@ def __init__(self, *args, **kwargs): setattr(backend_pkg, "apps", apps_pkg) setattr(apps_pkg, "config_app", base_app_mod) -# Mock external dependencies before importing backend modules -with patch('backend.database.client.MinioClient', return_value=minio_client_mock), \ - patch('elasticsearch.Elasticsearch', return_value=MagicMock()), \ - patch('nexent.vector_database.elasticsearch_core.ElasticSearchCore', return_value=MagicMock()): - # Mock dotenv before importing config_service - with patch('dotenv.load_dotenv'): - # Mock logging configuration - with patch('utils.logging_utils.configure_logging'), \ - patch('utils.logging_utils.configure_elasticsearch_logging'): - from config_service import startup_initialization - - -class TestMainService: - """Test cases for config_service module""" - - @pytest.mark.asyncio - @patch('config_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('config_service.logger') - async def test_startup_initialization_success(self, mock_logger, mock_initialize_tools): - """ - Test successful startup initialization. - - This test verifies that: - 1. The function logs the start of initialization - 2. It logs the APP version - 3. It calls initialize_tools_on_startup - 4. It logs successful completion - """ - # Setup - mock_initialize_tools.return_value = None - - # Execute - await startup_initialization() - - # Assert - # Check that appropriate log messages were called - mock_logger.info.assert_any_call("Starting server initialization...") - mock_logger.info.assert_any_call( - "Server initialization completed successfully!") - - # Verify initialize_tools_on_startup was called - mock_initialize_tools.assert_called_once() - - @pytest.mark.asyncio - @patch('config_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('config_service.logger') - async def test_startup_initialization_with_version_log(self, mock_logger, mock_initialize_tools): - """ - Test that startup initialization logs the APP version. - - This test verifies that: - 1. The function logs the APP version from consts.const - """ - # Setup - mock_initialize_tools.return_value = None - - # Execute - await startup_initialization() - - # Assert - # Check that version logging was called (should contain "APP version is:") - version_logged = any( - call for call in mock_logger.info.call_args_list - if len(call.args) > 0 and "APP version is:" in str(call.args[0]) - ) - assert version_logged, "APP version should be logged during initialization" - - @pytest.mark.asyncio - @patch('config_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('config_service.logger') - async def test_startup_initialization_tool_initialization_failure(self, mock_logger, mock_initialize_tools): - """ - Test startup initialization when tool initialization fails. - - This test verifies that: - 1. When initialize_tools_on_startup raises an exception - 2. The function catches the exception and logs an error - 3. The function logs a warning about continuing despite issues - 4. The function does not re-raise the exception - """ - # Setup - mock_initialize_tools.side_effect = Exception( - "Tool initialization failed") - - # Execute - should not raise exception - await startup_initialization() - - # Assert - mock_logger.error.assert_called_once() - error_call = mock_logger.error.call_args[0][0] - assert "Server initialization failed:" in error_call - assert "Tool initialization failed" in error_call - - mock_logger.warning.assert_called_once_with( - "Server will continue to start despite initialization issues" - ) - - # Verify initialize_tools_on_startup was called - mock_initialize_tools.assert_called_once() - - @pytest.mark.asyncio - @patch('config_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('config_service.logger') - async def test_startup_initialization_database_error(self, mock_logger, mock_initialize_tools): - """ - Test startup initialization when database connection fails. - - This test verifies that: - 1. Database-related exceptions are handled gracefully - 2. Appropriate error messages are logged - 3. The server startup is not blocked - """ - # Setup - mock_initialize_tools.side_effect = ConnectionError( - "Database connection failed") - - # Execute - should not raise exception - await startup_initialization() - - # Assert - mock_logger.error.assert_called_once() - error_message = mock_logger.error.call_args[0][0] - assert "Server initialization failed:" in error_message - assert "Database connection failed" in error_message - - mock_logger.warning.assert_called_once_with( - "Server will continue to start despite initialization issues" - ) - - @pytest.mark.asyncio - @patch('config_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('config_service.logger') - async def test_startup_initialization_timeout_error(self, mock_logger, mock_initialize_tools): - """ - Test startup initialization when tool initialization times out. - - This test verifies that: - 1. Timeout exceptions are handled gracefully - 2. Appropriate error messages are logged - 3. The function continues execution - """ - # Setup - mock_initialize_tools.side_effect = asyncio.TimeoutError( - "Tool initialization timed out") - - # Execute - should not raise exception - await startup_initialization() - - # Assert - mock_logger.error.assert_called_once() - error_message = mock_logger.error.call_args[0][0] - assert "Server initialization failed:" in error_message - - mock_logger.warning.assert_called_once_with( - "Server will continue to start despite initialization issues" - ) - - @pytest.mark.asyncio - @patch('config_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('config_service.logger') - async def test_startup_initialization_multiple_calls_safe(self, mock_logger, mock_initialize_tools): - """ - Test that multiple calls to startup_initialization are safe. - - This test verifies that: - 1. The function can be called multiple times without issues - 2. Each call properly executes the initialization sequence - """ - # Setup - mock_initialize_tools.return_value = None - - # Execute multiple times - await startup_initialization() - await startup_initialization() - - # Assert - assert mock_initialize_tools.call_count == 2 - # At least 2 calls * 2 info messages per call - assert mock_logger.info.call_count >= 4 - - @pytest.mark.asyncio - @patch('config_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('config_service.logger') - async def test_startup_initialization_logging_order(self, mock_logger, mock_initialize_tools): - """ - Test that logging occurs in the correct order during initialization. - - This test verifies that: - 1. Start message is logged first - 2. Version message is logged second - 3. Success message is logged last (when successful) - """ - # Setup - mock_initialize_tools.return_value = None - - # Execute - await startup_initialization() - - # Assert - info_calls = [call.args[0] for call in mock_logger.info.call_args_list] - - # Check order of log messages - assert len(info_calls) >= 3 - assert "Starting server initialization..." in info_calls[0] - assert "APP version is:" in info_calls[1] - assert "Server initialization completed successfully!" in info_calls[-1] - - @pytest.mark.asyncio - @patch('config_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('config_service.logger') - async def test_startup_initialization_exception_details_logged(self, mock_logger, mock_initialize_tools): - """ - Test that exception details are properly logged. - - This test verifies that: - 1. The specific exception message is included in error logs - 2. Both error and warning messages are logged on failure - """ - # Setup - specific_error_message = "Specific tool configuration error occurred" - mock_initialize_tools.side_effect = ValueError(specific_error_message) - - # Execute - await startup_initialization() - - # Assert - mock_logger.error.assert_called_once() - error_call_args = mock_logger.error.call_args[0][0] - assert specific_error_message in error_call_args - assert "Server initialization failed:" in error_call_args - - mock_logger.warning.assert_called_once() - - @pytest.mark.asyncio - @patch('config_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('config_service.logger') - async def test_startup_initialization_no_exception_propagation(self, mock_logger, mock_initialize_tools): - """ - Test that exceptions during initialization do not propagate. - - This test verifies that: - 1. Even when initialize_tools_on_startup fails, no exception is raised - 2. This allows the server to continue starting up - """ - # Setup - mock_initialize_tools.side_effect = RuntimeError( - "Critical initialization error") - - # Execute and Assert - should not raise any exception - try: - await startup_initialization() - except Exception as e: - pytest.fail( - f"startup_initialization should not raise exceptions, but raised: {e}") - - # Verify that error handling occurred - mock_logger.error.assert_called_once() - mock_logger.warning.assert_called_once() - - -class TestMainServiceModuleIntegration: - """Integration tests for config_service module dependencies""" - - @patch('config_service.configure_logging') - @patch('config_service.configure_elasticsearch_logging') - def test_logging_configuration_called_on_import(self, mock_configure_es, mock_configure_logging): - """ - Test that logging configuration functions are called when module is imported. - - This test verifies that: - 1. configure_logging is called with logging.INFO - 2. configure_elasticsearch_logging is called - """ - # Note: This test checks that logging configuration happens during module import - # The mocks should have been called when the module was imported - # In a real scenario, you might need to reload the module to test this properly - pass # The actual verification would depend on how the test runner handles imports - - @patch('config_service.APP_VERSION', 'test_version_1.2.3') - @patch('config_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('config_service.logger') - async def test_startup_initialization_with_custom_version(self, mock_logger, mock_initialize_tools): - """ - Test startup initialization with a custom APP_VERSION. - - This test verifies that: - 1. The custom version is properly logged - """ - # Setup - mock_initialize_tools.return_value = None - - # Execute - await startup_initialization() - - # Assert - version_logged = any( - "test_version_1.2.3" in str(call.args[0]) - for call in mock_logger.info.call_args_list - if len(call.args) > 0 - ) - assert version_logged, "Custom APP version should be logged" - class TestTenantConfigService: """Unit tests for tenant_config_service helpers""" diff --git a/test/backend/test_runtime_service.py b/test/backend/test_runtime_service.py index f7783cd82..81b2bb7fc 100644 --- a/test/backend/test_runtime_service.py +++ b/test/backend/test_runtime_service.py @@ -124,264 +124,6 @@ def __init__(self, *args, **kwargs): setattr(backend_pkg, "apps", apps_pkg) setattr(apps_pkg, "runtime_app", base_app_mod) -# Mock external dependencies before importing backend modules -with patch('elasticsearch.Elasticsearch', return_value=MagicMock()), \ - patch('nexent.vector_database.elasticsearch_core.ElasticSearchCore', return_value=MagicMock()): - # Mock dotenv before importing runtime_service - with patch('dotenv.load_dotenv'): - # Mock logging configuration - with patch('utils.logging_utils.configure_logging'), \ - patch('utils.logging_utils.configure_elasticsearch_logging'): - from runtime_service import startup_initialization - - -class TestMainService: - """Test cases for runtime_service module""" - - @pytest.mark.asyncio - @patch('runtime_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('runtime_service.logger') - async def test_startup_initialization_success(self, mock_logger, mock_initialize_tools): - """ - Test successful startup initialization. - - This test verifies that: - 1. The function logs the start of initialization - 2. It logs the APP version - 3. It calls initialize_tools_on_startup - 4. It logs successful completion - """ - # Setup - mock_initialize_tools.return_value = None - - # Execute - await startup_initialization() - - # Assert - # Check that appropriate log messages were called - mock_logger.info.assert_any_call("Starting server initialization...") - mock_logger.info.assert_any_call( - "Server initialization completed successfully!") - - # Verify initialize_tools_on_startup was called - mock_initialize_tools.assert_called_once() - - @pytest.mark.asyncio - @patch('runtime_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('runtime_service.logger') - async def test_startup_initialization_with_version_log(self, mock_logger, mock_initialize_tools): - """ - Test that startup initialization logs the APP version. - - This test verifies that: - 1. The function logs the APP version from consts.const - """ - # Setup - mock_initialize_tools.return_value = None - - # Execute - await startup_initialization() - - # Assert - # Check that version logging was called (should contain "APP version is:") - version_logged = any( - call for call in mock_logger.info.call_args_list - if len(call.args) > 0 and "APP version is:" in str(call.args[0]) - ) - assert version_logged, "APP version should be logged during initialization" - - @pytest.mark.asyncio - @patch('runtime_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('runtime_service.logger') - async def test_startup_initialization_tool_initialization_failure(self, mock_logger, mock_initialize_tools): - """ - Test startup initialization when tool initialization fails. - - This test verifies that: - 1. When initialize_tools_on_startup raises an exception - 2. The function catches the exception and logs an error - 3. The function logs a warning about continuing despite issues - 4. The function does not re-raise the exception - """ - # Setup - mock_initialize_tools.side_effect = Exception( - "Tool initialization failed") - - # Execute - should not raise exception - await startup_initialization() - - # Assert - mock_logger.error.assert_called_once() - error_call = mock_logger.error.call_args[0][0] - assert "Server initialization failed:" in error_call - assert "Tool initialization failed" in error_call - - mock_logger.warning.assert_called_once_with( - "Server will continue to start despite initialization issues" - ) - - # Verify initialize_tools_on_startup was called - mock_initialize_tools.assert_called_once() - - @pytest.mark.asyncio - @patch('runtime_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('runtime_service.logger') - async def test_startup_initialization_database_error(self, mock_logger, mock_initialize_tools): - """ - Test startup initialization when database connection fails. - - This test verifies that: - 1. Database-related exceptions are handled gracefully - 2. Appropriate error messages are logged - 3. The server startup is not blocked - """ - # Setup - mock_initialize_tools.side_effect = ConnectionError( - "Database connection failed") - - # Execute - should not raise exception - await startup_initialization() - - # Assert - mock_logger.error.assert_called_once() - error_message = mock_logger.error.call_args[0][0] - assert "Server initialization failed:" in error_message - assert "Database connection failed" in error_message - - mock_logger.warning.assert_called_once_with( - "Server will continue to start despite initialization issues" - ) - - @pytest.mark.asyncio - @patch('runtime_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('runtime_service.logger') - async def test_startup_initialization_timeout_error(self, mock_logger, mock_initialize_tools): - """ - Test startup initialization when tool initialization times out. - - This test verifies that: - 1. Timeout exceptions are handled gracefully - 2. Appropriate error messages are logged - 3. The function continues execution - """ - # Setup - mock_initialize_tools.side_effect = asyncio.TimeoutError( - "Tool initialization timed out") - - # Execute - should not raise exception - await startup_initialization() - - # Assert - mock_logger.error.assert_called_once() - error_message = mock_logger.error.call_args[0][0] - assert "Server initialization failed:" in error_message - - mock_logger.warning.assert_called_once_with( - "Server will continue to start despite initialization issues" - ) - - @pytest.mark.asyncio - @patch('runtime_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('runtime_service.logger') - async def test_startup_initialization_multiple_calls_safe(self, mock_logger, mock_initialize_tools): - """ - Test that multiple calls to startup_initialization are safe. - - This test verifies that: - 1. The function can be called multiple times without issues - 2. Each call properly executes the initialization sequence - """ - # Setup - mock_initialize_tools.return_value = None - - # Execute multiple times - await startup_initialization() - await startup_initialization() - - # Assert - assert mock_initialize_tools.call_count == 2 - # At least 2 calls * 2 info messages per call - assert mock_logger.info.call_count >= 4 - - @pytest.mark.asyncio - @patch('runtime_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('runtime_service.logger') - async def test_startup_initialization_logging_order(self, mock_logger, mock_initialize_tools): - """ - Test that logging occurs in the correct order during initialization. - - This test verifies that: - 1. Start message is logged first - 2. Version message is logged second - 3. Success message is logged last (when successful) - """ - # Setup - mock_initialize_tools.return_value = None - - # Execute - await startup_initialization() - - # Assert - info_calls = [call.args[0] for call in mock_logger.info.call_args_list] - - # Check order of log messages - assert len(info_calls) >= 3 - assert "Starting server initialization..." in info_calls[0] - assert "APP version is:" in info_calls[1] - assert "Server initialization completed successfully!" in info_calls[-1] - - @pytest.mark.asyncio - @patch('runtime_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('runtime_service.logger') - async def test_startup_initialization_exception_details_logged(self, mock_logger, mock_initialize_tools): - """ - Test that exception details are properly logged. - - This test verifies that: - 1. The specific exception message is included in error logs - 2. Both error and warning messages are logged on failure - """ - # Setup - specific_error_message = "Specific tool configuration error occurred" - mock_initialize_tools.side_effect = ValueError(specific_error_message) - - # Execute - await startup_initialization() - - # Assert - mock_logger.error.assert_called_once() - error_call_args = mock_logger.error.call_args[0][0] - assert specific_error_message in error_call_args - assert "Server initialization failed:" in error_call_args - - mock_logger.warning.assert_called_once() - - @pytest.mark.asyncio - @patch('runtime_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('runtime_service.logger') - async def test_startup_initialization_no_exception_propagation(self, mock_logger, mock_initialize_tools): - """ - Test that exceptions during initialization do not propagate. - - This test verifies that: - 1. Even when initialize_tools_on_startup fails, no exception is raised - 2. This allows the server to continue starting up - """ - # Setup - mock_initialize_tools.side_effect = RuntimeError( - "Critical initialization error") - - # Execute and Assert - should not raise any exception - try: - await startup_initialization() - except Exception as e: - pytest.fail( - f"startup_initialization should not raise exceptions, but raised: {e}") - - # Verify that error handling occurred - mock_logger.error.assert_called_once() - mock_logger.warning.assert_called_once() - class TestMainServiceModuleIntegration: """Integration tests for runtime_service module dependencies""" @@ -401,30 +143,6 @@ def test_logging_configuration_called_on_import(self, mock_configure_es, mock_co # In a real scenario, you might need to reload the module to test this properly pass # The actual verification would depend on how the test runner handles imports - @patch('runtime_service.APP_VERSION', 'test_version_1.2.3') - @patch('runtime_service.initialize_tools_on_startup', new_callable=AsyncMock) - @patch('runtime_service.logger') - async def test_startup_initialization_with_custom_version(self, mock_logger, mock_initialize_tools): - """ - Test startup initialization with a custom APP_VERSION. - - This test verifies that: - 1. The custom version is properly logged - """ - # Setup - mock_initialize_tools.return_value = None - - # Execute - await startup_initialization() - - # Assert - version_logged = any( - "test_version_1.2.3" in str(call.args[0]) - for call in mock_logger.info.call_args_list - if len(call.args) > 0 - ) - assert version_logged, "Custom APP version should be logged" - if __name__ == '__main__': pytest.main() From 83a25f9d0bbe7315910be0c9b2ce605c999232ab Mon Sep 17 00:00:00 2001 From: xuyaqi Date: Wed, 17 Dec 2025 10:48:49 +0800 Subject: [PATCH 21/36] Unify global dialog&modal, delete unused code --- .../components/AgentSetupOrchestrator.tsx | 60 ++------ .../[locale]/chat/components/chatHeader.tsx | 113 ++++---------- .../knowledges/KnowledgeBaseConfiguration.tsx | 32 ++-- .../app/[locale]/memory/MemoryContent.tsx | 53 +++---- .../components/MemoryDeleteConfirmation.tsx | 59 ------- .../app/[locale]/models/ModelsContent.tsx | 87 +---------- .../components/model/EmbedderCheckModal.tsx | 144 ------------------ .../models/components/modelConfig.tsx | 58 +++---- 8 files changed, 107 insertions(+), 499 deletions(-) delete mode 100644 frontend/app/[locale]/memory/components/MemoryDeleteConfirmation.tsx delete mode 100644 frontend/app/[locale]/models/components/model/EmbedderCheckModal.tsx diff --git a/frontend/app/[locale]/agents/components/AgentSetupOrchestrator.tsx b/frontend/app/[locale]/agents/components/AgentSetupOrchestrator.tsx index b1a01bfe9..b077ef785 100644 --- a/frontend/app/[locale]/agents/components/AgentSetupOrchestrator.tsx +++ b/frontend/app/[locale]/agents/components/AgentSetupOrchestrator.tsx @@ -116,9 +116,7 @@ export default function AgentSetupOrchestrator({ const [importWizardData, setImportWizardData] = useState(null); // Use generation state passed from parent component, not local state - // Delete confirmation popup status - const [isDeleteConfirmOpen, setIsDeleteConfirmOpen] = useState(false); - const [agentToDelete, setAgentToDelete] = useState(null); + // Embedding auto-unselect notice modal const [isEmbeddingAutoUnsetOpen, setIsEmbeddingAutoUnsetOpen] = @@ -1770,19 +1768,17 @@ export default function AgentSetupOrchestrator({ }, [pendingImportData, runAgentImport, t]); // Handle confirmed deletion - const handleConfirmDelete = async (t: TFunction) => { - if (!agentToDelete) return; - + const handleConfirmDelete = async (agent: Agent) => { try { - const result = await deleteAgent(Number(agentToDelete.id)); + const result = await deleteAgent(Number(agent.id)); if (result.success) { message.success( t("businessLogic.config.error.agentDeleteSuccess", { - name: agentToDelete.name, + name: agent.name, }) ); // If currently editing the deleted agent, reset to initial clean state and avoid confirm modal on next switch - const deletedId = Number(agentToDelete.id); + const deletedId = Number(agent.id); const currentEditingId = (isEditingAgent && editingAgent ? Number(editingAgent.id) : null) ?? null; @@ -1815,7 +1811,7 @@ export default function AgentSetupOrchestrator({ } else { // If deleting another agent that is in enabledAgentIds, remove it and update baseline // to avoid triggering false unsaved changes indicator - const deletedId = Number(agentToDelete.id); + const deletedId = Number(agent.id); if (enabledAgentIds.includes(deletedId)) { const updatedEnabledAgentIds = enabledAgentIds.filter( (id) => id !== deletedId @@ -1840,9 +1836,6 @@ export default function AgentSetupOrchestrator({ } catch (error) { log.error(t("agentConfig.agents.deleteFailed"), error); message.error(t("businessLogic.config.error.agentDeleteFailed")); - } finally { - setIsDeleteConfirmOpen(false); - setAgentToDelete(null); } }; @@ -2009,8 +2002,13 @@ export default function AgentSetupOrchestrator({ // Handle delete agent from list const handleDeleteAgentFromList = (agent: Agent) => { - setAgentToDelete(agent); - setIsDeleteConfirmOpen(true); + confirm({ + title: t("businessLogic.config.modal.deleteTitle"), + content: t("businessLogic.config.modal.deleteContent", { + name: agent.name, + }), + onOk: () => handleConfirmDelete(agent), + }); }; // Handle exit edit mode @@ -2305,37 +2303,7 @@ export default function AgentSetupOrchestrator({ - {/* Delete confirmation popup */} - setIsDeleteConfirmOpen(false)} - centered - footer={ -
- -
- } - width={520} - > -
-
-
-
- {t("businessLogic.config.modal.deleteContent", { - name: agentToDelete?.name, - })} -
-
-
-
-
+ {/* Save confirmation modal for unsaved changes (debug/navigation hooks) */} (null); const { t, i18n } = useTranslation("common"); const { user, isSpeedMode } = useAuth(); + const { confirm } = useConfirmModal(); const isAdmin = isSpeedMode || user?.role === USER_ROLES.ADMIN; const goToModelSetup = () => { @@ -36,6 +36,31 @@ export function ChatHeader({ title, onRename }: ChatHeaderProps) { router.push(`/${i18n.language}`); }; + const showAutoOffConfirm = () => { + confirm({ + title: t("embedding.chatMemoryAutoDeselectModal.title"), + content: ( +
+
+ {t("embedding.chatMemoryAutoDeselectModal.content")} +
+ {!isAdmin && ( +
+ {t("embedding.chatMemoryAutoDeselectModal.tip")} +
+ )} + {isAdmin && ( +
+ +
+ )} +
+ ), + }); + }; + // Update editTitle when the title attribute changes useEffect(() => { setEditTitle(title); @@ -73,7 +98,7 @@ export function ChatHeader({ title, onRename }: ChatHeaderProps) { "Failed to auto turn off memory switch when embedding is not configured" ); } - setShowAutoOffPrompt(true); + showAutoOffConfirm(); } }) .catch((e) => { @@ -145,83 +170,7 @@ export function ChatHeader({ title, onRename }: ChatHeaderProps) {
- {/* Embedding not configured prompt */} - setShowConfigPrompt(false)} - centered - footer={ -
- {isAdmin && ( - - )} - -
- } - > -
-
- -
-
- {t("embedding.chatMemoryWarningModal.content")} -
- {!isAdmin && ( -
- {t("embedding.chatMemoryWarningModal.tip")} -
- )} -
-
-
-
- - {/* Auto-off memory prompt when embedding missing */} - setShowAutoOffPrompt(false)} - centered - footer={ -
- {isAdmin && ( - - )} - -
- } - > -
-
- -
-
- {t("embedding.chatMemoryAutoDeselectModal.content")} -
- {!isAdmin && ( -
- {t("embedding.chatMemoryAutoDeselectModal.tip")} -
- )} -
-
-
-
+ ); } diff --git a/frontend/app/[locale]/knowledges/KnowledgeBaseConfiguration.tsx b/frontend/app/[locale]/knowledges/KnowledgeBaseConfiguration.tsx index 10943ef01..283730e0f 100644 --- a/frontend/app/[locale]/knowledges/KnowledgeBaseConfiguration.tsx +++ b/frontend/app/[locale]/knowledges/KnowledgeBaseConfiguration.tsx @@ -4,8 +4,8 @@ import type React from "react"; import { useState, useEffect, useRef, useLayoutEffect } from "react"; import { useTranslation } from "react-i18next"; -import { App, Modal, Row, Col } from "antd"; -import { InfoCircleFilled, WarningFilled } from "@ant-design/icons"; +import { App, Modal, Row, Col, theme } from "antd"; +import { ExclamationCircleFilled, WarningFilled, InfoCircleFilled } from "@ant-design/icons"; import { DOCUMENT_ACTION_TYPES, KNOWLEDGE_BASE_ACTION_TYPES, @@ -116,6 +116,7 @@ function DataConfig({ isActive }: DataConfigProps) { const { message } = App.useApp(); const { confirm } = useConfirmModal(); const { modelConfig } = useConfig(); + const { token } = theme.useToken(); // Clear cache when component initializes useEffect(() => { @@ -828,24 +829,29 @@ function DataConfig({ isActive }: DataConfigProps) { > setShowAutoDeselectModal(false)} onCancel={() => setShowAutoDeselectModal(false)} okText={t("common.confirm")} cancelButtonProps={{ style: { display: "none" } }} centered + okButtonProps={{ type: "primary", danger: true }} getContainer={() => contentRef.current || document.body} > -
-
- -
-
- {t("embedding.knowledgeBaseAutoDeselectModal.content")} -
+
+ +
+
+ {t("embedding.knowledgeBaseAutoDeselectModal.title")} +
+
+ {t("embedding.knowledgeBaseAutoDeselectModal.content")}
diff --git a/frontend/app/[locale]/memory/MemoryContent.tsx b/frontend/app/[locale]/memory/MemoryContent.tsx index 370e220b3..d116a8606 100644 --- a/frontend/app/[locale]/memory/MemoryContent.tsx +++ b/frontend/app/[locale]/memory/MemoryContent.tsx @@ -17,7 +17,7 @@ import { Check, X, } from "lucide-react"; -import { useTranslation } from "react-i18next"; +import { useTranslation, Trans } from "react-i18next"; import { useAuth } from "@/hooks/useAuth"; import { useMemory } from "@/hooks/useMemory"; @@ -29,7 +29,7 @@ import { STANDARD_CARD, } from "@/const/layoutConstants"; -import MemoryDeleteConfirmation from "./components/MemoryDeleteConfirmation"; +import { useConfirmModal } from "@/hooks/useConfirmModal"; interface MemoryContentProps { /** Custom navigation handler (optional) */ @@ -44,6 +44,7 @@ export default function MemoryContent({ onNavigate }: MemoryContentProps) { const { message } = App.useApp(); const { t } = useTranslation("common"); const { user, isSpeedMode } = useAuth(); + const { confirm } = useConfirmModal(); // Use custom hook for common setup flow logic const { canAccessProtectedData, pageVariants, pageTransition } = useSetupFlow({ @@ -66,29 +67,25 @@ export default function MemoryContent({ onNavigate }: MemoryContentProps) { message, }); - // Clear memory confirmation state - const [clearConfirmVisible, setClearConfirmVisible] = useState(false); - const [clearTarget, setClearTarget] = useState<{ - key: string; - title: string; - } | null>(null); - const handleClearConfirm = (groupKey: string, groupTitle: string) => { - setClearTarget({ key: groupKey, title: groupTitle }); - setClearConfirmVisible(true); - }; - - const handleClearConfirmOk = async () => { - if (clearTarget) { - await memory.handleClearMemory(clearTarget.key, clearTarget.title); - setClearConfirmVisible(false); - setClearTarget(null); - } - }; - - const handleClearConfirmCancel = () => { - setClearConfirmVisible(false); - setClearTarget(null); + confirm({ + title: t("memoryDeleteModal.title"), + content: ( +
+

+ }} + /> +

+

+ {t("memoryDeleteModal.prompt")} +

+
+ ), + onOk: () => memory.handleClearMemory(groupKey, groupTitle), + }); }; // Render base settings in a horizontal control bar @@ -379,13 +376,7 @@ export default function MemoryContent({ onNavigate }: MemoryContentProps) { ) : null} - {/* Clear memory confirmation */} - + ); } diff --git a/frontend/app/[locale]/memory/components/MemoryDeleteConfirmation.tsx b/frontend/app/[locale]/memory/components/MemoryDeleteConfirmation.tsx deleted file mode 100644 index 52efb93f5..000000000 --- a/frontend/app/[locale]/memory/components/MemoryDeleteConfirmation.tsx +++ /dev/null @@ -1,59 +0,0 @@ -"use client"; - -import React from "react"; -import { useTranslation, Trans } from "react-i18next"; -import { Modal } from "antd"; -import { AlertCircle } from "lucide-react"; - -interface MemoryDeleteConfirmationProps { - visible: boolean; - targetTitle: string; - onOk: () => void; - onCancel: () => void; -} - -/** - * MemoryDeleteConfirmation - Confirmation dialog for clearing memory - * Used in the memory management page to confirm destructive actions - */ -export default function MemoryDeleteConfirmation({ - visible, - targetTitle, - onOk, - onCancel, -}: MemoryDeleteConfirmationProps) { - const { t } = useTranslation(); - - return ( - - - {t("memoryDeleteModal.title")} -
- } - onOk={onOk} - onCancel={onCancel} - okText={t("memoryDeleteModal.clear")} - cancelText={t("common.cancel")} - okButtonProps={{ danger: true }} - destroyOnClose - centered - > -
-

- }} - /> -

-

- {t("memoryDeleteModal.prompt")} -

-
- - ); -} - diff --git a/frontend/app/[locale]/models/ModelsContent.tsx b/frontend/app/[locale]/models/ModelsContent.tsx index 6b48e8dac..87e0120d8 100644 --- a/frontend/app/[locale]/models/ModelsContent.tsx +++ b/frontend/app/[locale]/models/ModelsContent.tsx @@ -1,19 +1,15 @@ "use client"; -import React, {useRef, useState} from "react"; -import {App} from "antd"; +import {useRef} from "react"; import {motion} from "framer-motion"; import {useSetupFlow} from "@/hooks/useSetupFlow"; -import {configStore} from "@/lib/config"; -import {configService} from "@/services/configService"; import { ConnectionStatus, } from "@/const/modelConfig"; import AppModelConfig from "./ModelConfiguration"; import {ModelConfigSectionRef} from "./components/modelConfig"; -import EmbedderCheckModal from "./components/model/EmbedderCheckModal"; interface ModelsContentProps { /** Custom next button handler (optional) */ @@ -33,21 +29,16 @@ interface ModelsContentProps { * Can be used in setup flow or as standalone page */ export default function ModelsContent({ - onNext: customOnNext, connectionStatus: externalConnectionStatus, isCheckingConnection: externalIsCheckingConnection, onCheckConnection: externalOnCheckConnection, onConnectionStatusChange, }: ModelsContentProps) { - const {message} = App.useApp(); - // Use custom hook for common setup flow logic const { canAccessProtectedData, pageVariants, pageTransition, - router, - t, } = useSetupFlow({ requireAdmin: true, externalConnectionStatus, @@ -57,69 +48,8 @@ export default function ModelsContent({ nonAdminRedirect: "/setup/knowledges", }); - const [embeddingModalOpen, setEmbeddingModalOpen] = useState(false); - const [pendingJump, setPendingJump] = useState(false); - const [connectivityWarningOpen, setConnectivityWarningOpen] = useState(false); - const [liveSelectedModels, setLiveSelectedModels] = useState - > | null>(null); const modelConfigSectionRef = useRef(null); - // Centralized behavior: save current config and navigate to next page - const saveAndNavigateNext = async () => { - try { - const currentConfig = configStore.getConfig(); - const ok = await configService.saveConfigToBackend(currentConfig as any); - if (!ok) { - message.error(t("setup.page.error.saveConfig")); - } - } catch (e) { - message.error(t("setup.page.error.saveConfig")); - } - - // Call custom onNext if provided, otherwise navigate to default next page - if (customOnNext) { - customOnNext(); - } else { - router.push("/setup/knowledges"); - } - }; - - const handleEmbeddingOk = async () => { - setEmbeddingModalOpen(false); - if (pendingJump) { - setPendingJump(false); - await saveAndNavigateNext(); - } - }; - - const handleConnectivityOk = async () => { - setConnectivityWarningOpen(false); - if (pendingJump) { - setPendingJump(false); - // Apply live selections programmatically to mimic dropdown onChange - try { - const ref = modelConfigSectionRef.current; - const selections = liveSelectedModels || {}; - if (ref && selections) { - // Iterate categories and options - for (const [category, options] of Object.entries(selections)) { - for (const [option, displayName] of Object.entries(options)) { - if (displayName) { - // Simulate dropdown change and trigger onChange flow - await ref.simulateDropdownChange(category, option, displayName); - } - } - } - } - } catch (e) { - message.error(t("setup.page.error.saveConfig")); - } - await saveAndNavigateNext(); - } - }; - return ( <> {canAccessProtectedData ? ( - setLiveSelectedModels(selected) - } + onSelectedModelsChange={() => {}} onEmbeddingConnectivityChange={() => {}} forwardedRef={modelConfigSectionRef} canAccessProtectedData={canAccessProtectedData} @@ -144,17 +72,6 @@ export default function ModelsContent({
- setEmbeddingModalOpen(false)} - connectivityWarningOpen={connectivityWarningOpen} - onConnectivityOk={handleConnectivityOk} - onConnectivityCancel={() => setConnectivityWarningOpen(false)} - modifyWarningOpen={false} - onModifyOk={() => {}} - onModifyCancel={() => {}} - /> ); } diff --git a/frontend/app/[locale]/models/components/model/EmbedderCheckModal.tsx b/frontend/app/[locale]/models/components/model/EmbedderCheckModal.tsx deleted file mode 100644 index a519e63e0..000000000 --- a/frontend/app/[locale]/models/components/model/EmbedderCheckModal.tsx +++ /dev/null @@ -1,144 +0,0 @@ -"use client"; - -import { useTranslation } from "react-i18next"; -import { Modal, Button } from "antd"; -import { WarningFilled } from "@ant-design/icons"; - -interface EmbedderCheckModalProps { - // Existing empty selection warning modal - emptyWarningOpen: boolean; - onEmptyOk: () => void; - onEmptyCancel: () => void; - - // New connectivity warning modal - connectivityWarningOpen: boolean; - onConnectivityOk: () => void; - onConnectivityCancel: () => void; - - // New modify embedding confirmation modal - modifyWarningOpen: boolean; - onModifyOk: () => void; - onModifyCancel: () => void; -} - -export default function EmbedderCheckModal(props: EmbedderCheckModalProps) { - const { t } = useTranslation(); - const { - emptyWarningOpen, - onEmptyOk, - onEmptyCancel, - connectivityWarningOpen, - onConnectivityOk, - onConnectivityCancel, - modifyWarningOpen, - onModifyOk, - onModifyCancel, - } = props; - - return ( - <> - {/* Existing empty embedding selection warning */} - - - -
- } - > -
-
- -
-
{t("embedding.emptyWarningModal.content")}
-
-
- {t("embedding.emptyWarningModal.tip")} -
-
-
-
- - - {/* New connectivity check warning */} - - - -
- } - > -
-
- -
-
- {t("embedding.unavaliableWarningModal.content")} -
-
- {t("embedding.unavaliableWarningModal.tip")} -
-
-
-
-
- - {/* New modify embedding confirmation warning */} - - - -
- } - > -
-
- -
-
- {t("embedding.modifyWarningModal.content")} -
-
-
-
- - - ); -} - - diff --git a/frontend/app/[locale]/models/components/modelConfig.tsx b/frontend/app/[locale]/models/components/modelConfig.tsx index 968cac51f..24bf23850 100644 --- a/frontend/app/[locale]/models/components/modelConfig.tsx +++ b/frontend/app/[locale]/models/components/modelConfig.tsx @@ -25,7 +25,7 @@ import log from "@/lib/logger"; import { ModelListCard } from "./model/ModelListCard"; import { ModelAddDialog } from "./model/ModelAddDialog"; import { ModelDeleteDialog } from "./model/ModelDeleteDialog"; -import EmbedderCheckModal from "./model/EmbedderCheckModal"; +import { useConfirmModal } from "@/hooks/useConfirmModal"; // ModelConnectStatus type definition type ModelConnectStatus = (typeof MODEL_STATUS)[keyof typeof MODEL_STATUS]; @@ -98,13 +98,13 @@ export const ModelConfigSection = forwardRef< const { skipVerification = false, canAccessProtectedData = false } = props; const { modelConfig, updateModelConfig } = useConfig(); const modelData = getModelData(t); + const { confirm } = useConfirmModal(); // State management const [models, setModels] = useState([]); const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isVerifying, setIsVerifying] = useState(false); - const [isModifyWarningOpen, setIsModifyWarningOpen] = useState(false); // Error state management const [errorFields, setErrorFields] = useState<{ [key: string]: boolean }>({ @@ -118,11 +118,6 @@ export const ModelConfigSection = forwardRef< // Throttle timer const throttleTimerRef = useRef(null); const saveTimerRef = useRef(null); - const pendingChangeRef = useRef<{ - category: string; - option: string; - displayName: string; - } | null>(null); // Debounced auto-save scheduler const scheduleAutoSave = () => { @@ -777,8 +772,22 @@ export const ModelConfigSection = forwardRef< const currentValue = selectedModels[category]?.[option] || ""; // Only prompt when modifying from a non-empty value to a different value if (currentValue && currentValue !== displayName) { - pendingChangeRef.current = { category, option, displayName }; - setIsModifyWarningOpen(true); + confirm({ + title: t("embedding.modifyWarningModal.title"), + content: ( +
+
+ {t("embedding.modifyWarningModal.content")} +
+
+ ), + okText: t("embedding.modifyWarningModal.ok_proceed"), + cancelText: t("common.cancel"), + danger: false, + onOk: async () => { + await applyModelChange(category, option, displayName); + }, + }); return; } if (currentValue === displayName) { @@ -789,25 +798,6 @@ export const ModelConfigSection = forwardRef< await applyModelChange(category, option, displayName); }; - const handleModifyOk = async () => { - const pending = pendingChangeRef.current; - if (pending) { - await handleModelChange( - pending.category, - pending.option, - pending.displayName, - true - ); - } - pendingChangeRef.current = null; - setIsModifyWarningOpen(false); - }; - - const handleModifyCancel = () => { - pendingChangeRef.current = null; - setIsModifyWarningOpen(false); - }; - // Only update local UI state, no database operations involved const updateModelStatus = ( displayName: string, @@ -1058,17 +1048,7 @@ export const ModelConfigSection = forwardRef< models={models} /> - {}} - onEmptyCancel={() => {}} - connectivityWarningOpen={false} - onConnectivityOk={() => {}} - onConnectivityCancel={() => {}} - modifyWarningOpen={isModifyWarningOpen} - onModifyOk={handleModifyOk} - onModifyCancel={handleModifyCancel} - /> +
); From 15cf7a379d8e8a271c68bd7c6053da351d06e867 Mon Sep 17 00:00:00 2001 From: wmc1112 <759659013@qq.com> Date: Wed, 17 Dec 2025 11:25:37 +0800 Subject: [PATCH 22/36] =?UTF-8?q?=F0=9F=90=9B=20Bugfix:=20the=20deployment?= =?UTF-8?q?=20pipeline=20deploy.sh=20failed=20to=20execute=20#2096?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-deploy.yml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-deploy.yml b/.github/workflows/docker-deploy.yml index 82107df0c..9d04c8913 100644 --- a/.github/workflows/docker-deploy.yml +++ b/.github/workflows/docker-deploy.yml @@ -15,6 +15,11 @@ on: description: 'runner array in json format (e.g. ["ubuntu-latest"] or ["self-hosted"])' required: true default: '[]' + app_version: + description: 'Docker image tag to build and deploy (e.g. v1.7.1)' + required: true + default: 'latest' + type: string jobs: build-main: @@ -23,7 +28,7 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - name: Build main application image - run: docker build --build-arg MIRROR=https://pypi.tuna.tsinghua.edu.cn/simple --build-arg APT_MIRROR=tsinghua -t nexent/nexent -f make/main/Dockerfile . + run: docker build --build-arg MIRROR=https://pypi.tuna.tsinghua.edu.cn/simple --build-arg APT_MIRROR=tsinghua -t nexent/nexent:${{ github.event.inputs.app_version }} -t nexent/nexent -f make/main/Dockerfile . build-data-process: runs-on: ${{ fromJson(inputs.runner_label_json) }} @@ -47,7 +52,7 @@ jobs: GIT_TRACE=1 GIT_CURL_VERBOSE=1 GIT_LFS_LOG=debug git lfs pull rm -rf .git .gitattributes - name: Build data process image - run: docker build --build-arg MIRROR=https://pypi.tuna.tsinghua.edu.cn/simple --build-arg APT_MIRROR=tsinghua -t nexent/nexent-data-process -f make/data_process/Dockerfile . + run: docker build --build-arg MIRROR=https://pypi.tuna.tsinghua.edu.cn/simple --build-arg APT_MIRROR=tsinghua -t nexent/nexent-data-process:${{ github.event.inputs.app_version }} -t nexent/nexent-data-process -f make/data_process/Dockerfile . build-web: runs-on: ${{ fromJson(inputs.runner_label_json) }} @@ -55,7 +60,7 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - name: Build web frontend image - run: docker build --build-arg MIRROR=https://registry.npmmirror.com --build-arg APK_MIRROR=tsinghua -t nexent/nexent-web -f make/web/Dockerfile . + run: docker build --build-arg MIRROR=https://registry.npmmirror.com --build-arg APK_MIRROR=tsinghua -t nexent/nexent-web:${{ github.event.inputs.app_version }} -t nexent/nexent-web -f make/web/Dockerfile . build-docs: runs-on: ${{ fromJson(inputs.runner_label_json) }} @@ -63,7 +68,7 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - name: Build docs image - run: docker build --progress=plain -t nexent/nexent-docs -f make/docs/Dockerfile . + run: docker build --progress=plain -t nexent/nexent-docs:${{ github.event.inputs.app_version }} -t nexent/nexent-docs -f make/docs/Dockerfile . deploy: runs-on: ${{ fromJson(inputs.runner_label_json) }} @@ -76,6 +81,9 @@ jobs: rm -rf $HOME/nexent mkdir -p $HOME/nexent cp -r $GITHUB_WORKSPACE/* $HOME/nexent/ + - name: Force APP_VERSION to latest in deploy.sh (CI only) + run: | + sed -i 's/APP_VERSION="$(get_app_version)"/APP_VERSION="${{ github.event.inputs.app_version }}"/' $HOME/nexent/docker/deploy.sh - name: Start docs container run: | docker stop nexent-docs 2>/dev/null || true From 3eb3e8ab6173de5a2be78725b9e74b7541037f7b Mon Sep 17 00:00:00 2001 From: wmc1112 <759659013@qq.com> Date: Wed, 17 Dec 2025 11:26:38 +0800 Subject: [PATCH 23/36] =?UTF-8?q?=F0=9F=90=9B=20Bugfix:=20Skip=20MCP=20too?= =?UTF-8?q?ls=20as=20they=20will=20be=20handled=20in=20the=20MCP=20server?= =?UTF-8?q?=20installation=20step=20#1841?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/components/agent/AgentImportWizard.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/components/agent/AgentImportWizard.tsx b/frontend/components/agent/AgentImportWizard.tsx index aa1cc027c..e62533867 100644 --- a/frontend/components/agent/AgentImportWizard.tsx +++ b/frontend/components/agent/AgentImportWizard.tsx @@ -602,6 +602,12 @@ export default function AgentImportWizard({ const agentDisplayName = (agentInfo as any)?.display_name || (agentInfo as any)?.name || `${t("market.install.agent.defaultName", "Agent")} ${agentKey}`; if (Array.isArray((agentInfo as any)?.tools)) { (agentInfo as any).tools.forEach((tool: any) => { + // Skip MCP tools as they will be handled in the MCP server installation step + const toolSource = (tool?.source || "").toLowerCase(); + if (toolSource === "mcp") { + return; + } + const rawName = tool?.name || tool?.origin_name || tool?.class_name; const name = typeof rawName === "string" ? rawName.trim() : ""; if (!name) return; From bec3558979adbc9b696d02b6cd60ac522aba5b0c Mon Sep 17 00:00:00 2001 From: xuyaqi Date: Wed, 17 Dec 2025 15:39:26 +0800 Subject: [PATCH 24/36] Unify global modal, delete unused code --- .../app/[locale]/agents/AgentsContent.tsx | 29 ------ .../components/AgentSetupOrchestrator.tsx | 43 ++------- .../components/document/DocumentChunk.tsx | 93 ++++++------------- 3 files changed, 37 insertions(+), 128 deletions(-) diff --git a/frontend/app/[locale]/agents/AgentsContent.tsx b/frontend/app/[locale]/agents/AgentsContent.tsx index 5436e3aba..84f29ac28 100644 --- a/frontend/app/[locale]/agents/AgentsContent.tsx +++ b/frontend/app/[locale]/agents/AgentsContent.tsx @@ -9,7 +9,6 @@ import { } from "@/const/modelConfig"; import AgentConfig, {AgentConfigHandle} from "./AgentConfiguration"; -import SaveConfirmModal from "./components/SaveConfirmModal"; interface AgentsContentProps { /** Whether currently saving */ @@ -39,8 +38,6 @@ export default forwardRef(function Agents onSavingStateChange, }: AgentsContentProps, ref) { const agentConfigRef = useRef(null); - const [showSaveConfirm, setShowSaveConfirm] = useState(false); - const pendingNavRef = useRef void)>(null); // Use custom hook for common setup flow logic const { @@ -95,32 +92,6 @@ export default forwardRef(function Agents ) : null}
- - { - // Reload data from backend to discard changes - await agentConfigRef.current?.reloadCurrentAgentData?.(); - setShowSaveConfirm(false); - const go = pendingNavRef.current; - pendingNavRef.current = null; - if (go) go(); - }} - onSave={async () => { - try { - setInternalIsSaving(true); - await agentConfigRef.current?.saveAllChanges?.(); - setShowSaveConfirm(false); - const go = pendingNavRef.current; - pendingNavRef.current = null; - if (go) go(); - } catch (e) { - // errors are surfaced by underlying save - } finally { - setInternalIsSaving(false); - } - }} - /> ); }); diff --git a/frontend/app/[locale]/agents/components/AgentSetupOrchestrator.tsx b/frontend/app/[locale]/agents/components/AgentSetupOrchestrator.tsx index b077ef785..a89664114 100644 --- a/frontend/app/[locale]/agents/components/AgentSetupOrchestrator.tsx +++ b/frontend/app/[locale]/agents/components/AgentSetupOrchestrator.tsx @@ -118,9 +118,7 @@ export default function AgentSetupOrchestrator({ - // Embedding auto-unselect notice modal - const [isEmbeddingAutoUnsetOpen, setIsEmbeddingAutoUnsetOpen] = - useState(false); + const lastProcessedAgentIdForEmbedding = useRef(null); // Flag to track if we need to refresh enabledToolIds after tools update @@ -672,7 +670,12 @@ export default function AgentSetupOrchestrator({ } catch (error) { // Even if API fails, still inform user and prevent usage in UI } finally { - setIsEmbeddingAutoUnsetOpen(true); + confirm({ + title: t("embedding.agentToolAutoDeselectModal.title"), + content: t("embedding.agentToolAutoDeselectModal.content"), + okText: t("common.confirm"), + onOk: () => {}, + }); lastProcessedAgentIdForEmbedding.current = currentAgentId; } }; @@ -2443,37 +2446,7 @@ export default function AgentSetupOrchestrator({ } /> {/* Auto unselect knowledge_base_search notice when embedding not configured */} - setIsEmbeddingAutoUnsetOpen(false)} - centered - footer={ -
- -
- } - width={520} - > -
-
- -
-
- {t("embedding.agentToolAutoDeselectModal.content")} -
-
-
-
-
+ {/* Agent call relationship modal */} = ({ }) => { const { t } = useTranslation(); const { message } = App.useApp(); + const { confirm } = useConfirmModal(); const [chunks, setChunks] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(false); @@ -107,8 +108,6 @@ const DocumentChunk: React.FC = ({ const [chunkSubmitting, setChunkSubmitting] = useState(false); const [editingChunk, setEditingChunk] = useState(null); const [chunkForm] = Form.useForm(); - const [deleteModalOpen, setDeleteModalOpen] = useState(false); - const [chunkToDelete, setChunkToDelete] = useState(null); const [tooltipResetKey, setTooltipResetKey] = useState(0); const resetChunkSearch = React.useCallback(() => { @@ -455,39 +454,32 @@ const DocumentChunk: React.FC = ({ } forceCloseTooltips(); - setChunkToDelete(chunk); - setDeleteModalOpen(true); - }; - - const handleDeleteConfirm = async () => { - if (!chunkToDelete?.id || !knowledgeBaseName) { - return; - } - - try { - await knowledgeBaseService.deleteChunk( - knowledgeBaseName, - chunkToDelete.id - ); - message.success(t("document.chunk.success.delete")); - setDeleteModalOpen(false); - setChunkToDelete(null); - forceCloseTooltips(); - await refreshChunks(); - } catch (error) { - log.error("Failed to delete chunk:", error); - message.error( - error instanceof Error && error.message - ? error.message - : t("document.chunk.error.deleteFailed") - ); - } - }; - - const handleDeleteCancel = () => { - setDeleteModalOpen(false); - setChunkToDelete(null); - forceCloseTooltips(); + + confirm({ + title: t("document.chunk.confirm.deleteTitle"), + content: t("document.chunk.confirm.deleteContent"), + okText: t("common.delete"), + cancelText: t("common.cancel"), + danger: true, + onOk: async () => { + try { + await knowledgeBaseService.deleteChunk(knowledgeBaseName, chunk.id); + message.success(t("document.chunk.success.delete")); + forceCloseTooltips(); + await refreshChunks(); + } catch (error) { + log.error("Failed to delete chunk:", error); + message.error( + error instanceof Error && error.message + ? error.message + : t("document.chunk.error.deleteFailed") + ); + } + }, + onCancel: () => { + forceCloseTooltips(); + }, + }); }; const renderDocumentLabel = (doc: Document, chunkCount: number) => { @@ -847,34 +839,7 @@ const DocumentChunk: React.FC = ({ - - - -
- } - > -
-
- -
-
- {t("document.chunk.confirm.deleteContent")} -
-
-
-
- + ); }; From 85f347f2375d4f34ac337c7210b68222ba63bec2 Mon Sep 17 00:00:00 2001 From: xuyaqi Date: Wed, 17 Dec 2025 15:54:01 +0800 Subject: [PATCH 25/36] Delete unused modals --- .../components/agent/AgentConfigModal.tsx | 36 ++----------------- .../models/components/modelConfig.tsx | 1 - 2 files changed, 2 insertions(+), 35 deletions(-) diff --git a/frontend/app/[locale]/agents/components/agent/AgentConfigModal.tsx b/frontend/app/[locale]/agents/components/agent/AgentConfigModal.tsx index a987dfaee..4245d8ce3 100644 --- a/frontend/app/[locale]/agents/components/agent/AgentConfigModal.tsx +++ b/frontend/app/[locale]/agents/components/agent/AgentConfigModal.tsx @@ -48,8 +48,6 @@ export interface AgentConfigModalProps { isGeneratingAgent?: boolean; // Add new props for action buttons onDebug?: () => void; - onDeleteAgent?: () => void; - onDeleteSuccess?: () => void; // New prop for handling delete success onSaveAgent?: () => void; isCreatingNewAgent?: boolean; editingAgent?: Agent | null; @@ -84,8 +82,6 @@ export default function AgentConfigModal({ isGeneratingAgent = false, // Add new props for action buttons onDebug, - onDeleteAgent, - onDeleteSuccess, onSaveAgent, isCreatingNewAgent = false, editingAgent = null, @@ -107,8 +103,7 @@ export default function AgentConfigModal({ // Add segmented state management const [activeSegment, setActiveSegment] = useState("agent-info"); - // Add state for delete confirmation modal - const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); + // Add state for agent name validation error const [agentNameError, setAgentNameError] = useState(""); @@ -473,15 +468,7 @@ export default function AgentConfigModal({ } }, [isCreatingNewAgent, currentDisplayName, originalDisplayName]); - // Handle delete confirmation - const handleDeleteConfirm = useCallback(() => { - setIsDeleteModalVisible(false); - // Execute the delete operation - onDeleteAgent?.(); - // Call the success callback immediately after triggering delete - // The actual success/failure will be handled by the parent component - onDeleteSuccess?.(); - }, [onDeleteAgent, onDeleteSuccess]); + // Optimized click handlers using useCallback const handleSegmentClick = useCallback((segment: string) => { @@ -1196,25 +1183,6 @@ export default function AgentConfigModal({ )} - - {/* Delete Confirmation Modal */} - setIsDeleteModalVisible(false)} - okText={t("businessLogic.config.modal.button.confirm")} - cancelText={t("businessLogic.config.modal.button.cancel")} - okButtonProps={{ - danger: true, - }} - > -

- {t("businessLogic.config.modal.deleteContent", { - name: agentName || "Unnamed Agent", - })} -

-
); } diff --git a/frontend/app/[locale]/models/components/modelConfig.tsx b/frontend/app/[locale]/models/components/modelConfig.tsx index 24bf23850..7252292ee 100644 --- a/frontend/app/[locale]/models/components/modelConfig.tsx +++ b/frontend/app/[locale]/models/components/modelConfig.tsx @@ -783,7 +783,6 @@ export const ModelConfigSection = forwardRef< ), okText: t("embedding.modifyWarningModal.ok_proceed"), cancelText: t("common.cancel"), - danger: false, onOk: async () => { await applyModelChange(category, option, displayName); }, From f167937d72b043882b041f2ba99acc27c34aa076 Mon Sep 17 00:00:00 2001 From: xuyaqi Date: Wed, 17 Dec 2025 15:57:20 +0800 Subject: [PATCH 26/36] Delete unused navbar --- frontend/app/[locale]/setup/SetupLayout.tsx | 84 +------------ frontend/components/ui/navbar.tsx | 129 -------------------- 2 files changed, 1 insertion(+), 212 deletions(-) delete mode 100644 frontend/components/ui/navbar.tsx diff --git a/frontend/app/[locale]/setup/SetupLayout.tsx b/frontend/app/[locale]/setup/SetupLayout.tsx index 81d769298..e6a060e7d 100644 --- a/frontend/app/[locale]/setup/SetupLayout.tsx +++ b/frontend/app/[locale]/setup/SetupLayout.tsx @@ -1,89 +1,8 @@ "use client"; -import React, {ReactNode} from "react"; +import {ReactNode} from "react"; import {useTranslation} from "react-i18next"; -import {Badge, Button, Dropdown} from "antd"; -import {DownOutlined} from "@ant-design/icons"; -import {FiRefreshCw} from "react-icons/fi"; -import {Globe} from "lucide-react"; -import {languageOptions} from "@/const/constants"; -import {useLanguageSwitch} from "@/lib/language"; -import {CONNECTION_STATUS, ConnectionStatus,} from "@/const/modelConfig"; - -// ================ Setup Header Content Components ================ -// These components are exported so they can be used to customize the TopNavbar - -interface SetupHeaderRightContentProps { - connectionStatus: ConnectionStatus; - isCheckingConnection: boolean; - onCheckConnection: () => void; -} - -export function SetupHeaderRightContent({ - connectionStatus, - isCheckingConnection, - onCheckConnection, -}: SetupHeaderRightContentProps) { - const { t } = useTranslation(); - const { currentLanguage, handleLanguageChange } = useLanguageSwitch(); - - // Get status text - const getStatusText = () => { - switch (connectionStatus) { - case CONNECTION_STATUS.SUCCESS: - return t("setup.header.status.connected"); - case CONNECTION_STATUS.ERROR: - return t("setup.header.status.disconnected"); - case CONNECTION_STATUS.PROCESSING: - return t("setup.header.status.checking"); - default: - return t("setup.header.status.unknown"); - } - }; - - return ( -
- ); -} - // ================ Navigation ================ interface NavigationProps { onBack?: () => void; @@ -179,7 +98,6 @@ interface SetupLayoutProps { /** * SetupLayout - Content wrapper for setup pages * This component should be wrapped by NavigationLayout - * Use SetupHeaderRightContent for customizing the top navbar */ export default function SetupLayout({ children, diff --git a/frontend/components/ui/navbar.tsx b/frontend/components/ui/navbar.tsx deleted file mode 100644 index 45a938e56..000000000 --- a/frontend/components/ui/navbar.tsx +++ /dev/null @@ -1,129 +0,0 @@ -"use client"; - -import { Button } from "@/components/ui/button"; -import { AvatarDropdown } from "@/components/auth/avatarDropdown"; -import { useTranslation } from "react-i18next"; -import { useAuth } from "@/hooks/useAuth"; -import { Globe } from "lucide-react"; -import { Dropdown } from "antd"; -import { DownOutlined } from "@ant-design/icons"; -import Link from "next/link"; -import { HEADER_CONFIG } from "@/const/layoutConstants"; -import { languageOptions } from "@/const/constants"; -import { useLanguageSwitch } from "@/lib/language"; - -/** - * Main navigation bar component - * Displays logo, navigation links, language switcher, and user authentication status - */ -export function Navbar() { - const { t } = useTranslation("common"); - const { user, isLoading: userLoading, isSpeedMode } = useAuth(); - const { currentLanguage, handleLanguageChange } = useLanguageSwitch(); - - return ( -
- {/* Left section - Logo */} - -

- ModelEngine - - {t("assistant.name")} - -

- - - {/* Right section - Navigation links and user controls */} -
- {/* GitHub link */} - - - Github - - - {/* ModelEngine link */} - - ModelEngine - - - {/* Language switcher */} - ({ - key: opt.value, - label: opt.label, - })), - onClick: ({ key }) => handleLanguageChange(key as string), - }} - > - - - {languageOptions.find((o) => o.value === currentLanguage)?.label || - currentLanguage} - - - - - {/* User status - only shown in full version */} - {!isSpeedMode && ( - <> - {userLoading ? ( - - {t("common.loading")}... - - ) : user ? ( - - {user.email} - - ) : null} - - - )} -
- - {/* Mobile hamburger menu button */} - -
- ); -} - From 705fdd140aa5fcc041c9e3df0848fcab3e413a2a Mon Sep 17 00:00:00 2001 From: xuyaqi Date: Wed, 17 Dec 2025 16:06:54 +0800 Subject: [PATCH 27/36] Fix typeScript type check --- frontend/app/[locale]/agents/components/PromptManager.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/[locale]/agents/components/PromptManager.tsx b/frontend/app/[locale]/agents/components/PromptManager.tsx index f7cf9aea3..bf447c407 100644 --- a/frontend/app/[locale]/agents/components/PromptManager.tsx +++ b/frontend/app/[locale]/agents/components/PromptManager.tsx @@ -17,7 +17,7 @@ import { updateAgent } from "@/services/agentConfigService"; import { modelService } from "@/services/modelService"; import { ModelOption } from "@/types/modelConfig"; -import AgentConfigModal from "./agent/AgentConfigModal"; +import AgentConfigModal, { AgentConfigModalProps } from "./agent/AgentConfigModal"; import log from "@/lib/logger"; From 7766c062b221a3f6e4b5f7d4dc696705601df773 Mon Sep 17 00:00:00 2001 From: xuyaqi Date: Wed, 17 Dec 2025 16:16:30 +0800 Subject: [PATCH 28/36] Revert "Delete unused modals" This reverts commit 85f347f2375d4f34ac337c7210b68222ba63bec2. --- .../components/agent/AgentConfigModal.tsx | 36 +++++++++++++++++-- .../models/components/modelConfig.tsx | 1 + 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/frontend/app/[locale]/agents/components/agent/AgentConfigModal.tsx b/frontend/app/[locale]/agents/components/agent/AgentConfigModal.tsx index 4245d8ce3..a987dfaee 100644 --- a/frontend/app/[locale]/agents/components/agent/AgentConfigModal.tsx +++ b/frontend/app/[locale]/agents/components/agent/AgentConfigModal.tsx @@ -48,6 +48,8 @@ export interface AgentConfigModalProps { isGeneratingAgent?: boolean; // Add new props for action buttons onDebug?: () => void; + onDeleteAgent?: () => void; + onDeleteSuccess?: () => void; // New prop for handling delete success onSaveAgent?: () => void; isCreatingNewAgent?: boolean; editingAgent?: Agent | null; @@ -82,6 +84,8 @@ export default function AgentConfigModal({ isGeneratingAgent = false, // Add new props for action buttons onDebug, + onDeleteAgent, + onDeleteSuccess, onSaveAgent, isCreatingNewAgent = false, editingAgent = null, @@ -103,7 +107,8 @@ export default function AgentConfigModal({ // Add segmented state management const [activeSegment, setActiveSegment] = useState("agent-info"); - + // Add state for delete confirmation modal + const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); // Add state for agent name validation error const [agentNameError, setAgentNameError] = useState(""); @@ -468,7 +473,15 @@ export default function AgentConfigModal({ } }, [isCreatingNewAgent, currentDisplayName, originalDisplayName]); - + // Handle delete confirmation + const handleDeleteConfirm = useCallback(() => { + setIsDeleteModalVisible(false); + // Execute the delete operation + onDeleteAgent?.(); + // Call the success callback immediately after triggering delete + // The actual success/failure will be handled by the parent component + onDeleteSuccess?.(); + }, [onDeleteAgent, onDeleteSuccess]); // Optimized click handlers using useCallback const handleSegmentClick = useCallback((segment: string) => { @@ -1183,6 +1196,25 @@ export default function AgentConfigModal({ )} + + {/* Delete Confirmation Modal */} + setIsDeleteModalVisible(false)} + okText={t("businessLogic.config.modal.button.confirm")} + cancelText={t("businessLogic.config.modal.button.cancel")} + okButtonProps={{ + danger: true, + }} + > +

+ {t("businessLogic.config.modal.deleteContent", { + name: agentName || "Unnamed Agent", + })} +

+
); } diff --git a/frontend/app/[locale]/models/components/modelConfig.tsx b/frontend/app/[locale]/models/components/modelConfig.tsx index 7252292ee..24bf23850 100644 --- a/frontend/app/[locale]/models/components/modelConfig.tsx +++ b/frontend/app/[locale]/models/components/modelConfig.tsx @@ -783,6 +783,7 @@ export const ModelConfigSection = forwardRef< ), okText: t("embedding.modifyWarningModal.ok_proceed"), cancelText: t("common.cancel"), + danger: false, onOk: async () => { await applyModelChange(category, option, displayName); }, From 37087542bc136cc1ebde23184dfb5a1042e55af7 Mon Sep 17 00:00:00 2001 From: xuyaqi Date: Wed, 17 Dec 2025 16:24:41 +0800 Subject: [PATCH 29/36] Delete modals that will never be opened --- .../components/AgentSetupOrchestrator.tsx | 28 --------------- .../agents/components/PromptManager.tsx | 6 ---- .../components/agent/AgentConfigModal.tsx | 36 ------------------- 3 files changed, 70 deletions(-) diff --git a/frontend/app/[locale]/agents/components/AgentSetupOrchestrator.tsx b/frontend/app/[locale]/agents/components/AgentSetupOrchestrator.tsx index a89664114..e5718c98b 100644 --- a/frontend/app/[locale]/agents/components/AgentSetupOrchestrator.tsx +++ b/frontend/app/[locale]/agents/components/AgentSetupOrchestrator.tsx @@ -2014,32 +2014,6 @@ export default function AgentSetupOrchestrator({ }); }; - // Handle exit edit mode - const handleExitEdit = () => { - setIsEditingAgent(false); - setEditingAgent(null); - // Use the parent's exit creation handler to properly clear cache - if (isCreatingNewAgent && onExitCreation) { - onExitCreation(); - } else { - setIsCreatingNewAgent(false); - } - setBusinessLogic(""); - setDutyContent(""); - setConstraintContent(""); - setFewShotsContent(""); - setAgentName?.(""); - setAgentDescription?.(""); - // Reset mainAgentId and enabledAgentIds - setMainAgentId(null); - setEnabledAgentIds([]); - // Reset selected tools - setSelectedTools([]); - setEnabledToolIds([]); - // Notify parent component about editing state change - onEditingStateChange?.(false, null); - }; - // Refresh tool list const handleToolsRefresh = useCallback( async (showSuccessMessage = true) => { @@ -2298,8 +2272,6 @@ export default function AgentSetupOrchestrator({ isCreatingNewAgent={isCreatingNewAgent} canSaveAgent={localCanSaveAgent} getButtonTitle={getLocalButtonTitle} - onDeleteAgent={onDeleteAgent || (() => {})} - onDeleteSuccess={handleExitEdit} editingAgent={editingAgentFromParent || editingAgent} onViewCallRelationship={handleViewCallRelationship} /> diff --git a/frontend/app/[locale]/agents/components/PromptManager.tsx b/frontend/app/[locale]/agents/components/PromptManager.tsx index bf447c407..d2eb4798c 100644 --- a/frontend/app/[locale]/agents/components/PromptManager.tsx +++ b/frontend/app/[locale]/agents/components/PromptManager.tsx @@ -229,8 +229,6 @@ export interface PromptManagerProps { onGenerateAgent?: (model: ModelOption) => void; onSaveAgent?: () => void; onDebug?: () => void; - onDeleteAgent?: () => void; - onDeleteSuccess?: () => void; getButtonTitle?: () => string; onViewCallRelationship?: () => void; @@ -276,8 +274,6 @@ export default function PromptManager({ onGenerateAgent, onSaveAgent, onDebug, - onDeleteAgent, - onDeleteSuccess, getButtonTitle, onViewCallRelationship, editingAgent, @@ -692,8 +688,6 @@ export default function PromptManager({ onExpandCard={handleExpandCard} isGeneratingAgent={isGeneratingAgent} onDebug={onDebug} - onDeleteAgent={onDeleteAgent} - onDeleteSuccess={onDeleteSuccess} onSaveAgent={onSaveAgent} isCreatingNewAgent={isCreatingNewAgent} editingAgent={editingAgent} diff --git a/frontend/app/[locale]/agents/components/agent/AgentConfigModal.tsx b/frontend/app/[locale]/agents/components/agent/AgentConfigModal.tsx index a987dfaee..550aee29a 100644 --- a/frontend/app/[locale]/agents/components/agent/AgentConfigModal.tsx +++ b/frontend/app/[locale]/agents/components/agent/AgentConfigModal.tsx @@ -48,8 +48,6 @@ export interface AgentConfigModalProps { isGeneratingAgent?: boolean; // Add new props for action buttons onDebug?: () => void; - onDeleteAgent?: () => void; - onDeleteSuccess?: () => void; // New prop for handling delete success onSaveAgent?: () => void; isCreatingNewAgent?: boolean; editingAgent?: Agent | null; @@ -84,8 +82,6 @@ export default function AgentConfigModal({ isGeneratingAgent = false, // Add new props for action buttons onDebug, - onDeleteAgent, - onDeleteSuccess, onSaveAgent, isCreatingNewAgent = false, editingAgent = null, @@ -107,9 +103,6 @@ export default function AgentConfigModal({ // Add segmented state management const [activeSegment, setActiveSegment] = useState("agent-info"); - // Add state for delete confirmation modal - const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); - // Add state for agent name validation error const [agentNameError, setAgentNameError] = useState(""); // Add state for agent name status check @@ -473,16 +466,6 @@ export default function AgentConfigModal({ } }, [isCreatingNewAgent, currentDisplayName, originalDisplayName]); - // Handle delete confirmation - const handleDeleteConfirm = useCallback(() => { - setIsDeleteModalVisible(false); - // Execute the delete operation - onDeleteAgent?.(); - // Call the success callback immediately after triggering delete - // The actual success/failure will be handled by the parent component - onDeleteSuccess?.(); - }, [onDeleteAgent, onDeleteSuccess]); - // Optimized click handlers using useCallback const handleSegmentClick = useCallback((segment: string) => { setActiveSegment(segment); @@ -1196,25 +1179,6 @@ export default function AgentConfigModal({ )} - - {/* Delete Confirmation Modal */} - setIsDeleteModalVisible(false)} - okText={t("businessLogic.config.modal.button.confirm")} - cancelText={t("businessLogic.config.modal.button.cancel")} - okButtonProps={{ - danger: true, - }} - > -

- {t("businessLogic.config.modal.deleteContent", { - name: agentName || "Unnamed Agent", - })} -

-
); } From f292eb585596a52f0607972ceb9cb03fc0d7db1d Mon Sep 17 00:00:00 2001 From: Jasonxia007 Date: Wed, 17 Dec 2025 17:56:46 +0800 Subject: [PATCH 30/36] =?UTF-8?q?=F0=9F=8E=A8=20Standardize=20frontend=20i?= =?UTF-8?q?cons=20into=20lucide=20icon=20and=20antd=20icons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agents/components/McpConfigModal.tsx | 40 ++++++------ .../agents/components/PromptManager.tsx | 27 ++++---- .../agent/AgentCallRelationshipModal.tsx | 4 +- .../components/agent/AgentConfigModal.tsx | 11 ++-- .../agent/CollaborativeAgentDisplay.tsx | 6 +- .../agents/components/agent/SubAgentPool.tsx | 14 ++++- .../agents/components/tool/ToolPool.tsx | 32 ++++++---- .../agents/components/tool/ToolTestPanel.tsx | 14 ++--- .../[locale]/chat/components/chatInput.tsx | 49 ++++++++------- .../[locale]/chat/internal/chatAttachment.tsx | 61 ++++++++----------- .../chat/streaming/chatStreamFinalMessage.tsx | 15 +++-- .../components/document/DocumentStatus.tsx | 6 +- .../components/upload/UploadAreaUI.tsx | 5 +- .../components/model/ModelAddDialog.tsx | 22 +++---- .../components/model/ModelDeleteDialog.tsx | 19 +++--- .../models/components/modelConfig.tsx | 20 +++--- frontend/app/[locale]/page.tsx | 4 +- frontend/app/[locale]/setup/SetupLayout.tsx | 12 ++-- .../components/agent/AgentImportWizard.tsx | 20 +++--- frontend/components/auth/avatarDropdown.tsx | 27 ++++---- frontend/components/auth/loginModal.tsx | 6 +- frontend/components/auth/registerModal.tsx | 26 ++++---- frontend/components/auth/sessionListeners.tsx | 4 +- frontend/components/navigation/TopNavbar.tsx | 5 +- .../ui/AgentCallRelationshipModal.tsx | 4 +- frontend/components/ui/navbar.tsx | 5 +- frontend/lib/utils.ts | 14 ++--- 27 files changed, 235 insertions(+), 237 deletions(-) diff --git a/frontend/app/[locale]/agents/components/McpConfigModal.tsx b/frontend/app/[locale]/agents/components/McpConfigModal.tsx index d6405f11c..14ea9ebb0 100644 --- a/frontend/app/[locale]/agents/components/McpConfigModal.tsx +++ b/frontend/app/[locale]/agents/components/McpConfigModal.tsx @@ -15,14 +15,14 @@ import { App, } from "antd"; import { - DeleteOutlined, - EyeOutlined, - PlusOutlined, - LoadingOutlined, - ExpandAltOutlined, - CompressOutlined, - RedoOutlined, -} from "@ant-design/icons"; + Trash, + Eye, + Plus, + LoaderCircle, + Maximize, + Minimize, + RefreshCw, +} from "lucide-react"; import { McpConfigModalProps, AgentRefreshEvent } from "@/types/agentConfig"; import { @@ -299,8 +299,9 @@ export default function McpConfigModal({ }} > {healthCheckLoading[key] ? ( - ) : null} @@ -328,7 +329,7 @@ export default function McpConfigModal({ ) : ( @@ -652,7 +651,7 @@ export default function PromptManager({ style={{ border: "none" }} > {loadingModels ? ( - + ) : ( )} diff --git a/frontend/app/[locale]/agents/components/agent/AgentCallRelationshipModal.tsx b/frontend/app/[locale]/agents/components/agent/AgentCallRelationshipModal.tsx index c8ac7513f..dfb6faeaf 100644 --- a/frontend/app/[locale]/agents/components/agent/AgentCallRelationshipModal.tsx +++ b/frontend/app/[locale]/agents/components/agent/AgentCallRelationshipModal.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useCallback, useRef } from "react"; import { Modal, Spin, message, Typography } from "antd"; -import { RobotOutlined, ToolOutlined } from "@ant-design/icons"; +import { Bot, Wrench } from "lucide-react"; import { useTranslation } from "react-i18next"; import Tree from "react-d3-tree"; @@ -71,7 +71,7 @@ const CustomNode = ({ nodeDatum }: any) => { nodeDatum.type === AGENT_CALL_RELATIONSHIP_NODE_TYPES.MAIN || nodeDatum.type === AGENT_CALL_RELATIONSHIP_NODE_TYPES.SUB; const color = getNodeColor(nodeDatum.type, nodeDatum.depth); - const icon = isAgent ? : ; + const icon = isAgent ? : ; // Truncate tool names by maximum character count (avoid too long) const rawName: string = nodeDatum.name || ""; diff --git a/frontend/app/[locale]/agents/components/agent/AgentConfigModal.tsx b/frontend/app/[locale]/agents/components/agent/AgentConfigModal.tsx index a987dfaee..bd4e8026f 100644 --- a/frontend/app/[locale]/agents/components/agent/AgentConfigModal.tsx +++ b/frontend/app/[locale]/agents/components/agent/AgentConfigModal.tsx @@ -3,10 +3,7 @@ import { useState, useEffect, useCallback } from "react"; import { useTranslation } from "react-i18next"; import { Button, Modal, Spin, Input, Select, InputNumber } from "antd"; -import { - LoadingOutlined, -} from "@ant-design/icons"; -import { Bug, Save, Maximize2 } from "lucide-react"; +import { Bug, Save, Maximize2, LoaderCircle } from "lucide-react"; import log from "@/lib/logger"; import { ModelOption } from "@/types/modelConfig"; @@ -860,7 +857,7 @@ export default function AgentConfigModal({ > {t("systemPrompt.card.duty.title")} {isGeneratingAgent && activeSegment === "duty" && ( - + )} diff --git a/frontend/app/[locale]/agents/components/agent/CollaborativeAgentDisplay.tsx b/frontend/app/[locale]/agents/components/agent/CollaborativeAgentDisplay.tsx index 9659ee4a0..0e6ed669a 100644 --- a/frontend/app/[locale]/agents/components/agent/CollaborativeAgentDisplay.tsx +++ b/frontend/app/[locale]/agents/components/agent/CollaborativeAgentDisplay.tsx @@ -3,7 +3,7 @@ import { useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { Tag, App } from "antd"; -import { PlusOutlined, CloseOutlined } from "@ant-design/icons"; +import { Plus, X } from "lucide-react"; import { CollaborativeAgentDisplayProps } from "@/types/agentConfig"; @@ -174,7 +174,7 @@ export default function CollaborativeAgentDisplay({ }`} title={isEditingMode ? t("collaborativeAgent.button.add") : ""} > - + {/* Dropdown only renders in editing mode */} {isEditingMode && renderDropdown()} @@ -186,7 +186,7 @@ export default function CollaborativeAgentDisplay({ className="flex items-center h-8" closable={isEditingMode && !isGeneratingAgent} onClose={() => handleRemoveCollaborativeAgent(Number(agent.id))} - closeIcon={} + closeIcon={} style={{ maxWidth: "200px", }} diff --git a/frontend/app/[locale]/agents/components/agent/SubAgentPool.tsx b/frontend/app/[locale]/agents/components/agent/SubAgentPool.tsx index e2fc3ef60..8941cd6c2 100644 --- a/frontend/app/[locale]/agents/components/agent/SubAgentPool.tsx +++ b/frontend/app/[locale]/agents/components/agent/SubAgentPool.tsx @@ -4,8 +4,16 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { Button, Row, Col } from "antd"; -import { ExclamationCircleOutlined } from "@ant-design/icons"; -import { Copy, FileOutput, Network, FileInput, Trash2, Plus, X } from "lucide-react"; +import { + AlertCircle, + Copy, + FileOutput, + Network, + FileInput, + Trash2, + Plus, + X, +} from "lucide-react"; import { ScrollArea } from "@/components/ui/scrollArea"; import { @@ -333,7 +341,7 @@ export default function SubAgentPool({ >
{!isAvailable && ( - + )} {displayName && ( diff --git a/frontend/app/[locale]/agents/components/tool/ToolPool.tsx b/frontend/app/[locale]/agents/components/tool/ToolPool.tsx index 2460a8f07..c80b8969d 100644 --- a/frontend/app/[locale]/agents/components/tool/ToolPool.tsx +++ b/frontend/app/[locale]/agents/components/tool/ToolPool.tsx @@ -4,13 +4,13 @@ import { useState, useEffect, useMemo, useCallback, memo } from "react"; import { useTranslation } from "react-i18next"; import { Button, App, Tabs, Collapse, Tooltip } from "antd"; -import { - SettingOutlined, - LoadingOutlined, - ApiOutlined, - ReloadOutlined, - BulbOutlined, -} from "@ant-design/icons"; +import { + LoaderCircle, + Settings, + RefreshCw, + Lightbulb, + Plug, +} from "lucide-react"; import { TOOL_SOURCE_TYPES } from "@/const/agentConfig"; import log from "@/lib/logger"; @@ -524,7 +524,7 @@ function ToolPool({ }`} style={{ border: "none", padding: "4px" }} > - +
@@ -658,17 +658,23 @@ function ToolPool({ }, }} > - +
@@ -387,7 +394,7 @@ export function ChatStreamFinalMessage({ } transition-all duration-200 shadow-sm`} onClick={handleThumbsDown} > - + diff --git a/frontend/app/[locale]/knowledges/components/document/DocumentStatus.tsx b/frontend/app/[locale]/knowledges/components/document/DocumentStatus.tsx index f1c39af17..20e117000 100644 --- a/frontend/app/[locale]/knowledges/components/document/DocumentStatus.tsx +++ b/frontend/app/[locale]/knowledges/components/document/DocumentStatus.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { Popover, Progress } from "antd"; -import { QuestionCircleOutlined } from "@ant-design/icons"; +import { CircleHelp } from "lucide-react"; import { DOCUMENT_STATUS } from "@/const/knowledgeBase"; import knowledgeBaseService from "@/services/knowledgeBaseService"; import log from "@/lib/logger"; @@ -272,9 +272,9 @@ export const DocumentStatus: React.FC = ({ open={isPopoverOpen} onOpenChange={handlePopoverVisibleChange} > - )} diff --git a/frontend/app/[locale]/knowledges/components/upload/UploadAreaUI.tsx b/frontend/app/[locale]/knowledges/components/upload/UploadAreaUI.tsx index fd656ba83..93982236b 100644 --- a/frontend/app/[locale]/knowledges/components/upload/UploadAreaUI.tsx +++ b/frontend/app/[locale]/knowledges/components/upload/UploadAreaUI.tsx @@ -3,7 +3,8 @@ import { useTranslation } from "react-i18next"; import { NAME_CHECK_STATUS } from "@/const/agentConfig"; import { Upload, Progress } from "antd"; -import { InboxOutlined, WarningFilled } from "@ant-design/icons"; +import { WarningFilled } from "@ant-design/icons"; +import { Inbox } from "lucide-react"; import type { UploadFile, UploadProps } from "antd/es/upload/interface"; const { Dragger } = Upload; @@ -171,7 +172,7 @@ const UploadAreaUI: React.FC = ({ >

- +

{t("knowledgeBase.upload.dragHint")} diff --git a/frontend/app/[locale]/models/components/model/ModelAddDialog.tsx b/frontend/app/[locale]/models/components/model/ModelAddDialog.tsx index 1ce0995cf..f2257e03d 100644 --- a/frontend/app/[locale]/models/components/model/ModelAddDialog.tsx +++ b/frontend/app/[locale]/models/components/model/ModelAddDialog.tsx @@ -2,13 +2,13 @@ import { useMemo, useState, useCallback, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { Modal, Select, Input, Button, Switch, Tooltip, App } from "antd"; +import { InfoCircleFilled } from "@ant-design/icons"; import { - InfoCircleFilled, - LoadingOutlined, - RightOutlined, - DownOutlined, - SettingOutlined, -} from "@ant-design/icons"; + LoaderCircle, + ChevronRight, + ChevronDown, + Settings +} from "lucide-react"; import { useConfig } from "@/hooks/useConfig"; import { configService } from "@/services/configService"; @@ -924,9 +924,9 @@ export const ModelAddDialog = ({ className="flex items-center focus:outline-none" > {showModelList ? ( - + ) : ( - + )} {t("model.dialog.modelList.title")} @@ -962,8 +962,8 @@ export const ModelAddDialog = ({ )} {loadingModelList ? (

-
- + ); })} @@ -915,7 +910,7 @@ export const ModelDeleteDialog = ({ - + ); })} @@ -951,7 +946,7 @@ export const ModelDeleteDialog = ({
diff --git a/frontend/app/[locale]/models/components/modelConfig.tsx b/frontend/app/[locale]/models/components/modelConfig.tsx index 24bf23850..33e89df03 100644 --- a/frontend/app/[locale]/models/components/modelConfig.tsx +++ b/frontend/app/[locale]/models/components/modelConfig.tsx @@ -2,12 +2,12 @@ import { forwardRef, useEffect, useImperativeHandle, useState, useRef, ReactNode import { useTranslation } from 'react-i18next' import { Button, Card, Col, Row, Space, App } from 'antd' -import { - PlusOutlined, - SafetyCertificateOutlined, - SyncOutlined, - EditOutlined, -} from "@ant-design/icons"; +import { + Plus, + ShieldCheck, + RefreshCw, + PenLine +} from "lucide-react"; import { MODEL_TYPES, @@ -848,7 +848,7 @@ export const ModelConfigSection = forwardRef< style={{ width: "100%" }} block > - + {t("modelConfig.button.syncModelEngine")} @@ -863,7 +863,7 @@ export const ModelConfigSection = forwardRef<
- ({ - key: opt.value, - label: opt.label, - })), - onClick: ({ key }) => handleLanguageChange(key as string), - }} - > - - - {languageOptions.find((o) => o.value === currentLanguage)?.label || - currentLanguage} - - - - {/* ModelEngine connectivity status */} -
- -
-