From 5da697034f49bbf2f0bb90c53ad5fc01fbeb0e44 Mon Sep 17 00:00:00 2001 From: "alan.cl" <1165243776@qq.com> Date: Tue, 26 May 2026 13:03:24 +0800 Subject: [PATCH] feat(connector): multi-select connectors in chat composer - Replace single selectedConnector with selectedConnectors array - Send ext_info.connector_ids (array) to backend (Task C contract) - Render one tag per selected connector with individual onClose - Popover stays open after pick; click toggles membership - Badge shows count; tooltip shows comma-joined names - ChatMessage.attachedConnectors[] for replay --- web/locales/en/chat.ts | 1 + web/locales/zh/chat.ts | 1 + .../chat/content/ManusLeftPanel.tsx | 19 ++ web/new-components/connector/index.ts | 2 +- web/new-components/connector/types.ts | 6 + web/pages/index.tsx | 314 +++++++++++++++++- 6 files changed, 340 insertions(+), 3 deletions(-) diff --git a/web/locales/en/chat.ts b/web/locales/en/chat.ts index 33ea51c7e..7589ab2ce 100644 --- a/web/locales/en/chat.ts +++ b/web/locales/en/chat.ts @@ -52,6 +52,7 @@ export const ChatEn = { use_skill: 'Use Skill', use_knowledge: 'Use Knowledge Base', use_database: 'Use Database', + use_connector: 'Select Connector', execution_steps: 'Execution Steps', db_gpt_computer: "DB-GPT's Computer", load_skill: 'Load Skill', diff --git a/web/locales/zh/chat.ts b/web/locales/zh/chat.ts index 97e42829f..41635190d 100644 --- a/web/locales/zh/chat.ts +++ b/web/locales/zh/chat.ts @@ -60,6 +60,7 @@ export const ChatZh: Resources['translation'] = { use_skill: '使用技能', use_knowledge: '使用知识库', use_database: '使用数据库', + use_connector: '选择连接器', execution_steps: '执行步骤', db_gpt_computer: 'DB-GPT 的电脑', load_skill: '加载技能', diff --git a/web/new-components/chat/content/ManusLeftPanel.tsx b/web/new-components/chat/content/ManusLeftPanel.tsx index ed7500306..26f634408 100644 --- a/web/new-components/chat/content/ManusLeftPanel.tsx +++ b/web/new-components/chat/content/ManusLeftPanel.tsx @@ -1,5 +1,6 @@ import MarkdownContext from '@/new-components/common/MarkdownContext'; import { + ApiOutlined, AppstoreOutlined, BarChartOutlined, BookOutlined, @@ -34,6 +35,7 @@ import classNames from 'classnames'; import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import ObservationFormatter from './ObservationFormatter'; +import { AttachedConnector } from '@/new-components/connector/types'; export type StepStatus = 'pending' | 'running' | 'completed' | 'error'; @@ -118,6 +120,7 @@ export interface ManusLeftPanelProps { db_name: string; db_type: string; }; + attachedConnectors?: AttachedConnector[]; createdSkillName?: string; onSkillCardClick?: (skillName: string) => void; onSkillDownload?: (skillName: string) => void; @@ -810,6 +813,7 @@ const ManusLeftPanel: React.FC = ({ attachedKnowledge, attachedSkill, attachedDb, + attachedConnectors, createdSkillName, onSkillCardClick, onSkillDownload, @@ -921,6 +925,21 @@ const ManusLeftPanel: React.FC = ({ )} + {(attachedConnectors ?? []).map(c => ( +
+
+ +
+
+
+ {c.display_name} +
+
+ {c.connector_type} +
+
+
+ ))}
{userQuery}
diff --git a/web/new-components/connector/index.ts b/web/new-components/connector/index.ts index f5ee538f1..73a08d6f5 100644 --- a/web/new-components/connector/index.ts +++ b/web/new-components/connector/index.ts @@ -2,5 +2,5 @@ export { default as ConnectorCard } from './ConnectorCard'; export { default as ConnectorForm } from './ConnectorForm'; export { default as ConfirmDialog } from './ConfirmDialog'; export { useConfirmPolling } from './useConfirmPolling'; -export type { ConnectorCatalogEntry, ConnectorInstance, ConnectorStatus, CreateConnectorRequest } from './types'; +export type { AttachedConnector, ConnectorCatalogEntry, ConnectorInstance, ConnectorStatus, CreateConnectorRequest } from './types'; export type { PendingConfirmation, ConfirmActionRequest } from './types'; diff --git a/web/new-components/connector/types.ts b/web/new-components/connector/types.ts index f150f7835..22b187bd1 100644 --- a/web/new-components/connector/types.ts +++ b/web/new-components/connector/types.ts @@ -1,5 +1,11 @@ export type ConnectorStatus = 'active' | 'error' | 'disconnected'; +export type AttachedConnector = { + id: string; + connector_type: string; + display_name: string; +}; + export interface ConnectorInstance { id: string; connector_type: string; diff --git a/web/pages/index.tsx b/web/pages/index.tsx index 1d76b88be..77fcb41c0 100644 --- a/web/pages/index.tsx +++ b/web/pages/index.tsx @@ -13,9 +13,12 @@ import ManusRightPanel, { PanelView, } from '@/new-components/chat/content/ManusRightPanel'; import { MessagePart, ToolPart, ToolStatus } from '@/new-components/chat/content/OpenCodeSessionTurn'; +import { ConnectorInstance, AttachedConnector } from '@/new-components/connector/types'; +import { useConnectors } from '@/hooks/use-connector-api'; import axios from '@/utils/ctx-axios'; import { sendSpacePostRequest } from '@/utils/request'; import { + ApiOutlined, ArrowUpOutlined, AudioOutlined, BarChartOutlined, @@ -161,6 +164,7 @@ interface ChatMessage { attachedKnowledge?: KnowledgeSpace; attachedSkill?: { name: string; id: string }; attachedDb?: { db_name: string; db_type: string }; + attachedConnectors?: AttachedConnector[]; } interface ExecutionStep { @@ -573,6 +577,11 @@ const Playground: NextPage = () => { const [isDbPanelOpen, setIsDbPanelOpen] = useState(false); const [dbSearchQuery, setDbSearchQuery] = useState(''); + const [isConnectorPanelOpen, setIsConnectorPanelOpen] = useState(false); + const [selectedConnectors, setSelectedConnectors] = useState([]); + const [connectorSearchQuery, setConnectorSearchQuery] = useState(''); + const { connectors: connectorsList } = useConnectors(); + const [selectedStepId, setSelectedStepId] = useState(null); const [rightPanelCollapsed, setRightPanelCollapsed] = useState(false); const [rightPanelView, setRightPanelView] = useState('execution'); @@ -1493,6 +1502,9 @@ const Playground: NextPage = () => { attachedKnowledge: selectedKnowledge ?? undefined, attachedSkill: effectiveSkill ? { name: effectiveSkill.name, id: effectiveSkill.id } : undefined, attachedDb: effectiveDb ? { db_name: effectiveDb.db_name, db_type: effectiveDb.db_type } : undefined, + attachedConnectors: selectedConnectors.length > 0 + ? selectedConnectors.map(c => ({ id: c.id, connector_type: c.connector_type, display_name: c.display_name })) + : undefined, }, { id: responseId, @@ -1543,6 +1555,9 @@ const Playground: NextPage = () => { ...(selectedKnowledge ? { knowledge_space_name: selectedKnowledge.name, knowledge_space_id: selectedKnowledge.id } : {}), + ...(selectedConnectors.length > 0 + ? { connector_ids: selectedConnectors.map(c => c.id) } + : {}), }, }), signal: controller.signal, @@ -2316,6 +2331,7 @@ const Playground: NextPage = () => { attachedKnowledge={round.humanMsg?.attachedKnowledge} attachedSkill={round.humanMsg?.attachedSkill} attachedDb={round.humanMsg?.attachedDb} + attachedConnectors={round.humanMsg?.attachedConnectors} assistantText={roundAssistantText} modelName={round.viewMsg?.model_name || model} stepThoughts={stepThoughts} @@ -2437,6 +2453,20 @@ const Playground: NextPage = () => { {selectedKnowledge.name} )} + {selectedConnectors.length > 0 && ( + <> + {selectedConnectors.map(c => ( + setSelectedConnectors(prev => prev.filter(s => s.id !== c.id))} + className='flex items-center gap-1 bg-violet-50 border-violet-200 text-violet-700 px-3 py-1 rounded-full' + > + {c.display_name} + + ))} + + )} {uploadedFile && ( { icon: , onClick: () => setIsKnowledgeModalOpen(true), }, + { + key: 'connector', + label: t('use_connector'), + icon: , + onClick: () => setIsConnectorPanelOpen(true), + }, ], }} trigger={['click']} @@ -2653,6 +2689,133 @@ const Playground: NextPage = () => { + {/* Connector Selector Button */} + +
+ } + value={connectorSearchQuery} + onChange={e => setConnectorSearchQuery(e.target.value)} + className='rounded-lg' + allowClear + size='small' + /> +
+
+ {(connectorsList || []) + .filter( + (c: ConnectorInstance) => + !connectorSearchQuery || + c.display_name.toLowerCase().includes(connectorSearchQuery.toLowerCase()) || + c.connector_type.toLowerCase().includes(connectorSearchQuery.toLowerCase()), + ) + .map((c: ConnectorInstance) => ( +
{ + setSelectedConnectors(prev => + prev.some(s => s.id === c.id) + ? prev.filter(s => s.id !== c.id) + : [...prev, c] + ); + setConnectorSearchQuery(''); + }} + className={`flex items-start gap-3 px-3 py-2.5 cursor-pointer transition-all hover:bg-gray-50 dark:hover:bg-gray-800 ${ + selectedConnectors.some(s => s.id === c.id) ? 'bg-violet-50 dark:bg-violet-900/20' : '' + }`} + > +
+ +
+
+
+ + {c.display_name} + + + {c.status === 'active' ? '已激活' : c.status} + +
+

+ {c.connector_type} +

+
+ {selectedConnectors.some(s => s.id === c.id) && ( + + )} +
+ ))} + {(connectorsList || []).filter( + (c: ConnectorInstance) => + !connectorSearchQuery || + c.display_name.toLowerCase().includes(connectorSearchQuery.toLowerCase()) || + c.connector_type.toLowerCase().includes(connectorSearchQuery.toLowerCase()), + ).length === 0 && ( +
+ +
+ {connectorSearchQuery ? '未找到匹配的连接器' : '暂无可用连接器'} +
+
+ )} +
+
+ + {(connectorsList || []).length} 个连接器可用 + + +
+ + } + > + c.display_name).join('、')}`}> + + +
+ {/* Separator dot */}
@@ -2838,8 +3001,8 @@ const Playground: NextPage = () => {
{/* White Inner Box - Clean Glass Card */}
- {/* Uploaded File, Database, Knowledge Tags */} - {(uploadedFile || selectedDb || selectedKnowledge) && ( + {/* Uploaded File, Database, Knowledge, Connector Tags */} + {(uploadedFile || selectedDb || selectedKnowledge || selectedConnectors.length > 0) && (
{uploadedFile && ( { {selectedKnowledge.name} )} + {selectedConnectors.length > 0 && ( + <> + {selectedConnectors.map(c => ( + setSelectedConnectors(prev => prev.filter(s => s.id !== c.id))} + className='flex items-center gap-1 bg-violet-50 border-violet-200 text-violet-700 px-3 py-1 rounded-full' + > + {c.display_name} + + ))} + + )}
)} @@ -2930,6 +3107,12 @@ const Playground: NextPage = () => { icon: , onClick: () => setTimeout(() => setIsDbPanelOpen(true), 100), }, + { + key: 'connector', + label: t('use_connector'), + icon: , + onClick: () => setIsConnectorPanelOpen(true), + }, ], }} trigger={['click']} @@ -3079,6 +3262,133 @@ const Playground: NextPage = () => { + {/* Connector Selector Button */} + +
+ } + value={connectorSearchQuery} + onChange={e => setConnectorSearchQuery(e.target.value)} + className='rounded-lg' + allowClear + size='small' + /> +
+
+ {(connectorsList || []) + .filter( + (c: ConnectorInstance) => + !connectorSearchQuery || + c.display_name.toLowerCase().includes(connectorSearchQuery.toLowerCase()) || + c.connector_type.toLowerCase().includes(connectorSearchQuery.toLowerCase()), + ) + .map((c: ConnectorInstance) => ( +
{ + setSelectedConnectors(prev => + prev.some(s => s.id === c.id) + ? prev.filter(s => s.id !== c.id) + : [...prev, c] + ); + setConnectorSearchQuery(''); + }} + className={`flex items-start gap-3 px-3 py-2.5 cursor-pointer transition-all hover:bg-gray-50 dark:hover:bg-gray-800 ${ + selectedConnectors.some(s => s.id === c.id) ? 'bg-violet-50 dark:bg-violet-900/20' : '' + }`} + > +
+ +
+
+
+ + {c.display_name} + + + {c.status === 'active' ? '已激活' : c.status} + +
+

+ {c.connector_type} +

+
+ {selectedConnectors.some(s => s.id === c.id) && ( + + )} +
+ ))} + {(connectorsList || []).filter( + (c: ConnectorInstance) => + !connectorSearchQuery || + c.display_name.toLowerCase().includes(connectorSearchQuery.toLowerCase()) || + c.connector_type.toLowerCase().includes(connectorSearchQuery.toLowerCase()), + ).length === 0 && ( +
+ +
+ {connectorSearchQuery ? '未找到匹配的连接器' : '暂无可用连接器'} +
+
+ )} +
+
+ + {(connectorsList || []).length} 个连接器可用 + + +
+
+ } + > + c.display_name).join('、')}`}> + + + + {/* Database Selector Popover - Blue themed */}