mirror of
https://github.com/csunny/DB-GPT.git
synced 2026-07-16 17:15:22 +00:00
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
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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: '加载技能',
|
||||
|
||||
@@ -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<ManusLeftPanelProps> = ({
|
||||
attachedKnowledge,
|
||||
attachedSkill,
|
||||
attachedDb,
|
||||
attachedConnectors,
|
||||
createdSkillName,
|
||||
onSkillCardClick,
|
||||
onSkillDownload,
|
||||
@@ -921,6 +925,21 @@ const ManusLeftPanel: React.FC<ManusLeftPanelProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(attachedConnectors ?? []).map(c => (
|
||||
<div key={c.id} className='flex items-center gap-2.5 px-3.5 py-2.5 rounded-xl border border-gray-200 dark:border-gray-700/60 bg-white dark:bg-[#1a1b1e] shadow-sm'>
|
||||
<div className='w-8 h-8 rounded-lg bg-violet-50 dark:bg-violet-900/30 flex items-center justify-center flex-shrink-0'>
|
||||
<ApiOutlined className='text-violet-500 text-base' />
|
||||
</div>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<div className='text-sm font-medium text-gray-800 dark:text-gray-200 truncate'>
|
||||
{c.display_name}
|
||||
</div>
|
||||
<div className='text-[11px] text-gray-400 dark:text-gray-500'>
|
||||
{c.connector_type}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className='rounded-2xl bg-gray-100 dark:bg-[#2a2b2f] px-4 py-3 text-sm text-gray-800 dark:text-gray-200 leading-relaxed'>
|
||||
{userQuery}
|
||||
</div>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<ConnectorInstance[]>([]);
|
||||
const [connectorSearchQuery, setConnectorSearchQuery] = useState('');
|
||||
const { connectors: connectorsList } = useConnectors();
|
||||
|
||||
const [selectedStepId, setSelectedStepId] = useState<string | null>(null);
|
||||
const [rightPanelCollapsed, setRightPanelCollapsed] = useState(false);
|
||||
const [rightPanelView, setRightPanelView] = useState<PanelView>('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 = () => {
|
||||
<BookOutlined /> <span className='font-medium ml-1'>{selectedKnowledge.name}</span>
|
||||
</Tag>
|
||||
)}
|
||||
{selectedConnectors.length > 0 && (
|
||||
<>
|
||||
{selectedConnectors.map(c => (
|
||||
<Tag
|
||||
key={c.id}
|
||||
closable
|
||||
onClose={() => 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'
|
||||
>
|
||||
<ApiOutlined /> <span className='font-medium ml-1'>{c.display_name}</span>
|
||||
</Tag>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{uploadedFile && (
|
||||
<Tag
|
||||
closable
|
||||
@@ -2504,6 +2534,12 @@ const Playground: NextPage = () => {
|
||||
icon: <BookOutlined />,
|
||||
onClick: () => setIsKnowledgeModalOpen(true),
|
||||
},
|
||||
{
|
||||
key: 'connector',
|
||||
label: t('use_connector'),
|
||||
icon: <ApiOutlined />,
|
||||
onClick: () => setIsConnectorPanelOpen(true),
|
||||
},
|
||||
],
|
||||
}}
|
||||
trigger={['click']}
|
||||
@@ -2653,6 +2689,133 @@ const Playground: NextPage = () => {
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
|
||||
{/* Connector Selector Button */}
|
||||
<Popover
|
||||
trigger='click'
|
||||
placement='topLeft'
|
||||
open={isConnectorPanelOpen}
|
||||
onOpenChange={setIsConnectorPanelOpen}
|
||||
overlayClassName='manus-skill-menu'
|
||||
overlayInnerStyle={{ padding: 0, borderRadius: 12 }}
|
||||
content={
|
||||
<div className='w-[320px] bg-white dark:bg-[#2c2d31] rounded-xl shadow-xl overflow-hidden'>
|
||||
<div className='p-3 border-b border-gray-100 dark:border-gray-700'>
|
||||
<Input
|
||||
placeholder='搜索连接器...'
|
||||
prefix={<SearchOutlined className='text-gray-400' />}
|
||||
value={connectorSearchQuery}
|
||||
onChange={e => setConnectorSearchQuery(e.target.value)}
|
||||
className='rounded-lg'
|
||||
allowClear
|
||||
size='small'
|
||||
/>
|
||||
</div>
|
||||
<div className='max-h-[300px] overflow-y-auto'>
|
||||
{(connectorsList || [])
|
||||
.filter(
|
||||
(c: ConnectorInstance) =>
|
||||
!connectorSearchQuery ||
|
||||
c.display_name.toLowerCase().includes(connectorSearchQuery.toLowerCase()) ||
|
||||
c.connector_type.toLowerCase().includes(connectorSearchQuery.toLowerCase()),
|
||||
)
|
||||
.map((c: ConnectorInstance) => (
|
||||
<div
|
||||
key={c.id}
|
||||
onClick={() => {
|
||||
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' : ''
|
||||
}`}
|
||||
>
|
||||
<div className='flex-shrink-0 w-7 h-7 rounded-lg bg-gradient-to-br from-violet-500 to-indigo-500 flex items-center justify-center text-white text-xs'>
|
||||
<ApiOutlined />
|
||||
</div>
|
||||
<div className='flex-1 min-w-0'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<span className='font-medium text-sm text-gray-800 dark:text-gray-200'>
|
||||
{c.display_name}
|
||||
</span>
|
||||
<span
|
||||
className={`text-[10px] px-1.5 py-0.5 rounded ${
|
||||
c.status === 'active'
|
||||
? 'bg-green-100 text-green-600 dark:bg-green-900/30 dark:text-green-400'
|
||||
: 'bg-gray-100 text-gray-500 dark:bg-gray-700 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{c.status === 'active' ? '已激活' : c.status}
|
||||
</span>
|
||||
</div>
|
||||
<p className='text-xs text-gray-500 dark:text-gray-400 mt-0.5'>
|
||||
{c.connector_type}
|
||||
</p>
|
||||
</div>
|
||||
{selectedConnectors.some(s => s.id === c.id) && (
|
||||
<CheckCircleFilled className='text-violet-500 flex-shrink-0 text-sm' />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{(connectorsList || []).filter(
|
||||
(c: ConnectorInstance) =>
|
||||
!connectorSearchQuery ||
|
||||
c.display_name.toLowerCase().includes(connectorSearchQuery.toLowerCase()) ||
|
||||
c.connector_type.toLowerCase().includes(connectorSearchQuery.toLowerCase()),
|
||||
).length === 0 && (
|
||||
<div className='text-center py-8 text-gray-400'>
|
||||
<ApiOutlined className='text-2xl mb-2 opacity-50' />
|
||||
<div className='text-xs'>
|
||||
{connectorSearchQuery ? '未找到匹配的连接器' : '暂无可用连接器'}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='border-t border-gray-100 dark:border-gray-700 px-3 py-2 flex items-center justify-between bg-gray-50/50 dark:bg-gray-900/50'>
|
||||
<span className='text-[10px] text-gray-400'>
|
||||
{(connectorsList || []).length} 个连接器可用
|
||||
</span>
|
||||
<Button
|
||||
type='link'
|
||||
size='small'
|
||||
onClick={() => {
|
||||
router.push('/construct/connectors');
|
||||
setIsConnectorPanelOpen(false);
|
||||
}}
|
||||
className='text-[10px] p-0 h-auto'
|
||||
>
|
||||
管理连接器 →
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Tooltip title={selectedConnectors.length === 0 ? '选择连接器' : selectedConnectors.length === 1 ? `连接器: ${selectedConnectors[0].display_name}` : `已选 ${selectedConnectors.length} 个连接器: ${selectedConnectors.map(c => c.display_name).join('、')}`}>
|
||||
<Button
|
||||
type='text'
|
||||
shape='circle'
|
||||
size='small'
|
||||
className={`relative flex items-center justify-center flex-shrink-0 transition-all ${
|
||||
selectedConnectors.length > 0
|
||||
? 'bg-gradient-to-br from-[#8b5cf6] to-[#6366f1] text-white border border-transparent shadow-[0_2px_4px_rgba(139,92,246,0.3),inset_0_1px_0_rgba(255,255,255,0.3)] hover:-translate-y-[0.5px] hover:shadow-[0_4px_8px_rgba(139,92,246,0.4),inset_0_1px_0_rgba(255,255,255,0.3)]'
|
||||
: 'text-gray-500 hover:text-violet-600 bg-gradient-to-b from-white to-gray-50 dark:from-[#2a2b2f] dark:to-[#1e1f24] dark:text-gray-300 border border-gray-200/80 dark:border-white/10 shadow-[0_1px_2px_rgba(0,0,0,0.05),inset_0_1px_0_rgba(255,255,255,1)] dark:shadow-[0_1px_2px_rgba(0,0,0,0.2),inset_0_1px_0_rgba(255,255,255,0.05)] hover:-translate-y-[0.5px] hover:shadow-[0_2px_4px_rgba(0,0,0,0.06),inset_0_1px_0_rgba(255,255,255,1)] dark:hover:border-white/20'
|
||||
}`}
|
||||
>
|
||||
<div className='relative'>
|
||||
<ApiOutlined className={selectedConnectors.length > 0 ? 'text-white' : ''} />
|
||||
{selectedConnectors.length > 0 && (
|
||||
<span className='absolute -top-1.5 -right-1.5 bg-white text-violet-600 text-[8px] rounded-full w-3.5 h-3.5 flex items-center justify-center font-bold shadow-sm ring-1 ring-violet-500/30'>
|
||||
{selectedConnectors.length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
|
||||
{/* Separator dot */}
|
||||
<div className='w-px h-4 bg-gray-200 dark:bg-gray-700 mx-0.5' />
|
||||
|
||||
@@ -2838,8 +3001,8 @@ const Playground: NextPage = () => {
|
||||
<div className='w-full relative transition-all duration-500 rounded-[28px] shadow-[0_16px_48px_rgba(0,0,0,0.12),0_6px_20px_rgba(0,0,0,0.08)] hover:shadow-[0_24px_64px_rgba(0,0,0,0.2),0_12px_32px_rgba(0,0,0,0.1)] dark:shadow-[0_16px_48px_rgba(0,0,0,0.4)] dark:hover:shadow-[0_24px_64px_rgba(0,0,0,0.5)]'>
|
||||
{/* White Inner Box - Clean Glass Card */}
|
||||
<div className='bg-white/95 backdrop-blur-md dark:bg-[#1e1f24]/95 rounded-[28px] border border-gray-100 dark:border-[#33353b] shadow-[inset_0_1px_0_rgba(255,255,255,1)] dark:shadow-[inset_0_1px_0_rgba(255,255,255,0.05)] p-5 relative z-10'>
|
||||
{/* Uploaded File, Database, Knowledge Tags */}
|
||||
{(uploadedFile || selectedDb || selectedKnowledge) && (
|
||||
{/* Uploaded File, Database, Knowledge, Connector Tags */}
|
||||
{(uploadedFile || selectedDb || selectedKnowledge || selectedConnectors.length > 0) && (
|
||||
<div className='flex flex-wrap gap-2 mb-2'>
|
||||
{uploadedFile && (
|
||||
<Tag
|
||||
@@ -2869,6 +3032,20 @@ const Playground: NextPage = () => {
|
||||
<BookOutlined /> <span className='font-medium ml-1'>{selectedKnowledge.name}</span>
|
||||
</Tag>
|
||||
)}
|
||||
{selectedConnectors.length > 0 && (
|
||||
<>
|
||||
{selectedConnectors.map(c => (
|
||||
<Tag
|
||||
key={c.id}
|
||||
closable
|
||||
onClose={() => 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'
|
||||
>
|
||||
<ApiOutlined /> <span className='font-medium ml-1'>{c.display_name}</span>
|
||||
</Tag>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -2930,6 +3107,12 @@ const Playground: NextPage = () => {
|
||||
icon: <DatabaseOutlined />,
|
||||
onClick: () => setTimeout(() => setIsDbPanelOpen(true), 100),
|
||||
},
|
||||
{
|
||||
key: 'connector',
|
||||
label: t('use_connector'),
|
||||
icon: <ApiOutlined />,
|
||||
onClick: () => setIsConnectorPanelOpen(true),
|
||||
},
|
||||
],
|
||||
}}
|
||||
trigger={['click']}
|
||||
@@ -3079,6 +3262,133 @@ const Playground: NextPage = () => {
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
|
||||
{/* Connector Selector Button */}
|
||||
<Popover
|
||||
trigger='click'
|
||||
placement='topLeft'
|
||||
open={isConnectorPanelOpen}
|
||||
onOpenChange={setIsConnectorPanelOpen}
|
||||
overlayClassName='manus-skill-menu'
|
||||
overlayInnerStyle={{ padding: 0, borderRadius: 12 }}
|
||||
content={
|
||||
<div className='w-[320px] bg-white dark:bg-[#2c2d31] rounded-xl shadow-xl overflow-hidden'>
|
||||
<div className='p-3 border-b border-gray-100 dark:border-gray-700'>
|
||||
<Input
|
||||
placeholder='搜索连接器...'
|
||||
prefix={<SearchOutlined className='text-gray-400' />}
|
||||
value={connectorSearchQuery}
|
||||
onChange={e => setConnectorSearchQuery(e.target.value)}
|
||||
className='rounded-lg'
|
||||
allowClear
|
||||
size='small'
|
||||
/>
|
||||
</div>
|
||||
<div className='max-h-[300px] overflow-y-auto'>
|
||||
{(connectorsList || [])
|
||||
.filter(
|
||||
(c: ConnectorInstance) =>
|
||||
!connectorSearchQuery ||
|
||||
c.display_name.toLowerCase().includes(connectorSearchQuery.toLowerCase()) ||
|
||||
c.connector_type.toLowerCase().includes(connectorSearchQuery.toLowerCase()),
|
||||
)
|
||||
.map((c: ConnectorInstance) => (
|
||||
<div
|
||||
key={c.id}
|
||||
onClick={() => {
|
||||
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' : ''
|
||||
}`}
|
||||
>
|
||||
<div className='flex-shrink-0 w-7 h-7 rounded-lg bg-gradient-to-br from-violet-500 to-indigo-500 flex items-center justify-center text-white text-xs'>
|
||||
<ApiOutlined />
|
||||
</div>
|
||||
<div className='flex-1 min-w-0'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<span className='font-medium text-sm text-gray-800 dark:text-gray-200'>
|
||||
{c.display_name}
|
||||
</span>
|
||||
<span
|
||||
className={`text-[10px] px-1.5 py-0.5 rounded ${
|
||||
c.status === 'active'
|
||||
? 'bg-green-100 text-green-600 dark:bg-green-900/30 dark:text-green-400'
|
||||
: 'bg-gray-100 text-gray-500 dark:bg-gray-700 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{c.status === 'active' ? '已激活' : c.status}
|
||||
</span>
|
||||
</div>
|
||||
<p className='text-xs text-gray-500 dark:text-gray-400 mt-0.5'>
|
||||
{c.connector_type}
|
||||
</p>
|
||||
</div>
|
||||
{selectedConnectors.some(s => s.id === c.id) && (
|
||||
<CheckCircleFilled className='text-violet-500 flex-shrink-0 text-sm' />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{(connectorsList || []).filter(
|
||||
(c: ConnectorInstance) =>
|
||||
!connectorSearchQuery ||
|
||||
c.display_name.toLowerCase().includes(connectorSearchQuery.toLowerCase()) ||
|
||||
c.connector_type.toLowerCase().includes(connectorSearchQuery.toLowerCase()),
|
||||
).length === 0 && (
|
||||
<div className='text-center py-8 text-gray-400'>
|
||||
<ApiOutlined className='text-2xl mb-2 opacity-50' />
|
||||
<div className='text-xs'>
|
||||
{connectorSearchQuery ? '未找到匹配的连接器' : '暂无可用连接器'}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='border-t border-gray-100 dark:border-gray-700 px-3 py-2 flex items-center justify-between bg-gray-50/50 dark:bg-gray-900/50'>
|
||||
<span className='text-[10px] text-gray-400'>
|
||||
{(connectorsList || []).length} 个连接器可用
|
||||
</span>
|
||||
<Button
|
||||
type='link'
|
||||
size='small'
|
||||
onClick={() => {
|
||||
router.push('/construct/connectors');
|
||||
setIsConnectorPanelOpen(false);
|
||||
}}
|
||||
className='text-[10px] p-0 h-auto'
|
||||
>
|
||||
管理连接器 →
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Tooltip title={selectedConnectors.length === 0 ? '选择连接器' : selectedConnectors.length === 1 ? `连接器: ${selectedConnectors[0].display_name}` : `已选 ${selectedConnectors.length} 个连接器: ${selectedConnectors.map(c => c.display_name).join('、')}`}>
|
||||
<Button
|
||||
type='text'
|
||||
shape='circle'
|
||||
size='small'
|
||||
className={`relative flex items-center justify-center flex-shrink-0 transition-all ${
|
||||
selectedConnectors.length > 0
|
||||
? 'bg-gradient-to-br from-[#8b5cf6] to-[#6366f1] text-white border border-transparent shadow-[0_2px_4px_rgba(139,92,246,0.3),inset_0_1px_0_rgba(255,255,255,0.3)] hover:-translate-y-[0.5px] hover:shadow-[0_4px_8px_rgba(139,92,246,0.4),inset_0_1px_0_rgba(255,255,255,0.3)]'
|
||||
: 'text-gray-500 hover:text-violet-600 bg-gradient-to-b from-white to-gray-50 dark:from-[#2a2b2f] dark:to-[#1e1f24] dark:text-gray-300 border border-gray-200/80 dark:border-white/10 shadow-[0_1px_2px_rgba(0,0,0,0.05),inset_0_1px_0_rgba(255,255,255,1)] dark:shadow-[0_1px_2px_rgba(0,0,0,0.2),inset_0_1px_0_rgba(255,255,255,0.05)] hover:-translate-y-[0.5px] hover:shadow-[0_2px_4px_rgba(0,0,0,0.06),inset_0_1px_0_rgba(255,255,255,1)] dark:hover:border-white/20'
|
||||
}`}
|
||||
>
|
||||
<div className='relative'>
|
||||
<ApiOutlined className={selectedConnectors.length > 0 ? 'text-white' : ''} />
|
||||
{selectedConnectors.length > 0 && (
|
||||
<span className='absolute -top-1.5 -right-1.5 bg-white text-violet-600 text-[8px] rounded-full w-3.5 h-3.5 flex items-center justify-center font-bold shadow-sm ring-1 ring-violet-500/30'>
|
||||
{selectedConnectors.length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
|
||||
{/* Database Selector Popover - Blue themed */}
|
||||
<Popover
|
||||
trigger='click'
|
||||
|
||||
Reference in New Issue
Block a user