diff --git a/frontend/src/components/AIChatPanel.message-boundary.test.tsx b/frontend/src/components/AIChatPanel.message-boundary.test.tsx index 3144b13..13b66d6 100644 --- a/frontend/src/components/AIChatPanel.message-boundary.test.tsx +++ b/frontend/src/components/AIChatPanel.message-boundary.test.tsx @@ -37,6 +37,8 @@ describe('AIChatPanel message render isolation', () => { it('keeps the v2 history mode sorted by the latest updated session first', () => { expect(source).toContain('const orderedAISessions = useMemo('); expect(source).toContain('right.updatedAt - left.updatedAt'); - expect(source).toContain('const sessions = orderedAISessions.slice(0, 8);'); + expect(source).toContain('const panelHistorySessions = useMemo('); + expect(source).toContain('orderedAISessions.slice(0, 8)'); + expect(source).toContain('sessions={panelHistorySessions}'); }); }); diff --git a/frontend/src/components/AIChatPanel.tsx b/frontend/src/components/AIChatPanel.tsx index a54b22c..b835706 100644 --- a/frontend/src/components/AIChatPanel.tsx +++ b/frontend/src/components/AIChatPanel.tsx @@ -12,7 +12,7 @@ import type { JVMAIPlanContext, JVMDiagnosticPlanContext, } from '../types'; -import { DatabaseOutlined, DownOutlined, HistoryOutlined, TableOutlined, WarningOutlined } from '@ant-design/icons'; +import { DownOutlined } from '@ant-design/icons'; import './AIChatPanel.css'; import { AIChatHeader } from './ai/AIChatHeader'; @@ -20,6 +20,7 @@ import { AIChatWelcome } from './ai/AIChatWelcome'; import { AIMessageBubble } from './ai/AIMessageBubble'; import { AIChatInput } from './ai/AIChatInput'; import { AIHistoryDrawer } from './ai/AIHistoryDrawer'; +import AIChatPanelModeContent, { type AIChatInsightItem } from './ai/AIChatPanelModeContent'; import type { AIComposerNotice } from '../utils/aiComposerNotice'; import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig'; import { @@ -1664,7 +1665,7 @@ SELECT * FROM users WHERE status = 1; const ck = activeContext?.connectionId ? `${activeContext.connectionId}:${activeContext.dbName || ''}` : 'default'; return (aiContexts[ck] || []).map(c => `${c.dbName}.${c.tableName}`); }, [activeContext?.connectionId, activeContext?.dbName, aiContexts]); - const aiInsights = useMemo(() => { + const aiInsights = useMemo(() => { const recentLogs = sqlLogs.slice(0, 24); const slowest = recentLogs .filter((log) => log.status === 'success') @@ -1697,30 +1698,10 @@ SELECT * FROM users WHERE status = 1; }, ]; }, [contextTableNames, sqlLogs]); - - const renderPanelHistoryList = () => { - const sessions = orderedAISessions.slice(0, 8); - if (sessions.length === 0) { - return
暂无历史会话
; - } - return sessions.map((session) => ( - - )); - }; + const panelHistorySessions = useMemo( + () => orderedAISessions.slice(0, 8), + [orderedAISessions], + ); const effectivePanelMode = isV2Ui ? activePanelMode : 'chat'; return ( @@ -1827,28 +1808,17 @@ SELECT * FROM users WHERE status = 1; ) )} - {effectivePanelMode === 'insights' && ( -
- {aiInsights.map((item) => ( -
- - {item.tone === 'warn' ? : item.tone === 'accent' ? : } - -
- {item.title} -

{item.body}

-
-
- ))} -
- )} - - {effectivePanelMode === 'history' && ( -
- {renderPanelHistoryList()} -
- )} - + { + setAIActiveSessionId(sessionId); + setActivePanelMode('chat'); + }} + /> +
diff --git a/frontend/src/components/ai/AIChatPanelModeContent.test.tsx b/frontend/src/components/ai/AIChatPanelModeContent.test.tsx new file mode 100644 index 0000000..1fab95d --- /dev/null +++ b/frontend/src/components/ai/AIChatPanelModeContent.test.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { describe, expect, it } from 'vitest'; + +import AIChatPanelModeContent from './AIChatPanelModeContent'; + +describe('AIChatPanelModeContent', () => { + it('renders insight cards for the automatic insights mode', () => { + const markup = renderToStaticMarkup( + {}} + />, + ); + + expect(markup).toContain('gn-v2-ai-insight-card tone-info'); + expect(markup).toContain('已关联 3 张表'); + expect(markup).toContain('2 条最近查询失败'); + expect(markup).toContain('Unknown column foo'); + }); + + it('renders an empty state when there is no inline history session', () => { + const markup = renderToStaticMarkup( + {}} + />, + ); + + expect(markup).toContain('gn-v2-ai-empty-note'); + expect(markup).toContain('暂无历史会话'); + }); + + it('marks the active inline history session', () => { + const markup = renderToStaticMarkup( + {}} + />, + ); + + expect(markup).toContain('gn-v2-ai-history-card is-active'); + expect(markup).toContain('当前会话'); + expect(markup).toContain('旧会话'); + }); +}); diff --git a/frontend/src/components/ai/AIChatPanelModeContent.tsx b/frontend/src/components/ai/AIChatPanelModeContent.tsx new file mode 100644 index 0000000..7b02653 --- /dev/null +++ b/frontend/src/components/ai/AIChatPanelModeContent.tsx @@ -0,0 +1,98 @@ +import React from 'react'; +import { DatabaseOutlined, HistoryOutlined, TableOutlined, WarningOutlined } from '@ant-design/icons'; + +export type AIChatPanelMode = 'chat' | 'insights' | 'history'; + +export interface AIChatInsightItem { + tone: 'info' | 'accent' | 'warn'; + title: string; + body: string; +} + +export interface AIChatInlineHistorySession { + id: string; + title: string; + updatedAt: number; +} + +interface AIChatPanelModeContentProps { + mode: AIChatPanelMode; + insights: AIChatInsightItem[]; + sessions: AIChatInlineHistorySession[]; + activeSessionId: string; + onSelectSession: (sessionId: string) => void; +} + +const renderInsightIcon = (tone: AIChatInsightItem['tone']) => { + if (tone === 'warn') { + return ; + } + if (tone === 'accent') { + return ; + } + return ; +}; + +const AIChatPanelModeContent: React.FC = ({ + mode, + insights, + sessions, + activeSessionId, + onSelectSession, +}) => { + if (mode === 'insights') { + return ( +
+ {insights.map((item) => ( +
+ {renderInsightIcon(item.tone)} +
+ {item.title} +

{item.body}

+
+
+ ))} +
+ ); + } + + if (mode === 'history') { + if (sessions.length === 0) { + return ( +
+
暂无历史会话
+
+ ); + } + + return ( +
+ {sessions.map((session) => ( + + ))} +
+ ); + } + + return null; +}; + +export default AIChatPanelModeContent;