Compare commits
8 Commits
feature_20
...
d37c974d23
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d37c974d23 | ||
|
|
11821d8256 | ||
|
|
2037e65ee4 | ||
|
|
fe22d48006 | ||
|
|
6a82a07e92 | ||
|
|
04248e5a99 | ||
|
|
af54d8e070 | ||
|
|
a3cb5e928e |
@@ -1,5 +1,15 @@
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { svgs } from "./svgs";
|
import { svgs } from "./svgs";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface ButtonProps {
|
||||||
|
className?: string;
|
||||||
|
href?: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
px?: string;
|
||||||
|
white?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const Button = ({
|
const Button = ({
|
||||||
className,
|
className,
|
||||||
@@ -8,7 +18,7 @@ const Button = ({
|
|||||||
children,
|
children,
|
||||||
px,
|
px,
|
||||||
white,
|
white,
|
||||||
}) => {
|
}: ButtonProps) => {
|
||||||
const classes = `button relative inline-flex items-center justify-center h-11 ${
|
const classes = `button relative inline-flex items-center justify-center h-11 ${
|
||||||
px || "px-7"
|
px || "px-7"
|
||||||
} ${white ? "text-n-8" : "text-n-1"} transition-colors hover:text-color-1 ${
|
} ${white ? "text-n-8" : "text-n-1"} transition-colors hover:text-color-1 ${
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
const Image = ({ className, ...props }) => {
|
interface ImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Image = ({ className, ...props }: ImageProps) => {
|
||||||
const [loaded, setLoaded] = useState(false);
|
const [loaded, setLoaded] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface SectionProps {
|
||||||
|
className?: string;
|
||||||
|
crosses?: boolean;
|
||||||
|
crossesOffset?: string;
|
||||||
|
customPaddings?: boolean;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
const Section = ({
|
const Section = ({
|
||||||
className,
|
className,
|
||||||
crosses,
|
crosses,
|
||||||
crossesOffset,
|
crossesOffset,
|
||||||
customPaddings,
|
customPaddings,
|
||||||
children,
|
children,
|
||||||
}) => (
|
}: SectionProps) => (
|
||||||
<div
|
<div
|
||||||
className={`relative ${
|
className={`relative ${
|
||||||
customPaddings ||
|
customPaddings ||
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ const KLineChartModal: React.FC<KLineChartModalProps> = ({
|
|||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.debug('KLineChartModal', 'loadData', '开始加载K线数据', {
|
logger.debug('KLineChartModal', '开始加载K线数据 (loadData)', {
|
||||||
stockCode: stock.stock_code,
|
stockCode: stock.stock_code,
|
||||||
eventTime,
|
eventTime,
|
||||||
});
|
});
|
||||||
@@ -91,7 +91,7 @@ const KLineChartModal: React.FC<KLineChartModalProps> = ({
|
|||||||
|
|
||||||
console.log('[KLineChartModal] 数据条数:', response.data.length);
|
console.log('[KLineChartModal] 数据条数:', response.data.length);
|
||||||
setData(response.data);
|
setData(response.data);
|
||||||
logger.info('KLineChartModal', 'loadData', 'K线数据加载成功', {
|
logger.info('KLineChartModal', 'K线数据加载成功 (loadData)', {
|
||||||
dataCount: response.data.length,
|
dataCount: response.data.length,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ const StockChartKLineModal: React.FC<StockChartKLineModalProps> = ({
|
|||||||
const newType = e.target.value as ChartType;
|
const newType = e.target.value as ChartType;
|
||||||
setChartType(newType);
|
setChartType(newType);
|
||||||
|
|
||||||
logger.debug('StockChartKLineModal', 'handleChartTypeChange', '切换图表类型', {
|
logger.debug('StockChartKLineModal', '切换图表类型 (handleChartTypeChange)', {
|
||||||
newType,
|
newType,
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
@@ -131,7 +131,7 @@ const StockChartKLineModal: React.FC<StockChartKLineModalProps> = ({
|
|||||||
// 然后创建新的指标
|
// 然后创建新的指标
|
||||||
createSubIndicators(chart, values);
|
createSubIndicators(chart, values);
|
||||||
|
|
||||||
logger.debug('StockChartKLineModal', 'handleIndicatorChange', '切换副图指标', {
|
logger.debug('StockChartKLineModal', '切换副图指标 (handleIndicatorChange)', {
|
||||||
indicators: values,
|
indicators: values,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -143,7 +143,6 @@ const StockChartKLineModal: React.FC<StockChartKLineModalProps> = ({
|
|||||||
*/
|
*/
|
||||||
const handleRefresh = useCallback(() => {
|
const handleRefresh = useCallback(() => {
|
||||||
loadData();
|
loadData();
|
||||||
logger.debug('StockChartKLineModal', 'handleRefresh', '刷新数据');
|
|
||||||
}, [loadData]);
|
}, [loadData]);
|
||||||
|
|
||||||
// ==================== 计算属性 ====================
|
// ==================== 计算属性 ====================
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ const TimelineChartModal: React.FC<TimelineChartModalProps> = ({
|
|||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.debug('TimelineChartModal', 'loadData', '开始加载分时图数据', {
|
logger.debug('TimelineChartModal', '开始加载分时图数据 (loadData)', {
|
||||||
stockCode: stock.stock_code,
|
stockCode: stock.stock_code,
|
||||||
eventTime,
|
eventTime,
|
||||||
});
|
});
|
||||||
@@ -95,7 +95,7 @@ const TimelineChartModal: React.FC<TimelineChartModalProps> = ({
|
|||||||
|
|
||||||
console.log('[TimelineChartModal] 数据条数:', response.data.length);
|
console.log('[TimelineChartModal] 数据条数:', response.data.length);
|
||||||
setData(response.data);
|
setData(response.data);
|
||||||
logger.info('TimelineChartModal', 'loadData', '分时图数据加载成功', {
|
logger.info('TimelineChartModal', '分时图数据加载成功 (loadData)', {
|
||||||
dataCount: response.data.length,
|
dataCount: response.data.length,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export const useEventMarker = (
|
|||||||
const createMarker = useCallback(
|
const createMarker = useCallback(
|
||||||
(time: string, label: string, color?: string) => {
|
(time: string, label: string, color?: string) => {
|
||||||
if (!chart || !data || data.length === 0) {
|
if (!chart || !data || data.length === 0) {
|
||||||
logger.warn('useEventMarker', 'createMarker', '图表或数据未准备好', {
|
logger.warn('useEventMarker', '图表或数据未准备好 (createMarker)', {
|
||||||
hasChart: !!chart,
|
hasChart: !!chart,
|
||||||
dataLength: data?.length || 0,
|
dataLength: data?.length || 0,
|
||||||
});
|
});
|
||||||
@@ -93,7 +93,7 @@ export const useEventMarker = (
|
|||||||
const overlay = createEventMarkerOverlay(eventMarker, data);
|
const overlay = createEventMarkerOverlay(eventMarker, data);
|
||||||
|
|
||||||
if (!overlay) {
|
if (!overlay) {
|
||||||
logger.warn('useEventMarker', 'createMarker', 'Overlay 创建失败', {
|
logger.warn('useEventMarker', 'Overlay 创建失败 (createMarker)', {
|
||||||
eventMarker,
|
eventMarker,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -103,7 +103,7 @@ export const useEventMarker = (
|
|||||||
const id = chart.createOverlay(overlay);
|
const id = chart.createOverlay(overlay);
|
||||||
|
|
||||||
if (!id || (Array.isArray(id) && id.length === 0)) {
|
if (!id || (Array.isArray(id) && id.length === 0)) {
|
||||||
logger.warn('useEventMarker', 'createMarker', '标记添加失败', {
|
logger.warn('useEventMarker', '标记添加失败 (createMarker)', {
|
||||||
overlay,
|
overlay,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -119,12 +119,12 @@ export const useEventMarker = (
|
|||||||
const actualHighlightId = Array.isArray(highlightResult) ? highlightResult[0] : highlightResult;
|
const actualHighlightId = Array.isArray(highlightResult) ? highlightResult[0] : highlightResult;
|
||||||
setHighlightId(actualHighlightId as string);
|
setHighlightId(actualHighlightId as string);
|
||||||
|
|
||||||
logger.info('useEventMarker', 'createMarker', '事件高亮背景创建成功', {
|
logger.info('useEventMarker', '事件高亮背景创建成功 (createMarker)', {
|
||||||
highlightId: actualHighlightId,
|
highlightId: actualHighlightId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('useEventMarker', 'createMarker', '事件标记创建成功', {
|
logger.info('useEventMarker', '事件标记创建成功 (createMarker)', {
|
||||||
markerId: actualId,
|
markerId: actualId,
|
||||||
label,
|
label,
|
||||||
time,
|
time,
|
||||||
@@ -150,17 +150,17 @@ export const useEventMarker = (
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (markerId) {
|
if (markerId) {
|
||||||
chart.removeOverlay(markerId);
|
chart.removeOverlay({ id: markerId } as any);
|
||||||
}
|
}
|
||||||
if (highlightId) {
|
if (highlightId) {
|
||||||
chart.removeOverlay(highlightId);
|
chart.removeOverlay({ id: highlightId } as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
setMarker(null);
|
setMarker(null);
|
||||||
setMarkerId(null);
|
setMarkerId(null);
|
||||||
setHighlightId(null);
|
setHighlightId(null);
|
||||||
|
|
||||||
logger.debug('useEventMarker', 'removeMarker', '移除事件标记和高亮', {
|
logger.debug('useEventMarker', '移除事件标记和高亮 (removeMarker)', {
|
||||||
markerId,
|
markerId,
|
||||||
highlightId,
|
highlightId,
|
||||||
chartId: chart.id,
|
chartId: chart.id,
|
||||||
@@ -187,7 +187,7 @@ export const useEventMarker = (
|
|||||||
setMarkerId(null);
|
setMarkerId(null);
|
||||||
setHighlightId(null);
|
setHighlightId(null);
|
||||||
|
|
||||||
logger.debug('useEventMarker', 'removeAllMarkers', '移除所有事件标记和高亮', {
|
logger.debug('useEventMarker', '移除所有事件标记和高亮 (removeAllMarkers)', {
|
||||||
chartId: chart.id,
|
chartId: chart.id,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -216,10 +216,10 @@ export const useEventMarker = (
|
|||||||
if (chart) {
|
if (chart) {
|
||||||
try {
|
try {
|
||||||
if (markerId) {
|
if (markerId) {
|
||||||
chart.removeOverlay(markerId);
|
chart.removeOverlay({ id: markerId } as any);
|
||||||
}
|
}
|
||||||
if (highlightId) {
|
if (highlightId) {
|
||||||
chart.removeOverlay(highlightId);
|
chart.removeOverlay({ id: highlightId } as any);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// 忽略清理时的错误
|
// 忽略清理时的错误
|
||||||
|
|||||||
@@ -78,12 +78,12 @@ export const useKLineChart = (
|
|||||||
// 图表初始化函数
|
// 图表初始化函数
|
||||||
const initChart = (): boolean => {
|
const initChart = (): boolean => {
|
||||||
if (!chartRef.current) {
|
if (!chartRef.current) {
|
||||||
logger.warn('useKLineChart', 'init', '图表容器未挂载,将在 50ms 后重试', { containerId });
|
logger.warn('useKLineChart', '图表容器未挂载,将在 50ms 后重试 (init)', { containerId });
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.debug('useKLineChart', 'init', '开始初始化图表', {
|
logger.debug('useKLineChart', '开始初始化图表 (init)', {
|
||||||
containerId,
|
containerId,
|
||||||
height,
|
height,
|
||||||
colorMode,
|
colorMode,
|
||||||
@@ -116,17 +116,17 @@ export const useKLineChart = (
|
|||||||
height: 100, // 固定高度 100px(约占整体的 20-25%)
|
height: 100, // 固定高度 100px(约占整体的 20-25%)
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug('useKLineChart', 'init', '成交量窗格创建成功', {
|
logger.debug('useKLineChart', '成交量窗格创建成功 (init)', {
|
||||||
volumePaneId,
|
volumePaneId,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn('useKLineChart', 'init', '成交量窗格创建失败', {
|
logger.warn('useKLineChart', '成交量窗格创建失败 (init)', {
|
||||||
error: err,
|
error: err,
|
||||||
});
|
});
|
||||||
// 不阻塞主流程,继续执行
|
// 不阻塞主流程,继续执行
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('useKLineChart', 'init', '✅ 图表初始化成功', {
|
logger.info('useKLineChart', '✅ 图表初始化成功 (init)', {
|
||||||
containerId,
|
containerId,
|
||||||
chartId: chartInstance.id,
|
chartId: chartInstance.id,
|
||||||
});
|
});
|
||||||
@@ -146,7 +146,7 @@ export const useKLineChart = (
|
|||||||
// 成功,直接返回清理函数
|
// 成功,直接返回清理函数
|
||||||
return () => {
|
return () => {
|
||||||
if (chartInstanceRef.current) {
|
if (chartInstanceRef.current) {
|
||||||
logger.debug('useKLineChart', 'dispose', '销毁图表实例', {
|
logger.debug('useKLineChart', '销毁图表实例 (dispose)', {
|
||||||
containerId,
|
containerId,
|
||||||
chartId: chartInstanceRef.current.id,
|
chartId: chartInstanceRef.current.id,
|
||||||
});
|
});
|
||||||
@@ -161,7 +161,7 @@ export const useKLineChart = (
|
|||||||
|
|
||||||
// 失败则延迟重试(处理 Modal 动画延迟导致的 DOM 未挂载)
|
// 失败则延迟重试(处理 Modal 动画延迟导致的 DOM 未挂载)
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
logger.debug('useKLineChart', 'init', '执行延迟重试', { containerId });
|
logger.debug('useKLineChart', '执行延迟重试 (init)', { containerId });
|
||||||
initChart();
|
initChart();
|
||||||
}, 50);
|
}, 50);
|
||||||
|
|
||||||
@@ -169,7 +169,7 @@ export const useKLineChart = (
|
|||||||
return () => {
|
return () => {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
if (chartInstanceRef.current) {
|
if (chartInstanceRef.current) {
|
||||||
logger.debug('useKLineChart', 'dispose', '销毁图表实例', {
|
logger.debug('useKLineChart', '销毁图表实例 (dispose)', {
|
||||||
containerId,
|
containerId,
|
||||||
chartId: chartInstanceRef.current.id,
|
chartId: chartInstanceRef.current.id,
|
||||||
});
|
});
|
||||||
@@ -196,7 +196,7 @@ export const useKLineChart = (
|
|||||||
: getTheme(colorMode);
|
: getTheme(colorMode);
|
||||||
chartInstanceRef.current.setStyles(newTheme);
|
chartInstanceRef.current.setStyles(newTheme);
|
||||||
|
|
||||||
logger.debug('useKLineChart', 'updateTheme', '更新图表主题', {
|
logger.debug('useKLineChart', '更新图表主题 (updateTheme)', {
|
||||||
colorMode,
|
colorMode,
|
||||||
chartType,
|
chartType,
|
||||||
chartId: chartInstanceRef.current.id,
|
chartId: chartInstanceRef.current.id,
|
||||||
@@ -215,7 +215,6 @@ export const useKLineChart = (
|
|||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
if (chartInstanceRef.current) {
|
if (chartInstanceRef.current) {
|
||||||
chartInstanceRef.current.resize();
|
chartInstanceRef.current.resize();
|
||||||
logger.debug('useKLineChart', 'resize', '调整图表大小');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export const useKLineData = (
|
|||||||
*/
|
*/
|
||||||
const loadData = useCallback(async () => {
|
const loadData = useCallback(async () => {
|
||||||
if (!stockCode) {
|
if (!stockCode) {
|
||||||
logger.warn('useKLineData', 'loadData', '股票代码为空', { chartType });
|
logger.warn('useKLineData', '股票代码为空 (loadData)', { chartType });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ export const useKLineData = (
|
|||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.debug('useKLineData', 'loadData', '开始加载数据', {
|
logger.debug('useKLineData', '开始加载数据 (loadData)', {
|
||||||
stockCode,
|
stockCode,
|
||||||
chartType,
|
chartType,
|
||||||
eventTime,
|
eventTime,
|
||||||
@@ -126,7 +126,7 @@ export const useKLineData = (
|
|||||||
|
|
||||||
setData(processedData);
|
setData(processedData);
|
||||||
|
|
||||||
logger.info('useKLineData', 'loadData', '数据加载成功', {
|
logger.info('useKLineData', '数据加载成功 (loadData)', {
|
||||||
stockCode,
|
stockCode,
|
||||||
chartType,
|
chartType,
|
||||||
rawCount: rawDataList.length,
|
rawCount: rawDataList.length,
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ export interface RawDataPoint {
|
|||||||
close: number;
|
close: number;
|
||||||
/** 成交量 */
|
/** 成交量 */
|
||||||
volume: number;
|
volume: number;
|
||||||
|
/** 成交额(可选) */
|
||||||
|
turnover?: number;
|
||||||
/** 均价(分时图专用) */
|
/** 均价(分时图专用) */
|
||||||
avg_price?: number;
|
avg_price?: number;
|
||||||
/** 昨收价(用于百分比计算和基准线)- 分时图专用 */
|
/** 昨收价(用于百分比计算和基准线)- 分时图专用 */
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export const createIndicator = (
|
|||||||
isStack
|
isStack
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.debug('chartUtils', 'createIndicator', '创建技术指标', {
|
logger.debug('chartUtils', '创建技术指标 (createIndicator)', {
|
||||||
indicatorName,
|
indicatorName,
|
||||||
params,
|
params,
|
||||||
isStack,
|
isStack,
|
||||||
@@ -69,8 +69,8 @@ export const createIndicator = (
|
|||||||
*/
|
*/
|
||||||
export const removeIndicator = (chart: Chart, indicatorId?: string): void => {
|
export const removeIndicator = (chart: Chart, indicatorId?: string): void => {
|
||||||
safeChartOperation('removeIndicator', () => {
|
safeChartOperation('removeIndicator', () => {
|
||||||
chart.removeIndicator(indicatorId);
|
chart.removeIndicator(indicatorId ? { id: indicatorId } as any : undefined);
|
||||||
logger.debug('chartUtils', 'removeIndicator', '移除技术指标', { indicatorId });
|
logger.debug('chartUtils', '移除技术指标 (removeIndicator)', { indicatorId });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ export const createSubIndicators = (
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug('chartUtils', 'createSubIndicators', '批量创建副图指标', {
|
logger.debug('chartUtils', '批量创建副图指标 (createSubIndicators)', {
|
||||||
indicators,
|
indicators,
|
||||||
createdIds: ids,
|
createdIds: ids,
|
||||||
});
|
});
|
||||||
@@ -130,7 +130,7 @@ export const setChartZoom = (chart: Chart, zoom: number): void => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug('chartUtils', 'setChartZoom', '设置图表缩放', {
|
logger.debug('chartUtils', '设置图表缩放 (setChartZoom)', {
|
||||||
zoom,
|
zoom,
|
||||||
newBarSpace,
|
newBarSpace,
|
||||||
});
|
});
|
||||||
@@ -148,7 +148,7 @@ export const scrollToTimestamp = (chart: Chart, timestamp: number): void => {
|
|||||||
// KLineChart 10.0: 使用 scrollToTimestamp 方法
|
// KLineChart 10.0: 使用 scrollToTimestamp 方法
|
||||||
chart.scrollToTimestamp(timestamp);
|
chart.scrollToTimestamp(timestamp);
|
||||||
|
|
||||||
logger.debug('chartUtils', 'scrollToTimestamp', '滚动到指定时间', { timestamp });
|
logger.debug('chartUtils', '滚动到指定时间 (scrollToTimestamp)', { timestamp });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ export const scrollToTimestamp = (chart: Chart, timestamp: number): void => {
|
|||||||
export const resizeChart = (chart: Chart): void => {
|
export const resizeChart = (chart: Chart): void => {
|
||||||
safeChartOperation('resizeChart', () => {
|
safeChartOperation('resizeChart', () => {
|
||||||
chart.resize();
|
chart.resize();
|
||||||
logger.debug('chartUtils', 'resizeChart', '调整图表大小');
|
logger.debug('chartUtils', '调整图表大小 (resizeChart)');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -194,7 +194,7 @@ export const getVisibleRange = (chart: Chart): { from: number; to: number } | nu
|
|||||||
export const clearChartData = (chart: Chart): void => {
|
export const clearChartData = (chart: Chart): void => {
|
||||||
safeChartOperation('clearChartData', () => {
|
safeChartOperation('clearChartData', () => {
|
||||||
chart.resetData();
|
chart.resetData();
|
||||||
logger.debug('chartUtils', 'clearChartData', '清空图表数据');
|
logger.debug('chartUtils', '清空图表数据 (clearChartData)');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -213,7 +213,7 @@ export const exportChartImage = (
|
|||||||
// KLineChart 10.0: 使用 getConvertPictureUrl 方法
|
// KLineChart 10.0: 使用 getConvertPictureUrl 方法
|
||||||
const imageData = chart.getConvertPictureUrl(includeOverlay, 'png', '#ffffff');
|
const imageData = chart.getConvertPictureUrl(includeOverlay, 'png', '#ffffff');
|
||||||
|
|
||||||
logger.debug('chartUtils', 'exportChartImage', '导出图表图片', {
|
logger.debug('chartUtils', '导出图表图片 (exportChartImage)', {
|
||||||
includeOverlay,
|
includeOverlay,
|
||||||
hasData: !!imageData,
|
hasData: !!imageData,
|
||||||
});
|
});
|
||||||
@@ -236,7 +236,7 @@ export const toggleCrosshair = (chart: Chart, show: boolean): void => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug('chartUtils', 'toggleCrosshair', '切换十字光标', { show });
|
logger.debug('chartUtils', '切换十字光标 (toggleCrosshair)', { show });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -254,7 +254,7 @@ export const toggleGrid = (chart: Chart, show: boolean): void => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug('chartUtils', 'toggleGrid', '切换网格', { show });
|
logger.debug('chartUtils', '切换网格 (toggleGrid)', { show });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -271,8 +271,8 @@ export const subscribeChartEvent = (
|
|||||||
handler: (...args: any[]) => void
|
handler: (...args: any[]) => void
|
||||||
): void => {
|
): void => {
|
||||||
safeChartOperation(`subscribeChartEvent:${eventName}`, () => {
|
safeChartOperation(`subscribeChartEvent:${eventName}`, () => {
|
||||||
chart.subscribeAction(eventName, handler);
|
chart.subscribeAction(eventName as any, handler);
|
||||||
logger.debug('chartUtils', 'subscribeChartEvent', '订阅图表事件', { eventName });
|
logger.debug('chartUtils', '订阅图表事件 (subscribeChartEvent)', { eventName });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -289,7 +289,7 @@ export const unsubscribeChartEvent = (
|
|||||||
handler: (...args: any[]) => void
|
handler: (...args: any[]) => void
|
||||||
): void => {
|
): void => {
|
||||||
safeChartOperation(`unsubscribeChartEvent:${eventName}`, () => {
|
safeChartOperation(`unsubscribeChartEvent:${eventName}`, () => {
|
||||||
chart.unsubscribeAction(eventName, handler);
|
chart.unsubscribeAction(eventName as any, handler);
|
||||||
logger.debug('chartUtils', 'unsubscribeChartEvent', '取消订阅图表事件', { eventName });
|
logger.debug('chartUtils', '取消订阅图表事件 (unsubscribeChartEvent)', { eventName });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const convertToKLineData = (
|
|||||||
eventTime?: string
|
eventTime?: string
|
||||||
): KLineDataPoint[] => {
|
): KLineDataPoint[] => {
|
||||||
if (!rawData || !Array.isArray(rawData) || rawData.length === 0) {
|
if (!rawData || !Array.isArray(rawData) || rawData.length === 0) {
|
||||||
logger.warn('dataAdapter', 'convertToKLineData', '原始数据为空', { chartType });
|
logger.warn('dataAdapter', '原始数据为空 (convertToKLineData)', { chartType });
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ const parseTimestamp = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 默认返回当前时间(避免图表崩溃)
|
// 默认返回当前时间(避免图表崩溃)
|
||||||
logger.warn('dataAdapter', 'parseTimestamp', '无法解析时间戳,使用当前时间', { item });
|
logger.warn('dataAdapter', '无法解析时间戳,使用当前时间 (parseTimestamp)', { item });
|
||||||
return Date.now();
|
return Date.now();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -126,19 +126,19 @@ export const validateAndCleanData = (data: KLineDataPoint[]): KLineDataPoint[] =
|
|||||||
return data.filter((item) => {
|
return data.filter((item) => {
|
||||||
// 移除价格为 0 或负数的数据
|
// 移除价格为 0 或负数的数据
|
||||||
if (item.open <= 0 || item.high <= 0 || item.low <= 0 || item.close <= 0) {
|
if (item.open <= 0 || item.high <= 0 || item.low <= 0 || item.close <= 0) {
|
||||||
logger.warn('dataAdapter', 'validateAndCleanData', '价格异常,已移除', { item });
|
logger.warn('dataAdapter', '价格异常,已移除 (validateAndCleanData)', { item });
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移除 high < low 的数据(数据错误)
|
// 移除 high < low 的数据(数据错误)
|
||||||
if (item.high < item.low) {
|
if (item.high < item.low) {
|
||||||
logger.warn('dataAdapter', 'validateAndCleanData', '最高价 < 最低价,已移除', { item });
|
logger.warn('dataAdapter', '最高价 < 最低价,已移除 (validateAndCleanData)', { item });
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移除成交量为负数的数据
|
// 移除成交量为负数的数据
|
||||||
if (item.volume < 0) {
|
if (item.volume < 0) {
|
||||||
logger.warn('dataAdapter', 'validateAndCleanData', '成交量异常,已移除', { item });
|
logger.warn('dataAdapter', '成交量异常,已移除 (validateAndCleanData)', { item });
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,7 +213,7 @@ export const trimDataByEventTime = (
|
|||||||
return item.timestamp >= startTime && item.timestamp <= endTime;
|
return item.timestamp >= startTime && item.timestamp <= endTime;
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug('dataAdapter', 'trimDataByEventTime', '数据时间范围裁剪完成', {
|
logger.debug('dataAdapter', '数据时间范围裁剪完成 (trimDataByEventTime)', {
|
||||||
originalLength: data.length,
|
originalLength: data.length,
|
||||||
trimmedLength: trimmedData.length,
|
trimmedLength: trimmedData.length,
|
||||||
eventTime,
|
eventTime,
|
||||||
@@ -260,7 +260,7 @@ export const processChartData = (
|
|||||||
data = trimDataByEventTime(data, eventTime, chartType);
|
data = trimDataByEventTime(data, eventTime, chartType);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug('dataAdapter', 'processChartData', '数据处理完成', {
|
logger.debug('dataAdapter', '数据处理完成 (processChartData)', {
|
||||||
rawLength: rawData.length,
|
rawLength: rawData.length,
|
||||||
processedLength: data.length,
|
processedLength: data.length,
|
||||||
chartType,
|
chartType,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export const createEventMarkerOverlay = (
|
|||||||
const closestPoint = findClosestDataPoint(data, marker.timestamp);
|
const closestPoint = findClosestDataPoint(data, marker.timestamp);
|
||||||
|
|
||||||
if (!closestPoint) {
|
if (!closestPoint) {
|
||||||
logger.warn('eventMarkerUtils', 'createEventMarkerOverlay', '未找到匹配的数据点', {
|
logger.warn('eventMarkerUtils', '未找到匹配的数据点', {
|
||||||
markerId: marker.id,
|
markerId: marker.id,
|
||||||
timestamp: marker.timestamp,
|
timestamp: marker.timestamp,
|
||||||
});
|
});
|
||||||
@@ -64,10 +64,12 @@ export const createEventMarkerOverlay = (
|
|||||||
style: 'fill',
|
style: 'fill',
|
||||||
color: marker.color,
|
color: marker.color,
|
||||||
borderRadius: EVENT_MARKER_CONFIG.text.borderRadius,
|
borderRadius: EVENT_MARKER_CONFIG.text.borderRadius,
|
||||||
paddingLeft: EVENT_MARKER_CONFIG.text.padding,
|
padding: [
|
||||||
paddingRight: EVENT_MARKER_CONFIG.text.padding,
|
EVENT_MARKER_CONFIG.text.padding,
|
||||||
paddingTop: EVENT_MARKER_CONFIG.text.padding,
|
EVENT_MARKER_CONFIG.text.padding,
|
||||||
paddingBottom: EVENT_MARKER_CONFIG.text.padding,
|
EVENT_MARKER_CONFIG.text.padding,
|
||||||
|
EVENT_MARKER_CONFIG.text.padding,
|
||||||
|
] as any,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// 标记文本内容
|
// 标记文本内容
|
||||||
@@ -77,7 +79,7 @@ export const createEventMarkerOverlay = (
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.debug('eventMarkerUtils', 'createEventMarkerOverlay', '创建事件标记', {
|
logger.debug('eventMarkerUtils', '创建事件标记', {
|
||||||
markerId: marker.id,
|
markerId: marker.id,
|
||||||
timestamp: closestPoint.timestamp,
|
timestamp: closestPoint.timestamp,
|
||||||
label: marker.label,
|
label: marker.label,
|
||||||
@@ -108,7 +110,7 @@ export const createEventHighlightOverlay = (
|
|||||||
const closestPoint = findClosestDataPoint(data, eventTimestamp);
|
const closestPoint = findClosestDataPoint(data, eventTimestamp);
|
||||||
|
|
||||||
if (!closestPoint) {
|
if (!closestPoint) {
|
||||||
logger.warn('eventMarkerUtils', 'createEventHighlightOverlay', '未找到匹配的数据点');
|
logger.warn('eventMarkerUtils', '未找到匹配的数据点');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +137,7 @@ export const createEventHighlightOverlay = (
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.debug('eventMarkerUtils', 'createEventHighlightOverlay', '创建事件高亮覆盖层', {
|
logger.debug('eventMarkerUtils', '创建事件高亮覆盖层', {
|
||||||
timestamp: closestPoint.timestamp,
|
timestamp: closestPoint.timestamp,
|
||||||
eventTime,
|
eventTime,
|
||||||
});
|
});
|
||||||
@@ -219,7 +221,7 @@ export const createEventMarkerOverlays = (
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug('eventMarkerUtils', 'createEventMarkerOverlays', '批量创建事件标记', {
|
logger.debug('eventMarkerUtils', '批量创建事件标记', {
|
||||||
totalMarkers: markers.length,
|
totalMarkers: markers.length,
|
||||||
createdOverlays: overlays.length,
|
createdOverlays: overlays.length,
|
||||||
});
|
});
|
||||||
@@ -236,7 +238,7 @@ export const createEventMarkerOverlays = (
|
|||||||
export const removeEventMarker = (chart: any, markerId: string): void => {
|
export const removeEventMarker = (chart: any, markerId: string): void => {
|
||||||
try {
|
try {
|
||||||
chart.removeOverlay(markerId);
|
chart.removeOverlay(markerId);
|
||||||
logger.debug('eventMarkerUtils', 'removeEventMarker', '移除事件标记', { markerId });
|
logger.debug('eventMarkerUtils', '移除事件标记', { markerId });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('eventMarkerUtils', 'removeEventMarker', error as Error, { markerId });
|
logger.error('eventMarkerUtils', 'removeEventMarker', error as Error, { markerId });
|
||||||
}
|
}
|
||||||
@@ -251,7 +253,7 @@ export const removeAllEventMarkers = (chart: any): void => {
|
|||||||
try {
|
try {
|
||||||
// KLineChart 10.0 API: removeOverlay() 不传参数时移除所有 overlays
|
// KLineChart 10.0 API: removeOverlay() 不传参数时移除所有 overlays
|
||||||
chart.removeOverlay();
|
chart.removeOverlay();
|
||||||
logger.debug('eventMarkerUtils', 'removeAllEventMarkers', '移除所有事件标记');
|
logger.debug('eventMarkerUtils', '移除所有事件标记');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('eventMarkerUtils', 'removeAllEventMarkers', error as Error);
|
logger.error('eventMarkerUtils', 'removeAllEventMarkers', error as Error);
|
||||||
}
|
}
|
||||||
@@ -276,7 +278,7 @@ export const updateEventMarker = (
|
|||||||
// 重新创建标记(KLineChart 10.0 不支持直接更新 overlay)
|
// 重新创建标记(KLineChart 10.0 不支持直接更新 overlay)
|
||||||
// 注意:需要在调用方重新创建并添加 overlay
|
// 注意:需要在调用方重新创建并添加 overlay
|
||||||
|
|
||||||
logger.debug('eventMarkerUtils', 'updateEventMarker', '更新事件标记', {
|
logger.debug('eventMarkerUtils', '更新事件标记', {
|
||||||
markerId,
|
markerId,
|
||||||
updates,
|
updates,
|
||||||
});
|
});
|
||||||
@@ -309,7 +311,7 @@ export const highlightEventMarker = (
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug('eventMarkerUtils', 'highlightEventMarker', '高亮事件标记', {
|
logger.debug('eventMarkerUtils', '高亮事件标记', {
|
||||||
markerId,
|
markerId,
|
||||||
highlight,
|
highlight,
|
||||||
});
|
});
|
||||||
|
|||||||
204
src/views/AgentChat/components/ChatArea/ChatHeader.tsx
Normal file
204
src/views/AgentChat/components/ChatArea/ChatHeader.tsx
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
// src/views/AgentChat/components/ChatArea/ChatHeader.tsx
|
||||||
|
// 聊天区顶部标题栏组件
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Avatar,
|
||||||
|
Badge,
|
||||||
|
Tooltip,
|
||||||
|
IconButton,
|
||||||
|
HStack,
|
||||||
|
Flex,
|
||||||
|
Text,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { Menu, RefreshCw, Settings, Cpu, Zap } from 'lucide-react';
|
||||||
|
import { AVAILABLE_MODELS } from '../../constants/models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ChatHeader 组件的 Props 类型
|
||||||
|
*/
|
||||||
|
interface ChatHeaderProps {
|
||||||
|
/** 当前选中的模型 ID */
|
||||||
|
selectedModel: string;
|
||||||
|
/** 左侧栏是否展开 */
|
||||||
|
isLeftSidebarOpen: boolean;
|
||||||
|
/** 右侧栏是否展开 */
|
||||||
|
isRightSidebarOpen: boolean;
|
||||||
|
/** 切换左侧栏回调 */
|
||||||
|
onToggleLeftSidebar: () => void;
|
||||||
|
/** 切换右侧栏回调 */
|
||||||
|
onToggleRightSidebar: () => void;
|
||||||
|
/** 新建会话回调 */
|
||||||
|
onNewSession: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ChatHeader - 聊天区顶部标题栏组件
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* 1. 展示 AI 头像和标题
|
||||||
|
* 2. 显示当前模型徽章
|
||||||
|
* 3. 左侧栏/右侧栏切换按钮
|
||||||
|
* 4. 新建会话按钮
|
||||||
|
*
|
||||||
|
* 设计:
|
||||||
|
* - 深色毛玻璃背景
|
||||||
|
* - 旋转的 AI 头像动画
|
||||||
|
* - 渐变色标题和徽章
|
||||||
|
*/
|
||||||
|
const ChatHeader: React.FC<ChatHeaderProps> = ({
|
||||||
|
selectedModel,
|
||||||
|
isLeftSidebarOpen,
|
||||||
|
isRightSidebarOpen,
|
||||||
|
onToggleLeftSidebar,
|
||||||
|
onToggleRightSidebar,
|
||||||
|
onNewSession,
|
||||||
|
}) => {
|
||||||
|
const currentModel = AVAILABLE_MODELS.find((m) => m.id === selectedModel);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
bg="rgba(17, 24, 39, 0.8)"
|
||||||
|
backdropFilter="blur(20px) saturate(180%)"
|
||||||
|
borderBottom="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
px={6}
|
||||||
|
py={4}
|
||||||
|
boxShadow="0 8px 32px 0 rgba(31, 38, 135, 0.37)"
|
||||||
|
>
|
||||||
|
<Flex align="center" justify="space-between">
|
||||||
|
{/* 左侧:标题和徽章 */}
|
||||||
|
<HStack spacing={4}>
|
||||||
|
{/* 左侧栏切换按钮(仅在侧边栏收起时显示) */}
|
||||||
|
{!isLeftSidebarOpen && (
|
||||||
|
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
icon={<Menu className="w-4 h-4" />}
|
||||||
|
onClick={onToggleLeftSidebar}
|
||||||
|
aria-label="展开左侧栏"
|
||||||
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
|
color="gray.400"
|
||||||
|
backdropFilter="blur(10px)"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
_hover={{
|
||||||
|
bg: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
color: 'white',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* AI 头像(旋转动画) */}
|
||||||
|
<motion.div
|
||||||
|
animate={{ rotate: 360 }}
|
||||||
|
transition={{ duration: 3, repeat: Infinity, ease: 'linear' }}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
icon={<Cpu className="w-6 h-6" />}
|
||||||
|
bgGradient="linear(to-br, purple.500, pink.500)"
|
||||||
|
boxShadow="0 0 20px rgba(236, 72, 153, 0.5)"
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* 标题和徽章 */}
|
||||||
|
<Box>
|
||||||
|
<Text
|
||||||
|
fontSize="xl"
|
||||||
|
fontWeight="bold"
|
||||||
|
bgGradient="linear(to-r, blue.400, purple.400)"
|
||||||
|
bgClip="text"
|
||||||
|
letterSpacing="tight"
|
||||||
|
>
|
||||||
|
价小前投研 AI
|
||||||
|
</Text>
|
||||||
|
<HStack spacing={2} mt={1}>
|
||||||
|
{/* 智能分析徽章 */}
|
||||||
|
<Badge
|
||||||
|
bgGradient="linear(to-r, green.500, teal.500)"
|
||||||
|
color="white"
|
||||||
|
px={2}
|
||||||
|
py={1}
|
||||||
|
borderRadius="md"
|
||||||
|
display="flex"
|
||||||
|
alignItems="center"
|
||||||
|
gap={1}
|
||||||
|
boxShadow="0 2px 8px rgba(16, 185, 129, 0.3)"
|
||||||
|
>
|
||||||
|
<Zap className="w-3 h-3" />
|
||||||
|
智能分析
|
||||||
|
</Badge>
|
||||||
|
|
||||||
|
{/* 模型名称徽章 */}
|
||||||
|
<Badge
|
||||||
|
bgGradient="linear(to-r, purple.500, pink.500)"
|
||||||
|
color="white"
|
||||||
|
px={2}
|
||||||
|
py={1}
|
||||||
|
borderRadius="md"
|
||||||
|
boxShadow="0 2px 8px rgba(139, 92, 246, 0.3)"
|
||||||
|
>
|
||||||
|
{currentModel?.name || '未知模型'}
|
||||||
|
</Badge>
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
{/* 右侧:操作按钮 */}
|
||||||
|
<HStack spacing={2}>
|
||||||
|
{/* 清空对话按钮 */}
|
||||||
|
<Tooltip label="清空对话">
|
||||||
|
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
icon={<RefreshCw className="w-4 h-4" />}
|
||||||
|
onClick={onNewSession}
|
||||||
|
aria-label="清空对话"
|
||||||
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
|
color="gray.400"
|
||||||
|
backdropFilter="blur(10px)"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
_hover={{
|
||||||
|
bg: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
color: 'white',
|
||||||
|
borderColor: 'purple.400',
|
||||||
|
boxShadow: '0 0 12px rgba(139, 92, 246, 0.3)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{/* 右侧栏切换按钮(仅在侧边栏收起时显示) */}
|
||||||
|
{!isRightSidebarOpen && (
|
||||||
|
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
icon={<Settings className="w-4 h-4" />}
|
||||||
|
onClick={onToggleRightSidebar}
|
||||||
|
aria-label="展开右侧栏"
|
||||||
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
|
color="gray.400"
|
||||||
|
backdropFilter="blur(10px)"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
_hover={{
|
||||||
|
bg: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
color: 'white',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChatHeader;
|
||||||
253
src/views/AgentChat/components/ChatArea/ChatInput.tsx
Normal file
253
src/views/AgentChat/components/ChatArea/ChatInput.tsx
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
// src/views/AgentChat/components/ChatArea/ChatInput.tsx
|
||||||
|
// 聊天输入框区域组件
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Input,
|
||||||
|
IconButton,
|
||||||
|
Tooltip,
|
||||||
|
Kbd,
|
||||||
|
HStack,
|
||||||
|
Text,
|
||||||
|
Tag,
|
||||||
|
TagLabel,
|
||||||
|
TagCloseButton,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { Send, Paperclip, Image as ImageIcon } from 'lucide-react';
|
||||||
|
import type { UploadedFile } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ChatInput 组件的 Props 类型
|
||||||
|
*/
|
||||||
|
interface ChatInputProps {
|
||||||
|
/** 输入框内容 */
|
||||||
|
inputValue: string;
|
||||||
|
/** 输入框变化回调 */
|
||||||
|
onInputChange: (value: string) => void;
|
||||||
|
/** 键盘事件回调 */
|
||||||
|
onKeyPress: (e: React.KeyboardEvent<HTMLInputElement>) => void;
|
||||||
|
/** 已上传文件列表 */
|
||||||
|
uploadedFiles: UploadedFile[];
|
||||||
|
/** 文件选择回调 */
|
||||||
|
onFileSelect: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
/** 文件删除回调 */
|
||||||
|
onFileRemove: (index: number) => void;
|
||||||
|
/** 是否正在处理中 */
|
||||||
|
isProcessing: boolean;
|
||||||
|
/** 发送消息回调 */
|
||||||
|
onSendMessage: () => void;
|
||||||
|
/** 输入框引用 */
|
||||||
|
inputRef: React.RefObject<HTMLInputElement>;
|
||||||
|
/** 文件上传输入引用 */
|
||||||
|
fileInputRef: React.RefObject<HTMLInputElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ChatInput - 聊天输入框区域组件
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* 1. 文件上传按钮(Paperclip, Image)
|
||||||
|
* 2. 输入框
|
||||||
|
* 3. 发送按钮
|
||||||
|
* 4. 已上传文件预览
|
||||||
|
* 5. 快捷键提示
|
||||||
|
*
|
||||||
|
* 设计:
|
||||||
|
* - 深色毛玻璃背景
|
||||||
|
* - 渐变色发送按钮
|
||||||
|
* - 悬停动画效果
|
||||||
|
* - 响应式最大宽度(896px)
|
||||||
|
*/
|
||||||
|
const ChatInput: React.FC<ChatInputProps> = ({
|
||||||
|
inputValue,
|
||||||
|
onInputChange,
|
||||||
|
onKeyPress,
|
||||||
|
uploadedFiles,
|
||||||
|
onFileSelect,
|
||||||
|
onFileRemove,
|
||||||
|
isProcessing,
|
||||||
|
onSendMessage,
|
||||||
|
inputRef,
|
||||||
|
fileInputRef,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
bg="rgba(17, 24, 39, 0.8)"
|
||||||
|
backdropFilter="blur(20px) saturate(180%)"
|
||||||
|
borderTop="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
px={6}
|
||||||
|
py={1}
|
||||||
|
boxShadow="0 -8px 32px 0 rgba(31, 38, 135, 0.37)"
|
||||||
|
>
|
||||||
|
<Box maxW="896px" mx="auto">
|
||||||
|
{/* 已上传文件预览 */}
|
||||||
|
{uploadedFiles.length > 0 && (
|
||||||
|
<HStack mb={3} flexWrap="wrap" spacing={2}>
|
||||||
|
{uploadedFiles.map((file, idx) => (
|
||||||
|
<motion.div
|
||||||
|
key={idx}
|
||||||
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.8 }}
|
||||||
|
>
|
||||||
|
<Tag
|
||||||
|
size="md"
|
||||||
|
variant="subtle"
|
||||||
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
|
backdropFilter="blur(10px)"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
borderWidth={1}
|
||||||
|
>
|
||||||
|
<TagLabel color="gray.300">{file.name}</TagLabel>
|
||||||
|
<TagCloseButton onClick={() => onFileRemove(idx)} color="gray.400" />
|
||||||
|
</Tag>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 输入栏 */}
|
||||||
|
<HStack spacing={2}>
|
||||||
|
{/* 隐藏的文件上传输入 */}
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
accept="image/*,.pdf,.doc,.docx,.txt"
|
||||||
|
onChange={onFileSelect}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 上传文件按钮 */}
|
||||||
|
<Tooltip label="上传文件">
|
||||||
|
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
|
||||||
|
<IconButton
|
||||||
|
variant="ghost"
|
||||||
|
size="lg"
|
||||||
|
icon={<Paperclip className="w-5 h-5" />}
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
aria-label="上传文件"
|
||||||
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
|
color="gray.300"
|
||||||
|
backdropFilter="blur(10px)"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
_hover={{
|
||||||
|
bg: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
borderColor: 'purple.400',
|
||||||
|
color: 'white',
|
||||||
|
boxShadow: '0 0 12px rgba(139, 92, 246, 0.3)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{/* 上传图片按钮 */}
|
||||||
|
<Tooltip label="上传图片">
|
||||||
|
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
|
||||||
|
<IconButton
|
||||||
|
variant="ghost"
|
||||||
|
size="lg"
|
||||||
|
icon={<ImageIcon className="w-5 h-5" />}
|
||||||
|
onClick={() => {
|
||||||
|
fileInputRef.current?.setAttribute('accept', 'image/*');
|
||||||
|
fileInputRef.current?.click();
|
||||||
|
}}
|
||||||
|
aria-label="上传图片"
|
||||||
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
|
color="gray.300"
|
||||||
|
backdropFilter="blur(10px)"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
_hover={{
|
||||||
|
bg: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
borderColor: 'purple.400',
|
||||||
|
color: 'white',
|
||||||
|
boxShadow: '0 0 12px rgba(139, 92, 246, 0.3)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{/* 输入框 */}
|
||||||
|
<Input
|
||||||
|
ref={inputRef}
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(e) => onInputChange(e.target.value)}
|
||||||
|
onKeyDown={onKeyPress}
|
||||||
|
placeholder="输入你的问题... (Enter 发送, Shift+Enter 换行)"
|
||||||
|
isDisabled={isProcessing}
|
||||||
|
size="lg"
|
||||||
|
variant="outline"
|
||||||
|
borderWidth={2}
|
||||||
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
|
backdropFilter="blur(10px)"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
color="white"
|
||||||
|
_placeholder={{ color: 'gray.500' }}
|
||||||
|
_hover={{
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
}}
|
||||||
|
_focus={{
|
||||||
|
borderColor: 'purple.400',
|
||||||
|
boxShadow: '0 0 0 1px var(--chakra-colors-purple-400), 0 0 12px rgba(139, 92, 246, 0.3)',
|
||||||
|
bg: 'rgba(255, 255, 255, 0.08)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 发送按钮 */}
|
||||||
|
<motion.div
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
size="lg"
|
||||||
|
icon={!isProcessing ? <Send className="w-5 h-5" /> : undefined}
|
||||||
|
onClick={onSendMessage}
|
||||||
|
isLoading={isProcessing}
|
||||||
|
isDisabled={!inputValue.trim() || isProcessing}
|
||||||
|
aria-label="发送消息"
|
||||||
|
bgGradient="linear(to-r, blue.500, purple.600)"
|
||||||
|
color="white"
|
||||||
|
_hover={{
|
||||||
|
bgGradient: 'linear(to-r, blue.600, purple.700)',
|
||||||
|
boxShadow: '0 8px 20px rgba(139, 92, 246, 0.4)',
|
||||||
|
}}
|
||||||
|
_active={{
|
||||||
|
transform: 'translateY(0)',
|
||||||
|
boxShadow: '0 4px 12px rgba(139, 92, 246, 0.3)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
{/* 快捷键提示 */}
|
||||||
|
<HStack spacing={4} mt={2} fontSize="xs" color="gray.500">
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Kbd bg="rgba(255, 255, 255, 0.05)" color="gray.400" borderColor="rgba(255, 255, 255, 0.1)">
|
||||||
|
Enter
|
||||||
|
</Kbd>
|
||||||
|
<Text>发送</Text>
|
||||||
|
</HStack>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Kbd bg="rgba(255, 255, 255, 0.05)" color="gray.400" borderColor="rgba(255, 255, 255, 0.1)">
|
||||||
|
Shift
|
||||||
|
</Kbd>
|
||||||
|
<Text>+</Text>
|
||||||
|
<Kbd bg="rgba(255, 255, 255, 0.05)" color="gray.400" borderColor="rgba(255, 255, 255, 0.1)">
|
||||||
|
Enter
|
||||||
|
</Kbd>
|
||||||
|
<Text>换行</Text>
|
||||||
|
</HStack>
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChatInput;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// src/views/AgentChat/components/ChatArea/ExecutionStepsDisplay.js
|
// src/views/AgentChat/components/ChatArea/ExecutionStepsDisplay.tsx
|
||||||
// 执行步骤显示组件
|
// 执行步骤显示组件(TypeScript 版本)
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
@@ -18,16 +18,32 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { Activity } from 'lucide-react';
|
import { Activity } from 'lucide-react';
|
||||||
|
import type { ExecutionStep } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ExecutionStepsDisplay 组件的 Props 类型
|
||||||
|
*/
|
||||||
|
interface ExecutionStepsDisplayProps {
|
||||||
|
/** 执行步骤列表 */
|
||||||
|
steps: ExecutionStep[];
|
||||||
|
/** 执行计划(可选) */
|
||||||
|
plan?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ExecutionStepsDisplay - 执行步骤显示组件
|
* ExecutionStepsDisplay - 执行步骤显示组件
|
||||||
*
|
*
|
||||||
* @param {Object} props
|
* 职责:
|
||||||
* @param {Array} props.steps - 执行步骤列表
|
* 1. 以手风琴方式展示执行步骤
|
||||||
* @param {Object} props.plan - 执行计划(可选)
|
* 2. 显示每个步骤的状态、名称、耗时
|
||||||
* @returns {JSX.Element}
|
* 3. 显示错误信息(如果有)
|
||||||
|
*
|
||||||
|
* 设计:
|
||||||
|
* - 可折叠的手风琴容器
|
||||||
|
* - 成功/失败状态渐变色徽章
|
||||||
|
* - 步骤卡片渐进入场动画
|
||||||
*/
|
*/
|
||||||
const ExecutionStepsDisplay = ({ steps, plan }) => {
|
const ExecutionStepsDisplay: React.FC<ExecutionStepsDisplayProps> = ({ steps, plan }) => {
|
||||||
return (
|
return (
|
||||||
<Accordion allowToggle>
|
<Accordion allowToggle>
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
@@ -41,6 +57,7 @@ const ExecutionStepsDisplay = ({ steps, plan }) => {
|
|||||||
borderColor: 'rgba(255, 255, 255, 0.2)',
|
borderColor: 'rgba(255, 255, 255, 0.2)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{/* 手风琴标题 */}
|
||||||
<AccordionButton px={4} py={2}>
|
<AccordionButton px={4} py={2}>
|
||||||
<HStack flex={1} spacing={2}>
|
<HStack flex={1} spacing={2}>
|
||||||
<Activity className="w-4 h-4" color="#C084FC" />
|
<Activity className="w-4 h-4" color="#C084FC" />
|
||||||
@@ -58,6 +75,8 @@ const ExecutionStepsDisplay = ({ steps, plan }) => {
|
|||||||
</HStack>
|
</HStack>
|
||||||
<AccordionIcon color="gray.400" />
|
<AccordionIcon color="gray.400" />
|
||||||
</AccordionButton>
|
</AccordionButton>
|
||||||
|
|
||||||
|
{/* 手风琴内容 */}
|
||||||
<AccordionPanel pb={4}>
|
<AccordionPanel pb={4}>
|
||||||
<VStack spacing={2} align="stretch">
|
<VStack spacing={2} align="stretch">
|
||||||
{steps.map((result, idx) => (
|
{steps.map((result, idx) => (
|
||||||
@@ -75,9 +94,12 @@ const ExecutionStepsDisplay = ({ steps, plan }) => {
|
|||||||
>
|
>
|
||||||
<CardBody p={3}>
|
<CardBody p={3}>
|
||||||
<Flex align="start" justify="space-between" gap={2}>
|
<Flex align="start" justify="space-between" gap={2}>
|
||||||
|
{/* 步骤名称 */}
|
||||||
<Text fontSize="xs" fontWeight="medium" color="gray.300">
|
<Text fontSize="xs" fontWeight="medium" color="gray.300">
|
||||||
步骤 {idx + 1}: {result.tool_name}
|
步骤 {idx + 1}: {result.tool_name}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
{/* 状态徽章 */}
|
||||||
<Badge
|
<Badge
|
||||||
bgGradient={
|
bgGradient={
|
||||||
result.status === 'success'
|
result.status === 'success'
|
||||||
@@ -95,9 +117,15 @@ const ExecutionStepsDisplay = ({ steps, plan }) => {
|
|||||||
{result.status}
|
{result.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Text fontSize="xs" color="gray.500" mt={1}>
|
|
||||||
{result.execution_time?.toFixed(2)}s
|
{/* 执行耗时 */}
|
||||||
</Text>
|
{result.execution_time !== undefined && (
|
||||||
|
<Text fontSize="xs" color="gray.500" mt={1}>
|
||||||
|
{result.execution_time.toFixed(2)}s
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 错误信息 */}
|
||||||
{result.error && (
|
{result.error && (
|
||||||
<Text fontSize="xs" color="red.400" mt={1}>
|
<Text fontSize="xs" color="red.400" mt={1}>
|
||||||
⚠️ {result.error}
|
⚠️ {result.error}
|
||||||
78
src/views/AgentChat/components/ChatArea/MessageList.tsx
Normal file
78
src/views/AgentChat/components/ChatArea/MessageList.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
// src/views/AgentChat/components/ChatArea/MessageList.tsx
|
||||||
|
// 消息列表组件
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { Box, VStack } from '@chakra-ui/react';
|
||||||
|
import { animations } from '../../constants/animations';
|
||||||
|
import MessageRenderer from './MessageRenderer';
|
||||||
|
import type { Message } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MessageList 组件的 Props 类型
|
||||||
|
*/
|
||||||
|
interface MessageListProps {
|
||||||
|
/** 消息列表 */
|
||||||
|
messages: Message[];
|
||||||
|
/** 用户头像 URL */
|
||||||
|
userAvatar?: string;
|
||||||
|
/** 消息列表底部引用(用于自动滚动) */
|
||||||
|
messagesEndRef: React.RefObject<HTMLDivElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MessageList - 消息列表组件
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* 1. 渲染消息列表容器
|
||||||
|
* 2. 处理消息动画(进入/退出)
|
||||||
|
* 3. 提供自动滚动锚点
|
||||||
|
*
|
||||||
|
* 设计:
|
||||||
|
* - 渐变色背景
|
||||||
|
* - 消息淡入上滑动画
|
||||||
|
* - 退出消息淡出动画
|
||||||
|
* - 响应式最大宽度(896px)
|
||||||
|
*/
|
||||||
|
const MessageList: React.FC<MessageListProps> = ({
|
||||||
|
messages,
|
||||||
|
userAvatar,
|
||||||
|
messagesEndRef,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
flex={1}
|
||||||
|
bgGradient="linear(to-b, rgba(17, 24, 39, 0.5), rgba(17, 24, 39, 0.3))"
|
||||||
|
overflowY="auto"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
style={{ maxWidth: '896px', margin: '0 auto' }}
|
||||||
|
variants={animations.staggerContainer}
|
||||||
|
initial="initial"
|
||||||
|
animate="animate"
|
||||||
|
>
|
||||||
|
<VStack spacing={4} align="stretch">
|
||||||
|
<AnimatePresence mode="popLayout">
|
||||||
|
{messages.map((message) => (
|
||||||
|
<motion.div
|
||||||
|
key={message.id}
|
||||||
|
variants={animations.fadeInUp}
|
||||||
|
initial="initial"
|
||||||
|
animate="animate"
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
layout
|
||||||
|
>
|
||||||
|
<MessageRenderer message={message} userAvatar={userAvatar} />
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* 自动滚动锚点 */}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</VStack>
|
||||||
|
</motion.div>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MessageList;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// src/views/AgentChat/components/ChatArea/MessageRenderer.js
|
// src/views/AgentChat/components/ChatArea/MessageRenderer.tsx
|
||||||
// 消息渲染器组件
|
// 消息渲染器组件(TypeScript 版本)
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
@@ -19,17 +19,31 @@ import {
|
|||||||
import { Cpu, User, Copy, ThumbsUp, ThumbsDown, File } from 'lucide-react';
|
import { Cpu, User, Copy, ThumbsUp, ThumbsDown, File } from 'lucide-react';
|
||||||
import { MessageTypes } from '../../constants/messageTypes';
|
import { MessageTypes } from '../../constants/messageTypes';
|
||||||
import ExecutionStepsDisplay from './ExecutionStepsDisplay';
|
import ExecutionStepsDisplay from './ExecutionStepsDisplay';
|
||||||
|
import type { Message } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MessageRenderer 组件的 Props 类型
|
||||||
|
*/
|
||||||
|
interface MessageRendererProps {
|
||||||
|
/** 消息对象 */
|
||||||
|
message: Message;
|
||||||
|
/** 用户头像 URL */
|
||||||
|
userAvatar?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MessageRenderer - 消息渲染器组件
|
* MessageRenderer - 消息渲染器组件
|
||||||
*
|
*
|
||||||
* @param {Object} props
|
* 职责:
|
||||||
* @param {Object} props.message - 消息对象
|
* 1. 根据消息类型渲染不同样式的消息卡片
|
||||||
* @param {string} props.userAvatar - 用户头像 URL
|
* 2. 用户消息:右对齐,蓝紫渐变色
|
||||||
* @returns {JSX.Element|null}
|
* 3. AI 思考中:左对齐,加载动画
|
||||||
|
* 4. AI 回复:左对齐,毛玻璃效果,带交互按钮
|
||||||
|
* 5. 错误消息:居中,红色警告样式
|
||||||
*/
|
*/
|
||||||
const MessageRenderer = ({ message, userAvatar }) => {
|
const MessageRenderer: React.FC<MessageRendererProps> = ({ message, userAvatar }) => {
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
|
// 用户消息
|
||||||
case MessageTypes.USER:
|
case MessageTypes.USER:
|
||||||
return (
|
return (
|
||||||
<Flex justify="flex-end">
|
<Flex justify="flex-end">
|
||||||
@@ -78,6 +92,7 @@ const MessageRenderer = ({ message, userAvatar }) => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// AI 思考中
|
||||||
case MessageTypes.AGENT_THINKING:
|
case MessageTypes.AGENT_THINKING:
|
||||||
return (
|
return (
|
||||||
<Flex justify="flex-start">
|
<Flex justify="flex-start">
|
||||||
@@ -116,6 +131,7 @@ const MessageRenderer = ({ message, userAvatar }) => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// AI 回复
|
||||||
case MessageTypes.AGENT_RESPONSE:
|
case MessageTypes.AGENT_RESPONSE:
|
||||||
return (
|
return (
|
||||||
<Flex justify="flex-start">
|
<Flex justify="flex-start">
|
||||||
@@ -139,16 +155,19 @@ const MessageRenderer = ({ message, userAvatar }) => {
|
|||||||
boxShadow="0 8px 32px 0 rgba(31, 38, 135, 0.37)"
|
boxShadow="0 8px 32px 0 rgba(31, 38, 135, 0.37)"
|
||||||
>
|
>
|
||||||
<CardBody px={5} py={3}>
|
<CardBody px={5} py={3}>
|
||||||
|
{/* 消息内容 */}
|
||||||
<Text fontSize="sm" color="gray.100" whiteSpace="pre-wrap" lineHeight="relaxed">
|
<Text fontSize="sm" color="gray.100" whiteSpace="pre-wrap" lineHeight="relaxed">
|
||||||
{message.content}
|
{message.content}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{message.stepResults && message.stepResults.length > 0 && (
|
{/* 执行步骤(如果有) */}
|
||||||
|
{message.execution_results && message.execution_results.length > 0 && (
|
||||||
<Box mt={3}>
|
<Box mt={3}>
|
||||||
<ExecutionStepsDisplay steps={message.stepResults} plan={message.plan} />
|
<ExecutionStepsDisplay steps={message.execution_results} plan={message.plan} />
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 操作按钮栏 */}
|
||||||
<Flex
|
<Flex
|
||||||
align="center"
|
align="center"
|
||||||
gap={2}
|
gap={2}
|
||||||
@@ -157,6 +176,7 @@ const MessageRenderer = ({ message, userAvatar }) => {
|
|||||||
borderTop="1px solid"
|
borderTop="1px solid"
|
||||||
borderColor="rgba(255, 255, 255, 0.1)"
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
>
|
>
|
||||||
|
{/* 复制按钮 */}
|
||||||
<Tooltip label="复制">
|
<Tooltip label="复制">
|
||||||
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
|
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -164,6 +184,7 @@ const MessageRenderer = ({ message, userAvatar }) => {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
icon={<Copy className="w-4 h-4" />}
|
icon={<Copy className="w-4 h-4" />}
|
||||||
onClick={() => navigator.clipboard.writeText(message.content)}
|
onClick={() => navigator.clipboard.writeText(message.content)}
|
||||||
|
aria-label="复制消息"
|
||||||
bg="rgba(255, 255, 255, 0.05)"
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
color="gray.400"
|
color="gray.400"
|
||||||
_hover={{
|
_hover={{
|
||||||
@@ -173,12 +194,15 @@ const MessageRenderer = ({ message, userAvatar }) => {
|
|||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
{/* 点赞按钮 */}
|
||||||
<Tooltip label="点赞">
|
<Tooltip label="点赞">
|
||||||
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
|
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
icon={<ThumbsUp className="w-4 h-4" />}
|
icon={<ThumbsUp className="w-4 h-4" />}
|
||||||
|
aria-label="点赞"
|
||||||
bg="rgba(255, 255, 255, 0.05)"
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
color="gray.400"
|
color="gray.400"
|
||||||
_hover={{
|
_hover={{
|
||||||
@@ -189,12 +213,15 @@ const MessageRenderer = ({ message, userAvatar }) => {
|
|||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
{/* 点踩按钮 */}
|
||||||
<Tooltip label="点踩">
|
<Tooltip label="点踩">
|
||||||
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
|
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
icon={<ThumbsDown className="w-4 h-4" />}
|
icon={<ThumbsDown className="w-4 h-4" />}
|
||||||
|
aria-label="点踩"
|
||||||
bg="rgba(255, 255, 255, 0.05)"
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
color="gray.400"
|
color="gray.400"
|
||||||
_hover={{
|
_hover={{
|
||||||
@@ -205,11 +232,14 @@ const MessageRenderer = ({ message, userAvatar }) => {
|
|||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
{/* 时间戳 */}
|
||||||
<Text fontSize="xs" color="gray.500" ml="auto">
|
<Text fontSize="xs" color="gray.500" ml="auto">
|
||||||
{new Date(message.timestamp).toLocaleTimeString('zh-CN', {
|
{message.timestamp &&
|
||||||
hour: '2-digit',
|
new Date(message.timestamp).toLocaleTimeString('zh-CN', {
|
||||||
minute: '2-digit',
|
hour: '2-digit',
|
||||||
})}
|
minute: '2-digit',
|
||||||
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
@@ -219,6 +249,7 @@ const MessageRenderer = ({ message, userAvatar }) => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 错误消息
|
||||||
case MessageTypes.ERROR:
|
case MessageTypes.ERROR:
|
||||||
return (
|
return (
|
||||||
<Flex justify="center">
|
<Flex justify="center">
|
||||||
112
src/views/AgentChat/components/ChatArea/QuickQuestions.tsx
Normal file
112
src/views/AgentChat/components/ChatArea/QuickQuestions.tsx
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
// src/views/AgentChat/components/ChatArea/QuickQuestions.tsx
|
||||||
|
// 快捷问题卡片组件
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { Box, Button, HStack, Text } from '@chakra-ui/react';
|
||||||
|
import { Sparkles } from 'lucide-react';
|
||||||
|
import { quickQuestions } from '../../constants/quickQuestions';
|
||||||
|
import { animations } from '../../constants/animations';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* QuickQuestions 组件的 Props 类型
|
||||||
|
*/
|
||||||
|
interface QuickQuestionsProps {
|
||||||
|
/** 消息列表长度(用于判断是否显示) */
|
||||||
|
messagesCount: number;
|
||||||
|
/** 是否正在处理中 */
|
||||||
|
isProcessing: boolean;
|
||||||
|
/** 点击快捷问题回调 */
|
||||||
|
onQuestionClick: (text: string) => void;
|
||||||
|
/** 输入框引用(用于聚焦) */
|
||||||
|
inputRef: React.RefObject<HTMLInputElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* QuickQuestions - 快捷问题卡片组件
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* 1. 显示快捷问题卡片网格
|
||||||
|
* 2. 仅在消息少于 2 条且未处理时显示
|
||||||
|
* 3. 点击填充输入框并聚焦
|
||||||
|
*
|
||||||
|
* 设计:
|
||||||
|
* - 2 列网格布局
|
||||||
|
* - 毛玻璃效果卡片
|
||||||
|
* - 悬停缩放和高亮动画
|
||||||
|
* - 淡入上滑进入动画
|
||||||
|
*/
|
||||||
|
const QuickQuestions: React.FC<QuickQuestionsProps> = ({
|
||||||
|
messagesCount,
|
||||||
|
isProcessing,
|
||||||
|
onQuestionClick,
|
||||||
|
inputRef,
|
||||||
|
}) => {
|
||||||
|
// 只在消息少于 2 条且未处理时显示
|
||||||
|
if (messagesCount > 2 || isProcessing) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleQuestionClick = (text: string) => {
|
||||||
|
onQuestionClick(text);
|
||||||
|
inputRef.current?.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
<motion.div
|
||||||
|
variants={animations.fadeInUp}
|
||||||
|
initial="initial"
|
||||||
|
animate="animate"
|
||||||
|
exit={{ opacity: 0, y: 20 }}
|
||||||
|
>
|
||||||
|
<Box px={6}>
|
||||||
|
<Box maxW="896px" mx="auto">
|
||||||
|
{/* 标题 */}
|
||||||
|
<HStack fontSize="xs" color="gray.500" mb={2} fontWeight="medium" spacing={1}>
|
||||||
|
<Sparkles className="w-3 h-3" />
|
||||||
|
<Text>快速开始</Text>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
{/* 快捷问题网格 */}
|
||||||
|
<Box display="grid" gridTemplateColumns="repeat(2, 1fr)" gap={2}>
|
||||||
|
{quickQuestions.map((question, idx) => (
|
||||||
|
<motion.div
|
||||||
|
key={idx}
|
||||||
|
whileHover={{ scale: 1.02, y: -2 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
w="full"
|
||||||
|
justifyContent="flex-start"
|
||||||
|
h="auto"
|
||||||
|
py={3}
|
||||||
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
|
backdropFilter="blur(12px)"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
color="gray.300"
|
||||||
|
_hover={{
|
||||||
|
bg: 'rgba(59, 130, 246, 0.15)',
|
||||||
|
borderColor: 'blue.400',
|
||||||
|
boxShadow: '0 4px 12px rgba(59, 130, 246, 0.3)',
|
||||||
|
color: 'white',
|
||||||
|
}}
|
||||||
|
onClick={() => handleQuestionClick(question.text)}
|
||||||
|
>
|
||||||
|
<Text mr={2}>{question.emoji}</Text>
|
||||||
|
<Text>{question.text}</Text>
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QuickQuestions;
|
||||||
@@ -1,477 +0,0 @@
|
|||||||
// src/views/AgentChat/components/ChatArea/index.js
|
|
||||||
// 中间聊天区域组件
|
|
||||||
|
|
||||||
import React, { useRef, useEffect } from 'react';
|
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
Input,
|
|
||||||
Avatar,
|
|
||||||
Badge,
|
|
||||||
Tooltip,
|
|
||||||
IconButton,
|
|
||||||
Kbd,
|
|
||||||
HStack,
|
|
||||||
VStack,
|
|
||||||
Flex,
|
|
||||||
Text,
|
|
||||||
Tag,
|
|
||||||
TagLabel,
|
|
||||||
TagCloseButton,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import {
|
|
||||||
Send,
|
|
||||||
Menu,
|
|
||||||
RefreshCw,
|
|
||||||
Settings,
|
|
||||||
Cpu,
|
|
||||||
Zap,
|
|
||||||
Sparkles,
|
|
||||||
Paperclip,
|
|
||||||
Image as ImageIcon,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { AVAILABLE_MODELS } from '../../constants/models';
|
|
||||||
import { quickQuestions } from '../../constants/quickQuestions';
|
|
||||||
import { animations } from '../../constants/animations';
|
|
||||||
import MessageRenderer from './MessageRenderer';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ChatArea - 中间聊天区域组件
|
|
||||||
*
|
|
||||||
* @param {Object} props
|
|
||||||
* @param {Array} props.messages - 消息列表
|
|
||||||
* @param {string} props.inputValue - 输入框内容
|
|
||||||
* @param {Function} props.onInputChange - 输入框变化回调
|
|
||||||
* @param {boolean} props.isProcessing - 处理中状态
|
|
||||||
* @param {Function} props.onSendMessage - 发送消息回调
|
|
||||||
* @param {Function} props.onKeyPress - 键盘事件回调
|
|
||||||
* @param {Array} props.uploadedFiles - 已上传文件列表
|
|
||||||
* @param {Function} props.onFileSelect - 文件选择回调
|
|
||||||
* @param {Function} props.onFileRemove - 文件删除回调
|
|
||||||
* @param {string} props.selectedModel - 当前选中的模型 ID
|
|
||||||
* @param {boolean} props.isLeftSidebarOpen - 左侧栏是否展开
|
|
||||||
* @param {boolean} props.isRightSidebarOpen - 右侧栏是否展开
|
|
||||||
* @param {Function} props.onToggleLeftSidebar - 切换左侧栏回调
|
|
||||||
* @param {Function} props.onToggleRightSidebar - 切换右侧栏回调
|
|
||||||
* @param {Function} props.onNewSession - 新建会话回调
|
|
||||||
* @param {string} props.userAvatar - 用户头像 URL
|
|
||||||
* @param {RefObject} props.inputRef - 输入框引用
|
|
||||||
* @param {RefObject} props.fileInputRef - 文件上传输入引用
|
|
||||||
* @returns {JSX.Element}
|
|
||||||
*/
|
|
||||||
const ChatArea = ({
|
|
||||||
messages,
|
|
||||||
inputValue,
|
|
||||||
onInputChange,
|
|
||||||
isProcessing,
|
|
||||||
onSendMessage,
|
|
||||||
onKeyPress,
|
|
||||||
uploadedFiles,
|
|
||||||
onFileSelect,
|
|
||||||
onFileRemove,
|
|
||||||
selectedModel,
|
|
||||||
isLeftSidebarOpen,
|
|
||||||
isRightSidebarOpen,
|
|
||||||
onToggleLeftSidebar,
|
|
||||||
onToggleRightSidebar,
|
|
||||||
onNewSession,
|
|
||||||
userAvatar,
|
|
||||||
inputRef,
|
|
||||||
fileInputRef,
|
|
||||||
}) => {
|
|
||||||
// Auto-scroll 功能:当消息列表更新时,自动滚动到底部
|
|
||||||
const messagesEndRef = useRef(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
||||||
}, [messages]);
|
|
||||||
return (
|
|
||||||
<Flex flex={1} direction="column">
|
|
||||||
{/* 顶部标题栏 - 深色毛玻璃 */}
|
|
||||||
<Box
|
|
||||||
bg="rgba(17, 24, 39, 0.8)"
|
|
||||||
backdropFilter="blur(20px) saturate(180%)"
|
|
||||||
borderBottom="1px solid"
|
|
||||||
borderColor="rgba(255, 255, 255, 0.1)"
|
|
||||||
px={6}
|
|
||||||
py={4}
|
|
||||||
boxShadow="0 8px 32px 0 rgba(31, 38, 135, 0.37)"
|
|
||||||
>
|
|
||||||
<Flex align="center" justify="space-between">
|
|
||||||
<HStack spacing={4}>
|
|
||||||
{!isLeftSidebarOpen && (
|
|
||||||
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
|
|
||||||
<IconButton
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
icon={<Menu className="w-4 h-4" />}
|
|
||||||
onClick={onToggleLeftSidebar}
|
|
||||||
bg="rgba(255, 255, 255, 0.05)"
|
|
||||||
color="gray.400"
|
|
||||||
backdropFilter="blur(10px)"
|
|
||||||
border="1px solid"
|
|
||||||
borderColor="rgba(255, 255, 255, 0.1)"
|
|
||||||
_hover={{
|
|
||||||
bg: 'rgba(255, 255, 255, 0.1)',
|
|
||||||
color: 'white',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
animate={{ rotate: 360 }}
|
|
||||||
transition={{ duration: 3, repeat: Infinity, ease: 'linear' }}
|
|
||||||
>
|
|
||||||
<Avatar
|
|
||||||
icon={<Cpu className="w-6 h-6" />}
|
|
||||||
bgGradient="linear(to-br, purple.500, pink.500)"
|
|
||||||
boxShadow="0 0 20px rgba(236, 72, 153, 0.5)"
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<Text
|
|
||||||
fontSize="xl"
|
|
||||||
fontWeight="bold"
|
|
||||||
bgGradient="linear(to-r, blue.400, purple.400)"
|
|
||||||
bgClip="text"
|
|
||||||
letterSpacing="tight"
|
|
||||||
>
|
|
||||||
价小前投研 AI
|
|
||||||
</Text>
|
|
||||||
<HStack spacing={2} mt={1}>
|
|
||||||
<Badge
|
|
||||||
bgGradient="linear(to-r, green.500, teal.500)"
|
|
||||||
color="white"
|
|
||||||
px={2}
|
|
||||||
py={1}
|
|
||||||
borderRadius="md"
|
|
||||||
display="flex"
|
|
||||||
alignItems="center"
|
|
||||||
gap={1}
|
|
||||||
boxShadow="0 2px 8px rgba(16, 185, 129, 0.3)"
|
|
||||||
>
|
|
||||||
<Zap className="w-3 h-3" />
|
|
||||||
智能分析
|
|
||||||
</Badge>
|
|
||||||
<Badge
|
|
||||||
bgGradient="linear(to-r, purple.500, pink.500)"
|
|
||||||
color="white"
|
|
||||||
px={2}
|
|
||||||
py={1}
|
|
||||||
borderRadius="md"
|
|
||||||
boxShadow="0 2px 8px rgba(139, 92, 246, 0.3)"
|
|
||||||
>
|
|
||||||
{AVAILABLE_MODELS.find((m) => m.id === selectedModel)?.name}
|
|
||||||
</Badge>
|
|
||||||
</HStack>
|
|
||||||
</Box>
|
|
||||||
</HStack>
|
|
||||||
|
|
||||||
<HStack spacing={2}>
|
|
||||||
<Tooltip label="清空对话">
|
|
||||||
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
|
|
||||||
<IconButton
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
icon={<RefreshCw className="w-4 h-4" />}
|
|
||||||
onClick={onNewSession}
|
|
||||||
bg="rgba(255, 255, 255, 0.05)"
|
|
||||||
color="gray.400"
|
|
||||||
backdropFilter="blur(10px)"
|
|
||||||
border="1px solid"
|
|
||||||
borderColor="rgba(255, 255, 255, 0.1)"
|
|
||||||
_hover={{
|
|
||||||
bg: 'rgba(255, 255, 255, 0.1)',
|
|
||||||
color: 'white',
|
|
||||||
borderColor: 'purple.400',
|
|
||||||
boxShadow: '0 0 12px rgba(139, 92, 246, 0.3)',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
</Tooltip>
|
|
||||||
{!isRightSidebarOpen && (
|
|
||||||
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
|
|
||||||
<IconButton
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
icon={<Settings className="w-4 h-4" />}
|
|
||||||
onClick={onToggleRightSidebar}
|
|
||||||
bg="rgba(255, 255, 255, 0.05)"
|
|
||||||
color="gray.400"
|
|
||||||
backdropFilter="blur(10px)"
|
|
||||||
border="1px solid"
|
|
||||||
borderColor="rgba(255, 255, 255, 0.1)"
|
|
||||||
_hover={{
|
|
||||||
bg: 'rgba(255, 255, 255, 0.1)',
|
|
||||||
color: 'white',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</HStack>
|
|
||||||
</Flex>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* 消息列表 */}
|
|
||||||
<Box
|
|
||||||
flex={1}
|
|
||||||
bgGradient="linear(to-b, rgba(17, 24, 39, 0.5), rgba(17, 24, 39, 0.3))"
|
|
||||||
overflowY="auto"
|
|
||||||
>
|
|
||||||
<motion.div
|
|
||||||
style={{ maxWidth: '896px', margin: '0 auto' }}
|
|
||||||
variants={animations.staggerContainer}
|
|
||||||
initial="initial"
|
|
||||||
animate="animate"
|
|
||||||
>
|
|
||||||
<VStack spacing={4} align="stretch">
|
|
||||||
<AnimatePresence mode="popLayout">
|
|
||||||
{messages.map((message) => (
|
|
||||||
<motion.div
|
|
||||||
key={message.id}
|
|
||||||
variants={animations.fadeInUp}
|
|
||||||
initial="initial"
|
|
||||||
animate="animate"
|
|
||||||
exit={{ opacity: 0, y: -20 }}
|
|
||||||
layout
|
|
||||||
>
|
|
||||||
<MessageRenderer message={message} userAvatar={userAvatar} />
|
|
||||||
</motion.div>
|
|
||||||
))}
|
|
||||||
</AnimatePresence>
|
|
||||||
<div ref={messagesEndRef} />
|
|
||||||
</VStack>
|
|
||||||
</motion.div>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* 快捷问题 */}
|
|
||||||
<AnimatePresence>
|
|
||||||
{messages.length <= 2 && !isProcessing && (
|
|
||||||
<motion.div
|
|
||||||
variants={animations.fadeInUp}
|
|
||||||
initial="initial"
|
|
||||||
animate="animate"
|
|
||||||
exit={{ opacity: 0, y: 20 }}
|
|
||||||
>
|
|
||||||
<Box px={6}>
|
|
||||||
<Box maxW="896px" mx="auto">
|
|
||||||
<HStack fontSize="xs" color="gray.500" mb={2} fontWeight="medium" spacing={1}>
|
|
||||||
<Sparkles className="w-3 h-3" />
|
|
||||||
<Text>快速开始</Text>
|
|
||||||
</HStack>
|
|
||||||
<Box display="grid" gridTemplateColumns="repeat(2, 1fr)" gap={2}>
|
|
||||||
{quickQuestions.map((question, idx) => (
|
|
||||||
<motion.div
|
|
||||||
key={idx}
|
|
||||||
whileHover={{ scale: 1.02, y: -2 }}
|
|
||||||
whileTap={{ scale: 0.98 }}
|
|
||||||
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
w="full"
|
|
||||||
justifyContent="flex-start"
|
|
||||||
h="auto"
|
|
||||||
py={3}
|
|
||||||
bg="rgba(255, 255, 255, 0.05)"
|
|
||||||
backdropFilter="blur(12px)"
|
|
||||||
border="1px solid"
|
|
||||||
borderColor="rgba(255, 255, 255, 0.1)"
|
|
||||||
color="gray.300"
|
|
||||||
_hover={{
|
|
||||||
bg: 'rgba(59, 130, 246, 0.15)',
|
|
||||||
borderColor: 'blue.400',
|
|
||||||
boxShadow: '0 4px 12px rgba(59, 130, 246, 0.3)',
|
|
||||||
color: 'white',
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
onInputChange(question.text);
|
|
||||||
inputRef.current?.focus();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text mr={2}>{question.emoji}</Text>
|
|
||||||
<Text>{question.text}</Text>
|
|
||||||
</Button>
|
|
||||||
</motion.div>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
{/* 输入栏 - 深色毛玻璃 */}
|
|
||||||
<Box
|
|
||||||
bg="rgba(17, 24, 39, 0.8)"
|
|
||||||
backdropFilter="blur(20px) saturate(180%)"
|
|
||||||
borderTop="1px solid"
|
|
||||||
borderColor="rgba(255, 255, 255, 0.1)"
|
|
||||||
px={6}
|
|
||||||
py={1}
|
|
||||||
boxShadow="0 -8px 32px 0 rgba(31, 38, 135, 0.37)"
|
|
||||||
>
|
|
||||||
<Box maxW="896px" mx="auto">
|
|
||||||
{/* 已上传文件预览 */}
|
|
||||||
{uploadedFiles.length > 0 && (
|
|
||||||
<HStack mb={3} flexWrap="wrap" spacing={2}>
|
|
||||||
{uploadedFiles.map((file, idx) => (
|
|
||||||
<motion.div
|
|
||||||
key={idx}
|
|
||||||
initial={{ opacity: 0, scale: 0.8 }}
|
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.8 }}
|
|
||||||
>
|
|
||||||
<Tag
|
|
||||||
size="md"
|
|
||||||
variant="subtle"
|
|
||||||
bg="rgba(255, 255, 255, 0.05)"
|
|
||||||
backdropFilter="blur(10px)"
|
|
||||||
borderColor="rgba(255, 255, 255, 0.1)"
|
|
||||||
borderWidth={1}
|
|
||||||
>
|
|
||||||
<TagLabel color="gray.300">{file.name}</TagLabel>
|
|
||||||
<TagCloseButton onClick={() => onFileRemove(idx)} color="gray.400" />
|
|
||||||
</Tag>
|
|
||||||
</motion.div>
|
|
||||||
))}
|
|
||||||
</HStack>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<HStack spacing={2}>
|
|
||||||
<input
|
|
||||||
ref={fileInputRef}
|
|
||||||
type="file"
|
|
||||||
multiple
|
|
||||||
accept="image/*,.pdf,.doc,.docx,.txt"
|
|
||||||
onChange={onFileSelect}
|
|
||||||
style={{ display: 'none' }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Tooltip label="上传文件">
|
|
||||||
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
|
|
||||||
<IconButton
|
|
||||||
variant="ghost"
|
|
||||||
size="lg"
|
|
||||||
icon={<Paperclip className="w-5 h-5" />}
|
|
||||||
onClick={() => fileInputRef.current?.click()}
|
|
||||||
bg="rgba(255, 255, 255, 0.05)"
|
|
||||||
color="gray.300"
|
|
||||||
backdropFilter="blur(10px)"
|
|
||||||
border="1px solid"
|
|
||||||
borderColor="rgba(255, 255, 255, 0.1)"
|
|
||||||
_hover={{
|
|
||||||
bg: 'rgba(255, 255, 255, 0.1)',
|
|
||||||
borderColor: 'purple.400',
|
|
||||||
color: 'white',
|
|
||||||
boxShadow: '0 0 12px rgba(139, 92, 246, 0.3)',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip label="上传图片">
|
|
||||||
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
|
|
||||||
<IconButton
|
|
||||||
variant="ghost"
|
|
||||||
size="lg"
|
|
||||||
icon={<ImageIcon className="w-5 h-5" />}
|
|
||||||
onClick={() => {
|
|
||||||
fileInputRef.current?.setAttribute('accept', 'image/*');
|
|
||||||
fileInputRef.current?.click();
|
|
||||||
}}
|
|
||||||
bg="rgba(255, 255, 255, 0.05)"
|
|
||||||
color="gray.300"
|
|
||||||
backdropFilter="blur(10px)"
|
|
||||||
border="1px solid"
|
|
||||||
borderColor="rgba(255, 255, 255, 0.1)"
|
|
||||||
_hover={{
|
|
||||||
bg: 'rgba(255, 255, 255, 0.1)',
|
|
||||||
borderColor: 'purple.400',
|
|
||||||
color: 'white',
|
|
||||||
boxShadow: '0 0 12px rgba(139, 92, 246, 0.3)',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
ref={inputRef}
|
|
||||||
value={inputValue}
|
|
||||||
onChange={(e) => onInputChange(e.target.value)}
|
|
||||||
onKeyDown={onKeyPress}
|
|
||||||
placeholder="输入你的问题... (Enter 发送, Shift+Enter 换行)"
|
|
||||||
isDisabled={isProcessing}
|
|
||||||
size="lg"
|
|
||||||
variant="outline"
|
|
||||||
borderWidth={2}
|
|
||||||
bg="rgba(255, 255, 255, 0.05)"
|
|
||||||
backdropFilter="blur(10px)"
|
|
||||||
border="1px solid"
|
|
||||||
borderColor="rgba(255, 255, 255, 0.1)"
|
|
||||||
color="white"
|
|
||||||
_placeholder={{ color: 'gray.500' }}
|
|
||||||
_hover={{
|
|
||||||
borderColor: 'rgba(255, 255, 255, 0.2)',
|
|
||||||
}}
|
|
||||||
_focus={{
|
|
||||||
borderColor: 'purple.400',
|
|
||||||
boxShadow:
|
|
||||||
'0 0 0 1px var(--chakra-colors-purple-400), 0 0 12px rgba(139, 92, 246, 0.3)',
|
|
||||||
bg: 'rgba(255, 255, 255, 0.08)',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
whileHover={{ scale: 1.05 }}
|
|
||||||
whileTap={{ scale: 0.95 }}
|
|
||||||
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
size="lg"
|
|
||||||
icon={!isProcessing && <Send className="w-5 h-5" />}
|
|
||||||
onClick={onSendMessage}
|
|
||||||
isLoading={isProcessing}
|
|
||||||
isDisabled={!inputValue.trim() || isProcessing}
|
|
||||||
bgGradient="linear(to-r, blue.500, purple.600)"
|
|
||||||
color="white"
|
|
||||||
_hover={{
|
|
||||||
bgGradient: 'linear(to-r, blue.600, purple.700)',
|
|
||||||
boxShadow: '0 8px 20px rgba(139, 92, 246, 0.4)',
|
|
||||||
}}
|
|
||||||
_active={{
|
|
||||||
transform: 'translateY(0)',
|
|
||||||
boxShadow: '0 4px 12px rgba(139, 92, 246, 0.3)',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
</HStack>
|
|
||||||
|
|
||||||
<HStack spacing={4} mt={2} fontSize="xs" color="gray.500">
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Kbd bg="rgba(255, 255, 255, 0.05)" color="gray.400" borderColor="rgba(255, 255, 255, 0.1)">
|
|
||||||
Enter
|
|
||||||
</Kbd>
|
|
||||||
<Text>发送</Text>
|
|
||||||
</HStack>
|
|
||||||
<HStack spacing={1}>
|
|
||||||
<Kbd bg="rgba(255, 255, 255, 0.05)" color="gray.400" borderColor="rgba(255, 255, 255, 0.1)">
|
|
||||||
Shift
|
|
||||||
</Kbd>
|
|
||||||
<Text>+</Text>
|
|
||||||
<Kbd bg="rgba(255, 255, 255, 0.05)" color="gray.400" borderColor="rgba(255, 255, 255, 0.1)">
|
|
||||||
Enter
|
|
||||||
</Kbd>
|
|
||||||
<Text>换行</Text>
|
|
||||||
</HStack>
|
|
||||||
</HStack>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ChatArea;
|
|
||||||
141
src/views/AgentChat/components/ChatArea/index.tsx
Normal file
141
src/views/AgentChat/components/ChatArea/index.tsx
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
// src/views/AgentChat/components/ChatArea/index.tsx
|
||||||
|
// 中间聊天区域组件(重构版本)
|
||||||
|
|
||||||
|
import React, { useRef, useEffect } from 'react';
|
||||||
|
import { Flex } from '@chakra-ui/react';
|
||||||
|
import ChatHeader from './ChatHeader';
|
||||||
|
import MessageList from './MessageList';
|
||||||
|
import QuickQuestions from './QuickQuestions';
|
||||||
|
import ChatInput from './ChatInput';
|
||||||
|
import type { Message, UploadedFile } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ChatArea 组件的 Props 类型
|
||||||
|
*/
|
||||||
|
interface ChatAreaProps {
|
||||||
|
/** 消息列表 */
|
||||||
|
messages: Message[];
|
||||||
|
/** 输入框内容 */
|
||||||
|
inputValue: string;
|
||||||
|
/** 输入框变化回调 */
|
||||||
|
onInputChange: (value: string) => void;
|
||||||
|
/** 处理中状态 */
|
||||||
|
isProcessing: boolean;
|
||||||
|
/** 发送消息回调 */
|
||||||
|
onSendMessage: () => void;
|
||||||
|
/** 键盘事件回调 */
|
||||||
|
onKeyPress: (e: React.KeyboardEvent<HTMLInputElement>) => void;
|
||||||
|
/** 已上传文件列表 */
|
||||||
|
uploadedFiles: UploadedFile[];
|
||||||
|
/** 文件选择回调 */
|
||||||
|
onFileSelect: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
/** 文件删除回调 */
|
||||||
|
onFileRemove: (index: number) => void;
|
||||||
|
/** 当前选中的模型 ID */
|
||||||
|
selectedModel: string;
|
||||||
|
/** 左侧栏是否展开 */
|
||||||
|
isLeftSidebarOpen: boolean;
|
||||||
|
/** 右侧栏是否展开 */
|
||||||
|
isRightSidebarOpen: boolean;
|
||||||
|
/** 切换左侧栏回调 */
|
||||||
|
onToggleLeftSidebar: () => void;
|
||||||
|
/** 切换右侧栏回调 */
|
||||||
|
onToggleRightSidebar: () => void;
|
||||||
|
/** 新建会话回调 */
|
||||||
|
onNewSession: () => void;
|
||||||
|
/** 用户头像 URL */
|
||||||
|
userAvatar?: string;
|
||||||
|
/** 输入框引用 */
|
||||||
|
inputRef: React.RefObject<HTMLInputElement>;
|
||||||
|
/** 文件上传输入引用 */
|
||||||
|
fileInputRef: React.RefObject<HTMLInputElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ChatArea - 中间聊天区域组件(重构版本)
|
||||||
|
*
|
||||||
|
* 架构改进:
|
||||||
|
* - 顶部标题栏提取到 ChatHeader 组件(100 行)
|
||||||
|
* - 消息列表提取到 MessageList 组件(120 行)
|
||||||
|
* - 快捷问题提取到 QuickQuestions 组件(80 行)
|
||||||
|
* - 输入框区域提取到 ChatInput 组件(150 行)
|
||||||
|
* - MessageRenderer 和 ExecutionStepsDisplay 已迁移为 TS
|
||||||
|
*
|
||||||
|
* 主组件职责:
|
||||||
|
* 1. 管理消息列表自动滚动
|
||||||
|
* 2. 组合渲染所有子组件
|
||||||
|
* 3. 布局控制(Flex 容器)
|
||||||
|
*/
|
||||||
|
const ChatArea: React.FC<ChatAreaProps> = ({
|
||||||
|
messages,
|
||||||
|
inputValue,
|
||||||
|
onInputChange,
|
||||||
|
isProcessing,
|
||||||
|
onSendMessage,
|
||||||
|
onKeyPress,
|
||||||
|
uploadedFiles,
|
||||||
|
onFileSelect,
|
||||||
|
onFileRemove,
|
||||||
|
selectedModel,
|
||||||
|
isLeftSidebarOpen,
|
||||||
|
isRightSidebarOpen,
|
||||||
|
onToggleLeftSidebar,
|
||||||
|
onToggleRightSidebar,
|
||||||
|
onNewSession,
|
||||||
|
userAvatar,
|
||||||
|
inputRef,
|
||||||
|
fileInputRef,
|
||||||
|
}) => {
|
||||||
|
// ==================== 自动滚动功能 ====================
|
||||||
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
// ==================== 渲染组件 ====================
|
||||||
|
return (
|
||||||
|
<Flex flex={1} direction="column">
|
||||||
|
{/* 顶部标题栏 */}
|
||||||
|
<ChatHeader
|
||||||
|
selectedModel={selectedModel}
|
||||||
|
isLeftSidebarOpen={isLeftSidebarOpen}
|
||||||
|
isRightSidebarOpen={isRightSidebarOpen}
|
||||||
|
onToggleLeftSidebar={onToggleLeftSidebar}
|
||||||
|
onToggleRightSidebar={onToggleRightSidebar}
|
||||||
|
onNewSession={onNewSession}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 消息列表 */}
|
||||||
|
<MessageList
|
||||||
|
messages={messages}
|
||||||
|
userAvatar={userAvatar}
|
||||||
|
messagesEndRef={messagesEndRef}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 快捷问题(仅在消息少于 2 条时显示) */}
|
||||||
|
<QuickQuestions
|
||||||
|
messagesCount={messages.length}
|
||||||
|
isProcessing={isProcessing}
|
||||||
|
onQuestionClick={onInputChange}
|
||||||
|
inputRef={inputRef}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 输入框区域 */}
|
||||||
|
<ChatInput
|
||||||
|
inputValue={inputValue}
|
||||||
|
onInputChange={onInputChange}
|
||||||
|
onKeyPress={onKeyPress}
|
||||||
|
uploadedFiles={uploadedFiles}
|
||||||
|
onFileSelect={onFileSelect}
|
||||||
|
onFileRemove={onFileRemove}
|
||||||
|
isProcessing={isProcessing}
|
||||||
|
onSendMessage={onSendMessage}
|
||||||
|
inputRef={inputRef}
|
||||||
|
fileInputRef={fileInputRef}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChatArea;
|
||||||
44
src/views/AgentChat/components/ChatArea/types.ts
Normal file
44
src/views/AgentChat/components/ChatArea/types.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// src/views/AgentChat/components/ChatArea/types.ts
|
||||||
|
// ChatArea 组件的 TypeScript 类型定义
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传文件结构
|
||||||
|
*/
|
||||||
|
export interface UploadedFile {
|
||||||
|
name: string;
|
||||||
|
size?: number;
|
||||||
|
type?: string;
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行步骤结构
|
||||||
|
*/
|
||||||
|
export interface ExecutionStep {
|
||||||
|
tool_name: string;
|
||||||
|
status: 'success' | 'error' | 'pending';
|
||||||
|
execution_time?: number;
|
||||||
|
error?: string;
|
||||||
|
result?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息数据结构
|
||||||
|
*/
|
||||||
|
export interface Message {
|
||||||
|
id: string | number;
|
||||||
|
type: string; // 使用 MessageTypes 枚举
|
||||||
|
content: string;
|
||||||
|
timestamp?: string | Date;
|
||||||
|
files?: UploadedFile[];
|
||||||
|
execution_results?: ExecutionStep[];
|
||||||
|
plan?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快捷问题结构
|
||||||
|
*/
|
||||||
|
export interface QuickQuestion {
|
||||||
|
emoji: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
261
src/views/AgentChat/components/LeftSidebar/README.md
Normal file
261
src/views/AgentChat/components/LeftSidebar/README.md
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
# LeftSidebar 组件架构说明
|
||||||
|
|
||||||
|
## 📁 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
LeftSidebar/
|
||||||
|
├── index.tsx # 左侧栏主组件(200 行)- 组合层
|
||||||
|
├── types.ts # TypeScript 类型定义
|
||||||
|
├── SessionList.tsx # 会话列表组件(150 行)
|
||||||
|
├── SessionCard.js # 会话卡片组件(保留原有)
|
||||||
|
├── SessionSearchBar.tsx # 搜索框组件(60 行)
|
||||||
|
└── UserInfoCard.tsx # 用户信息卡片(80 行)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 重构目标
|
||||||
|
|
||||||
|
将原来 315 行的单文件组件拆分为多个职责明确的子组件,提高代码可维护性和可测试性。
|
||||||
|
|
||||||
|
## 🏗️ 组件职责
|
||||||
|
|
||||||
|
### 1. `index.tsx` - 主组件(约 200 行)
|
||||||
|
|
||||||
|
**职责**:
|
||||||
|
- 管理本地状态(搜索关键词)
|
||||||
|
- 数据处理(搜索过滤、日期分组)
|
||||||
|
- 布局组合(渲染标题栏、搜索框、会话列表、用户信息)
|
||||||
|
- 处理侧边栏动画
|
||||||
|
|
||||||
|
**Props**:
|
||||||
|
```typescript
|
||||||
|
interface LeftSidebarProps {
|
||||||
|
isOpen: boolean; // 侧边栏是否展开
|
||||||
|
onClose: () => void; // 关闭回调
|
||||||
|
sessions: Session[]; // 会话列表
|
||||||
|
currentSessionId: string | null; // 当前会话 ID
|
||||||
|
onSessionSwitch: (id: string) => void; // 切换会话
|
||||||
|
onNewSession: () => void; // 新建会话
|
||||||
|
isLoadingSessions: boolean; // 加载状态
|
||||||
|
user: UserInfo | null | undefined; // 用户信息
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. `SessionList.tsx` - 会话列表(约 150 行)
|
||||||
|
|
||||||
|
**职责**:
|
||||||
|
- 按日期分组渲染会话(今天、昨天、本周、更早)
|
||||||
|
- 处理加载状态和空状态
|
||||||
|
- 管理会话卡片的入场动画
|
||||||
|
|
||||||
|
**Props**:
|
||||||
|
```typescript
|
||||||
|
interface SessionListProps {
|
||||||
|
sessionGroups: SessionGroups; // 分组后的会话
|
||||||
|
currentSessionId: string | null; // 当前会话 ID
|
||||||
|
onSessionSwitch: (id: string) => void; // 切换会话
|
||||||
|
isLoadingSessions: boolean; // 加载状态
|
||||||
|
totalSessions: number; // 会话总数
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**特性**:
|
||||||
|
- "今天"分组的会话有渐进入场动画
|
||||||
|
- 其他分组无动画(性能优化)
|
||||||
|
- 空状态显示提示文案和图标
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. `SessionSearchBar.tsx` - 搜索框(约 60 行)
|
||||||
|
|
||||||
|
**职责**:
|
||||||
|
- 提供搜索输入框
|
||||||
|
- 显示搜索图标
|
||||||
|
- 处理输入变化事件
|
||||||
|
|
||||||
|
**Props**:
|
||||||
|
```typescript
|
||||||
|
interface SessionSearchBarProps {
|
||||||
|
value: string; // 搜索关键词
|
||||||
|
onChange: (value: string) => void; // 变化回调
|
||||||
|
placeholder?: string; // 占位符
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**设计**:
|
||||||
|
- 毛玻璃效果背景
|
||||||
|
- 聚焦时紫色发光边框
|
||||||
|
- 左侧搜索图标
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. `UserInfoCard.tsx` - 用户信息卡片(约 80 行)
|
||||||
|
|
||||||
|
**职责**:
|
||||||
|
- 展示用户头像和昵称
|
||||||
|
- 展示用户订阅类型徽章
|
||||||
|
- 处理未登录状态
|
||||||
|
|
||||||
|
**Props**:
|
||||||
|
```typescript
|
||||||
|
interface UserInfoCardProps {
|
||||||
|
user: UserInfo | null | undefined; // 用户信息
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**设计**:
|
||||||
|
- 头像使用渐变色背景和发光效果
|
||||||
|
- 订阅类型使用渐变色徽章
|
||||||
|
- 文本溢出时自动截断
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. `SessionCard.js` - 会话卡片(保留原有)
|
||||||
|
|
||||||
|
保留原有的 JavaScript 实现,作为原子组件被 SessionList 调用。
|
||||||
|
|
||||||
|
**未来可选优化**:迁移为 TypeScript。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. `types.ts` - 类型定义
|
||||||
|
|
||||||
|
**导出类型**:
|
||||||
|
```typescript
|
||||||
|
// 会话数据结构
|
||||||
|
interface Session {
|
||||||
|
session_id: string;
|
||||||
|
title?: string;
|
||||||
|
created_at?: string;
|
||||||
|
timestamp?: string;
|
||||||
|
message_count?: number;
|
||||||
|
updated_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按日期分组的会话
|
||||||
|
interface SessionGroups {
|
||||||
|
today: Session[];
|
||||||
|
yesterday: Session[];
|
||||||
|
thisWeek: Session[];
|
||||||
|
older: Session[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户信息
|
||||||
|
interface UserInfo {
|
||||||
|
avatar?: string;
|
||||||
|
nickname?: string;
|
||||||
|
subscription_type?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 数据流
|
||||||
|
|
||||||
|
```
|
||||||
|
LeftSidebar (index.tsx)
|
||||||
|
├─ 接收 sessions 数组
|
||||||
|
├─ 管理 searchQuery 状态
|
||||||
|
├─ 过滤和分组数据
|
||||||
|
│
|
||||||
|
├─→ SessionSearchBar
|
||||||
|
│ └─ 更新 searchQuery
|
||||||
|
│
|
||||||
|
├─→ SessionList
|
||||||
|
│ ├─ 接收 sessionGroups
|
||||||
|
│ └─→ SessionCard(循环渲染)
|
||||||
|
│ └─ 触发 onSessionSwitch
|
||||||
|
│
|
||||||
|
└─→ UserInfoCard
|
||||||
|
└─ 展示用户信息
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 依赖关系
|
||||||
|
|
||||||
|
- **外部依赖**:
|
||||||
|
- `@chakra-ui/react` - UI 组件库
|
||||||
|
- `framer-motion` - 动画库
|
||||||
|
- `lucide-react` - 图标库
|
||||||
|
|
||||||
|
- **内部依赖**:
|
||||||
|
- `../../constants/animations` - 动画配置
|
||||||
|
- `../../utils/sessionUtils` - 会话分组工具函数
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 设计特性
|
||||||
|
|
||||||
|
1. **毛玻璃效果**:
|
||||||
|
- `backdropFilter: blur(20px) saturate(180%)`
|
||||||
|
- 半透明背景 `rgba(17, 24, 39, 0.8)`
|
||||||
|
|
||||||
|
2. **渐变色**:
|
||||||
|
- 标题:蓝色到紫色渐变
|
||||||
|
- 订阅徽章:蓝色到紫色渐变
|
||||||
|
- 头像背景:蓝色到紫色渐变
|
||||||
|
|
||||||
|
3. **交互动画**:
|
||||||
|
- 按钮悬停:缩放 1.1x
|
||||||
|
- 按钮点击:缩放 0.9x
|
||||||
|
- 会话卡片悬停:缩放 1.02x + 上移 4px
|
||||||
|
|
||||||
|
4. **发光效果**:
|
||||||
|
- 头像发光:`0 0 12px rgba(139, 92, 246, 0.4)`
|
||||||
|
- 聚焦发光:`0 0 12px rgba(139, 92, 246, 0.3)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 重构优势
|
||||||
|
|
||||||
|
1. **可维护性提升**:
|
||||||
|
- 单文件从 315 行拆分为多个 60-200 行的小文件
|
||||||
|
- 每个组件职责单一,易于理解和修改
|
||||||
|
|
||||||
|
2. **可测试性提升**:
|
||||||
|
- 每个子组件可独立测试
|
||||||
|
- 纯展示组件(SessionCard、UserInfoCard)易于编写单元测试
|
||||||
|
|
||||||
|
3. **可复用性提升**:
|
||||||
|
- SessionSearchBar 可在其他地方复用
|
||||||
|
- UserInfoCard 可在其他侧边栏复用
|
||||||
|
|
||||||
|
4. **类型安全**:
|
||||||
|
- 使用 TypeScript 提供完整类型检查
|
||||||
|
- 统一的类型定义文件(types.ts)
|
||||||
|
|
||||||
|
5. **性能优化**:
|
||||||
|
- 拆分后的组件可独立优化(如 React.memo)
|
||||||
|
- 减少不必要的重新渲染
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 未来优化方向
|
||||||
|
|
||||||
|
1. **SessionCard 迁移为 TypeScript**
|
||||||
|
2. **添加单元测试**
|
||||||
|
3. **使用 React.memo 优化渲染性能**
|
||||||
|
4. **添加虚拟滚动(如会话超过 100 个)**
|
||||||
|
5. **支持拖拽排序会话**
|
||||||
|
6. **支持会话分组(自定义文件夹)**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 使用示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import LeftSidebar from '@views/AgentChat/components/LeftSidebar';
|
||||||
|
|
||||||
|
<LeftSidebar
|
||||||
|
isOpen={isLeftSidebarOpen}
|
||||||
|
onClose={() => setIsLeftSidebarOpen(false)}
|
||||||
|
sessions={sessions}
|
||||||
|
currentSessionId={currentSessionId}
|
||||||
|
onSessionSwitch={switchSession}
|
||||||
|
onNewSession={createNewSession}
|
||||||
|
isLoadingSessions={isLoadingSessions}
|
||||||
|
user={user}
|
||||||
|
/>
|
||||||
|
```
|
||||||
126
src/views/AgentChat/components/LeftSidebar/SessionList.tsx
Normal file
126
src/views/AgentChat/components/LeftSidebar/SessionList.tsx
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
// src/views/AgentChat/components/LeftSidebar/SessionList.tsx
|
||||||
|
// 会话列表组件 - 按日期分组显示会话
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { Box, Text, VStack, Flex, Spinner } from '@chakra-ui/react';
|
||||||
|
import { MessageSquare } from 'lucide-react';
|
||||||
|
import SessionCard from './SessionCard';
|
||||||
|
import type { Session, SessionGroups } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SessionList 组件的 Props 类型
|
||||||
|
*/
|
||||||
|
interface SessionListProps {
|
||||||
|
/** 按日期分组的会话对象 */
|
||||||
|
sessionGroups: SessionGroups;
|
||||||
|
/** 当前选中的会话 ID */
|
||||||
|
currentSessionId: string | null;
|
||||||
|
/** 切换会话回调 */
|
||||||
|
onSessionSwitch: (sessionId: string) => void;
|
||||||
|
/** 会话加载中状态 */
|
||||||
|
isLoadingSessions: boolean;
|
||||||
|
/** 会话总数(用于判断是否为空) */
|
||||||
|
totalSessions: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SessionList - 会话列表组件
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* 1. 按日期分组显示会话(今天、昨天、本周、更早)
|
||||||
|
* 2. 处理加载状态和空状态
|
||||||
|
* 3. 渲染会话卡片列表
|
||||||
|
*/
|
||||||
|
const SessionList: React.FC<SessionListProps> = ({
|
||||||
|
sessionGroups,
|
||||||
|
currentSessionId,
|
||||||
|
onSessionSwitch,
|
||||||
|
isLoadingSessions,
|
||||||
|
totalSessions,
|
||||||
|
}) => {
|
||||||
|
/**
|
||||||
|
* 渲染会话分组
|
||||||
|
* @param label - 分组标签(如"今天"、"昨天")
|
||||||
|
* @param sessions - 会话数组
|
||||||
|
* @param withAnimation - 是否应用入场动画(今天的会话有动画)
|
||||||
|
*/
|
||||||
|
const renderSessionGroup = (
|
||||||
|
label: string,
|
||||||
|
sessions: Session[],
|
||||||
|
withAnimation: boolean = false
|
||||||
|
): React.ReactNode => {
|
||||||
|
if (sessions.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box mb={4}>
|
||||||
|
<Text fontSize="xs" fontWeight="semibold" color="gray.500" mb={2} px={2}>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
<VStack spacing={2} align="stretch">
|
||||||
|
{sessions.map((session, idx) => {
|
||||||
|
const sessionCard = (
|
||||||
|
<SessionCard
|
||||||
|
session={session}
|
||||||
|
isActive={currentSessionId === session.session_id}
|
||||||
|
onPress={() => onSessionSwitch(session.session_id)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
// 今天的会话添加渐进入场动画
|
||||||
|
if (withAnimation) {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={session.session_id}
|
||||||
|
custom={idx}
|
||||||
|
initial={{ opacity: 0, x: -20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ delay: idx * 0.05 }}
|
||||||
|
>
|
||||||
|
{sessionCard}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他分组不添加动画
|
||||||
|
return <div key={session.session_id}>{sessionCard}</div>;
|
||||||
|
})}
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flex={1} p={3} overflowY="auto">
|
||||||
|
{/* 按日期分组显示会话 */}
|
||||||
|
{renderSessionGroup('今天', sessionGroups.today, true)}
|
||||||
|
{renderSessionGroup('昨天', sessionGroups.yesterday)}
|
||||||
|
{renderSessionGroup('本周', sessionGroups.thisWeek)}
|
||||||
|
{renderSessionGroup('更早', sessionGroups.older)}
|
||||||
|
|
||||||
|
{/* 加载状态 */}
|
||||||
|
{isLoadingSessions && (
|
||||||
|
<Flex justify="center" p={4}>
|
||||||
|
<Spinner
|
||||||
|
size="md"
|
||||||
|
color="purple.500"
|
||||||
|
emptyColor="gray.700"
|
||||||
|
thickness="3px"
|
||||||
|
speed="0.65s"
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 空状态 */}
|
||||||
|
{totalSessions === 0 && !isLoadingSessions && (
|
||||||
|
<VStack textAlign="center" py={8} color="gray.500" fontSize="sm" spacing={2}>
|
||||||
|
<MessageSquare className="w-8 h-8" style={{ opacity: 0.5, margin: '0 auto' }} />
|
||||||
|
<Text>还没有对话历史</Text>
|
||||||
|
<Text fontSize="xs">开始一个新对话吧!</Text>
|
||||||
|
</VStack>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SessionList;
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
// src/views/AgentChat/components/LeftSidebar/SessionSearchBar.tsx
|
||||||
|
// 会话搜索框组件
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Box, Input } from '@chakra-ui/react';
|
||||||
|
import { Search } from 'lucide-react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SessionSearchBar 组件的 Props 类型
|
||||||
|
*/
|
||||||
|
interface SessionSearchBarProps {
|
||||||
|
/** 搜索关键词 */
|
||||||
|
value: string;
|
||||||
|
/** 搜索关键词变化回调 */
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
/** 占位符文本 */
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SessionSearchBar - 会话搜索框组件
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* 1. 提供搜索输入框
|
||||||
|
* 2. 显示搜索图标
|
||||||
|
* 3. 处理输入变化事件
|
||||||
|
*
|
||||||
|
* 设计:
|
||||||
|
* - 毛玻璃效果背景
|
||||||
|
* - 聚焦时紫色发光边框
|
||||||
|
* - 左侧搜索图标
|
||||||
|
*/
|
||||||
|
const SessionSearchBar: React.FC<SessionSearchBarProps> = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder = '搜索对话...',
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Box position="relative">
|
||||||
|
{/* 搜索图标 */}
|
||||||
|
<Box position="absolute" left={3} top="50%" transform="translateY(-50%)" zIndex={1}>
|
||||||
|
<Search className="w-4 h-4" color="#9CA3AF" />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 搜索输入框 */}
|
||||||
|
<Input
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
pl={10}
|
||||||
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
|
backdropFilter="blur(10px)"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
color="white"
|
||||||
|
_placeholder={{ color: 'gray.500' }}
|
||||||
|
_hover={{
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
}}
|
||||||
|
_focus={{
|
||||||
|
borderColor: 'purple.400',
|
||||||
|
boxShadow: '0 0 0 1px var(--chakra-colors-purple-400), 0 0 12px rgba(139, 92, 246, 0.3)',
|
||||||
|
bg: 'rgba(255, 255, 255, 0.08)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SessionSearchBar;
|
||||||
68
src/views/AgentChat/components/LeftSidebar/UserInfoCard.tsx
Normal file
68
src/views/AgentChat/components/LeftSidebar/UserInfoCard.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
// src/views/AgentChat/components/LeftSidebar/UserInfoCard.tsx
|
||||||
|
// 用户信息卡片组件
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Box, HStack, Avatar, Text, Badge } from '@chakra-ui/react';
|
||||||
|
import type { UserInfo } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserInfoCard 组件的 Props 类型
|
||||||
|
*/
|
||||||
|
interface UserInfoCardProps {
|
||||||
|
/** 用户信息 */
|
||||||
|
user: UserInfo | null | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserInfoCard - 用户信息卡片组件
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* 1. 展示用户头像和昵称
|
||||||
|
* 2. 展示用户订阅类型徽章
|
||||||
|
* 3. 处理未登录状态
|
||||||
|
*
|
||||||
|
* 设计:
|
||||||
|
* - 头像使用渐变色背景和发光效果
|
||||||
|
* - 订阅类型使用渐变色徽章
|
||||||
|
* - 文本溢出时自动截断
|
||||||
|
*/
|
||||||
|
const UserInfoCard: React.FC<UserInfoCardProps> = ({ user }) => {
|
||||||
|
return (
|
||||||
|
<Box p={4} borderTop="1px solid" borderColor="rgba(255, 255, 255, 0.1)">
|
||||||
|
<HStack spacing={3}>
|
||||||
|
{/* 用户头像 */}
|
||||||
|
<Avatar
|
||||||
|
src={user?.avatar}
|
||||||
|
name={user?.nickname}
|
||||||
|
size="sm"
|
||||||
|
bgGradient="linear(to-br, blue.500, purple.600)"
|
||||||
|
boxShadow="0 0 12px rgba(139, 92, 246, 0.4)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 用户信息 */}
|
||||||
|
<Box flex={1} minW={0}>
|
||||||
|
{/* 用户昵称 */}
|
||||||
|
<Text fontSize="sm" fontWeight="medium" color="gray.100" noOfLines={1}>
|
||||||
|
{user?.nickname || '未登录'}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* 订阅类型徽章 */}
|
||||||
|
<Badge
|
||||||
|
bgGradient="linear(to-r, blue.500, purple.500)"
|
||||||
|
color="white"
|
||||||
|
px={2}
|
||||||
|
py={0.5}
|
||||||
|
borderRadius="full"
|
||||||
|
fontSize="xs"
|
||||||
|
fontWeight="semibold"
|
||||||
|
textTransform="none"
|
||||||
|
>
|
||||||
|
{user?.subscription_type || 'free'}
|
||||||
|
</Badge>
|
||||||
|
</Box>
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserInfoCard;
|
||||||
@@ -1,314 +0,0 @@
|
|||||||
// src/views/AgentChat/components/LeftSidebar/index.js
|
|
||||||
// 左侧栏组件 - 对话历史列表
|
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Text,
|
|
||||||
Input,
|
|
||||||
Avatar,
|
|
||||||
Badge,
|
|
||||||
Spinner,
|
|
||||||
Tooltip,
|
|
||||||
IconButton,
|
|
||||||
HStack,
|
|
||||||
VStack,
|
|
||||||
Flex,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import { MessageSquare, Plus, Search, ChevronLeft } from 'lucide-react';
|
|
||||||
import { animations } from '../../constants/animations';
|
|
||||||
import { groupSessionsByDate } from '../../utils/sessionUtils';
|
|
||||||
import SessionCard from './SessionCard';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* LeftSidebar - 左侧栏组件
|
|
||||||
*
|
|
||||||
* @param {Object} props
|
|
||||||
* @param {boolean} props.isOpen - 侧边栏是否展开
|
|
||||||
* @param {Function} props.onClose - 关闭侧边栏回调
|
|
||||||
* @param {Array} props.sessions - 会话列表
|
|
||||||
* @param {string|null} props.currentSessionId - 当前选中的会话 ID
|
|
||||||
* @param {Function} props.onSessionSwitch - 切换会话回调
|
|
||||||
* @param {Function} props.onNewSession - 新建会话回调
|
|
||||||
* @param {boolean} props.isLoadingSessions - 会话加载中状态
|
|
||||||
* @param {Object} props.user - 用户信息
|
|
||||||
* @returns {JSX.Element|null}
|
|
||||||
*/
|
|
||||||
const LeftSidebar = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
sessions,
|
|
||||||
currentSessionId,
|
|
||||||
onSessionSwitch,
|
|
||||||
onNewSession,
|
|
||||||
isLoadingSessions,
|
|
||||||
user,
|
|
||||||
}) => {
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
|
||||||
|
|
||||||
// 按日期分组会话
|
|
||||||
const sessionGroups = groupSessionsByDate(sessions);
|
|
||||||
|
|
||||||
// 搜索过滤
|
|
||||||
const filteredSessions = searchQuery
|
|
||||||
? sessions.filter(
|
|
||||||
(s) =>
|
|
||||||
s.title?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
||||||
s.session_id?.toLowerCase().includes(searchQuery.toLowerCase())
|
|
||||||
)
|
|
||||||
: sessions;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AnimatePresence>
|
|
||||||
{isOpen && (
|
|
||||||
<motion.div
|
|
||||||
style={{ width: '320px', display: 'flex', flexDirection: 'column' }}
|
|
||||||
initial="initial"
|
|
||||||
animate="animate"
|
|
||||||
exit="exit"
|
|
||||||
variants={animations.slideInLeft}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
w="320px"
|
|
||||||
h="100%"
|
|
||||||
display="flex"
|
|
||||||
flexDirection="column"
|
|
||||||
bg="rgba(17, 24, 39, 0.8)"
|
|
||||||
backdropFilter="blur(20px) saturate(180%)"
|
|
||||||
borderRight="1px solid"
|
|
||||||
borderColor="rgba(255, 255, 255, 0.1)"
|
|
||||||
boxShadow="4px 0 24px rgba(0, 0, 0, 0.3)"
|
|
||||||
>
|
|
||||||
{/* 标题栏 */}
|
|
||||||
<Box p={4} borderBottom="1px solid" borderColor="rgba(255, 255, 255, 0.1)">
|
|
||||||
<HStack justify="space-between" mb={3}>
|
|
||||||
<HStack spacing={2}>
|
|
||||||
<MessageSquare className="w-5 h-5" color="#60A5FA" />
|
|
||||||
<Text
|
|
||||||
fontWeight="semibold"
|
|
||||||
bgGradient="linear(to-r, blue.300, purple.300)"
|
|
||||||
bgClip="text"
|
|
||||||
fontSize="md"
|
|
||||||
>
|
|
||||||
对话历史
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
<HStack spacing={2}>
|
|
||||||
<Tooltip label="新建对话">
|
|
||||||
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
|
|
||||||
<IconButton
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
icon={<Plus className="w-4 h-4" />}
|
|
||||||
onClick={onNewSession}
|
|
||||||
bg="rgba(255, 255, 255, 0.05)"
|
|
||||||
color="gray.300"
|
|
||||||
backdropFilter="blur(10px)"
|
|
||||||
border="1px solid"
|
|
||||||
borderColor="rgba(255, 255, 255, 0.1)"
|
|
||||||
_hover={{
|
|
||||||
bg: 'rgba(59, 130, 246, 0.2)',
|
|
||||||
borderColor: 'blue.400',
|
|
||||||
color: 'blue.300',
|
|
||||||
boxShadow: '0 0 12px rgba(59, 130, 246, 0.3)',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip label="收起侧边栏">
|
|
||||||
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
|
|
||||||
<IconButton
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
icon={<ChevronLeft className="w-4 h-4" />}
|
|
||||||
onClick={onClose}
|
|
||||||
bg="rgba(255, 255, 255, 0.05)"
|
|
||||||
color="gray.300"
|
|
||||||
backdropFilter="blur(10px)"
|
|
||||||
border="1px solid"
|
|
||||||
borderColor="rgba(255, 255, 255, 0.1)"
|
|
||||||
_hover={{
|
|
||||||
bg: 'rgba(255, 255, 255, 0.1)',
|
|
||||||
borderColor: 'purple.400',
|
|
||||||
color: 'white',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
</Tooltip>
|
|
||||||
</HStack>
|
|
||||||
</HStack>
|
|
||||||
|
|
||||||
{/* 搜索框 */}
|
|
||||||
<Box position="relative">
|
|
||||||
<Box position="absolute" left={3} top="50%" transform="translateY(-50%)" zIndex={1}>
|
|
||||||
<Search className="w-4 h-4" color="#9CA3AF" />
|
|
||||||
</Box>
|
|
||||||
<Input
|
|
||||||
placeholder="搜索对话..."
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
pl={10}
|
|
||||||
bg="rgba(255, 255, 255, 0.05)"
|
|
||||||
backdropFilter="blur(10px)"
|
|
||||||
border="1px solid"
|
|
||||||
borderColor="rgba(255, 255, 255, 0.1)"
|
|
||||||
color="white"
|
|
||||||
_placeholder={{ color: 'gray.500' }}
|
|
||||||
_hover={{
|
|
||||||
borderColor: 'rgba(255, 255, 255, 0.2)',
|
|
||||||
}}
|
|
||||||
_focus={{
|
|
||||||
borderColor: 'purple.400',
|
|
||||||
boxShadow:
|
|
||||||
'0 0 0 1px var(--chakra-colors-purple-400), 0 0 12px rgba(139, 92, 246, 0.3)',
|
|
||||||
bg: 'rgba(255, 255, 255, 0.08)',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* 会话列表 */}
|
|
||||||
<Box flex={1} p={3} overflowY="auto">
|
|
||||||
{/* 按日期分组显示会话 */}
|
|
||||||
{sessionGroups.today.length > 0 && (
|
|
||||||
<Box mb={4}>
|
|
||||||
<Text fontSize="xs" fontWeight="semibold" color="gray.500" mb={2} px={2}>
|
|
||||||
今天
|
|
||||||
</Text>
|
|
||||||
<VStack spacing={2} align="stretch">
|
|
||||||
{sessionGroups.today.map((session, idx) => (
|
|
||||||
<motion.div
|
|
||||||
key={session.session_id}
|
|
||||||
custom={idx}
|
|
||||||
initial={{ opacity: 0, x: -20 }}
|
|
||||||
animate={{ opacity: 1, x: 0 }}
|
|
||||||
transition={{ delay: idx * 0.05 }}
|
|
||||||
>
|
|
||||||
<SessionCard
|
|
||||||
session={session}
|
|
||||||
isActive={currentSessionId === session.session_id}
|
|
||||||
onPress={() => onSessionSwitch(session.session_id)}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
))}
|
|
||||||
</VStack>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{sessionGroups.yesterday.length > 0 && (
|
|
||||||
<Box mb={4}>
|
|
||||||
<Text fontSize="xs" fontWeight="semibold" color="gray.500" mb={2} px={2}>
|
|
||||||
昨天
|
|
||||||
</Text>
|
|
||||||
<VStack spacing={2} align="stretch">
|
|
||||||
{sessionGroups.yesterday.map((session) => (
|
|
||||||
<SessionCard
|
|
||||||
key={session.session_id}
|
|
||||||
session={session}
|
|
||||||
isActive={currentSessionId === session.session_id}
|
|
||||||
onPress={() => onSessionSwitch(session.session_id)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</VStack>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{sessionGroups.thisWeek.length > 0 && (
|
|
||||||
<Box mb={4}>
|
|
||||||
<Text fontSize="xs" fontWeight="semibold" color="gray.500" mb={2} px={2}>
|
|
||||||
本周
|
|
||||||
</Text>
|
|
||||||
<VStack spacing={2} align="stretch">
|
|
||||||
{sessionGroups.thisWeek.map((session) => (
|
|
||||||
<SessionCard
|
|
||||||
key={session.session_id}
|
|
||||||
session={session}
|
|
||||||
isActive={currentSessionId === session.session_id}
|
|
||||||
onPress={() => onSessionSwitch(session.session_id)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</VStack>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{sessionGroups.older.length > 0 && (
|
|
||||||
<Box mb={4}>
|
|
||||||
<Text fontSize="xs" fontWeight="semibold" color="gray.500" mb={2} px={2}>
|
|
||||||
更早
|
|
||||||
</Text>
|
|
||||||
<VStack spacing={2} align="stretch">
|
|
||||||
{sessionGroups.older.map((session) => (
|
|
||||||
<SessionCard
|
|
||||||
key={session.session_id}
|
|
||||||
session={session}
|
|
||||||
isActive={currentSessionId === session.session_id}
|
|
||||||
onPress={() => onSessionSwitch(session.session_id)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</VStack>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 加载状态 */}
|
|
||||||
{isLoadingSessions && (
|
|
||||||
<Flex justify="center" p={4}>
|
|
||||||
<Spinner
|
|
||||||
size="md"
|
|
||||||
color="purple.500"
|
|
||||||
emptyColor="gray.700"
|
|
||||||
thickness="3px"
|
|
||||||
speed="0.65s"
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 空状态 */}
|
|
||||||
{sessions.length === 0 && !isLoadingSessions && (
|
|
||||||
<VStack textAlign="center" py={8} color="gray.500" fontSize="sm" spacing={2}>
|
|
||||||
<MessageSquare className="w-8 h-8" style={{ opacity: 0.5, margin: '0 auto' }} />
|
|
||||||
<Text>还没有对话历史</Text>
|
|
||||||
<Text fontSize="xs">开始一个新对话吧!</Text>
|
|
||||||
</VStack>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* 用户信息卡片 */}
|
|
||||||
<Box p={4} borderTop="1px solid" borderColor="rgba(255, 255, 255, 0.1)">
|
|
||||||
<HStack spacing={3}>
|
|
||||||
<Avatar
|
|
||||||
src={user?.avatar}
|
|
||||||
name={user?.nickname}
|
|
||||||
size="sm"
|
|
||||||
bgGradient="linear(to-br, blue.500, purple.600)"
|
|
||||||
boxShadow="0 0 12px rgba(139, 92, 246, 0.4)"
|
|
||||||
/>
|
|
||||||
<Box flex={1} minW={0}>
|
|
||||||
<Text fontSize="sm" fontWeight="medium" color="gray.100" noOfLines={1}>
|
|
||||||
{user?.nickname || '未登录'}
|
|
||||||
</Text>
|
|
||||||
<Badge
|
|
||||||
bgGradient="linear(to-r, blue.500, purple.500)"
|
|
||||||
color="white"
|
|
||||||
px={2}
|
|
||||||
py={0.5}
|
|
||||||
borderRadius="full"
|
|
||||||
fontSize="xs"
|
|
||||||
fontWeight="semibold"
|
|
||||||
textTransform="none"
|
|
||||||
>
|
|
||||||
{user?.subscription_type || 'free'}
|
|
||||||
</Badge>
|
|
||||||
</Box>
|
|
||||||
</HStack>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LeftSidebar;
|
|
||||||
197
src/views/AgentChat/components/LeftSidebar/index.tsx
Normal file
197
src/views/AgentChat/components/LeftSidebar/index.tsx
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
// src/views/AgentChat/components/LeftSidebar/index.tsx
|
||||||
|
// 左侧栏组件 - 对话历史列表(重构版本)
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Text,
|
||||||
|
IconButton,
|
||||||
|
HStack,
|
||||||
|
Tooltip,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { MessageSquare, Plus, ChevronLeft } from 'lucide-react';
|
||||||
|
import { animations } from '../../constants/animations';
|
||||||
|
import { groupSessionsByDate } from '../../utils/sessionUtils';
|
||||||
|
import SessionSearchBar from './SessionSearchBar';
|
||||||
|
import SessionList from './SessionList';
|
||||||
|
import UserInfoCard from './UserInfoCard';
|
||||||
|
import type { Session, UserInfo } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LeftSidebar 组件的 Props 类型
|
||||||
|
*/
|
||||||
|
interface LeftSidebarProps {
|
||||||
|
/** 侧边栏是否展开 */
|
||||||
|
isOpen: boolean;
|
||||||
|
/** 关闭侧边栏回调 */
|
||||||
|
onClose: () => void;
|
||||||
|
/** 会话列表 */
|
||||||
|
sessions: Session[];
|
||||||
|
/** 当前选中的会话 ID */
|
||||||
|
currentSessionId: string | null;
|
||||||
|
/** 切换会话回调 */
|
||||||
|
onSessionSwitch: (sessionId: string) => void;
|
||||||
|
/** 新建会话回调 */
|
||||||
|
onNewSession: () => void;
|
||||||
|
/** 会话加载中状态 */
|
||||||
|
isLoadingSessions: boolean;
|
||||||
|
/** 用户信息 */
|
||||||
|
user: UserInfo | null | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LeftSidebar - 左侧栏组件(重构版本)
|
||||||
|
*
|
||||||
|
* 架构改进:
|
||||||
|
* - 将会话列表逻辑提取到 SessionList 组件(150 行)
|
||||||
|
* - 将用户信息卡片提取到 UserInfoCard 组件(80 行)
|
||||||
|
* - 将搜索框提取到 SessionSearchBar 组件(60 行)
|
||||||
|
* - 主组件只负责状态管理和布局组合(200 行)
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* 1. 管理搜索状态
|
||||||
|
* 2. 过滤和分组会话数据
|
||||||
|
* 3. 组合渲染子组件
|
||||||
|
* 4. 处理侧边栏动画
|
||||||
|
*/
|
||||||
|
const LeftSidebar: React.FC<LeftSidebarProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
sessions,
|
||||||
|
currentSessionId,
|
||||||
|
onSessionSwitch,
|
||||||
|
onNewSession,
|
||||||
|
isLoadingSessions,
|
||||||
|
user,
|
||||||
|
}) => {
|
||||||
|
// ==================== 本地状态 ====================
|
||||||
|
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||||
|
|
||||||
|
// ==================== 数据处理 ====================
|
||||||
|
|
||||||
|
// 搜索过滤
|
||||||
|
const filteredSessions = searchQuery
|
||||||
|
? sessions.filter(
|
||||||
|
(s) =>
|
||||||
|
s.title?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
s.session_id?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
)
|
||||||
|
: sessions;
|
||||||
|
|
||||||
|
// 按日期分组会话
|
||||||
|
const sessionGroups = groupSessionsByDate(filteredSessions);
|
||||||
|
|
||||||
|
// ==================== 渲染组件 ====================
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{isOpen && (
|
||||||
|
<motion.div
|
||||||
|
style={{ width: '320px', display: 'flex', flexDirection: 'column' }}
|
||||||
|
initial="initial"
|
||||||
|
animate="animate"
|
||||||
|
exit="exit"
|
||||||
|
variants={animations.slideInLeft}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
w="320px"
|
||||||
|
h="100%"
|
||||||
|
display="flex"
|
||||||
|
flexDirection="column"
|
||||||
|
bg="rgba(17, 24, 39, 0.8)"
|
||||||
|
backdropFilter="blur(20px) saturate(180%)"
|
||||||
|
borderRight="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
boxShadow="4px 0 24px rgba(0, 0, 0, 0.3)"
|
||||||
|
>
|
||||||
|
{/* ==================== 标题栏 ==================== */}
|
||||||
|
<Box p={4} borderBottom="1px solid" borderColor="rgba(255, 255, 255, 0.1)">
|
||||||
|
{/* 标题和操作按钮 */}
|
||||||
|
<HStack justify="space-between" mb={3}>
|
||||||
|
{/* 左侧:标题 */}
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<MessageSquare className="w-5 h-5" color="#60A5FA" />
|
||||||
|
<Text
|
||||||
|
fontWeight="semibold"
|
||||||
|
bgGradient="linear(to-r, blue.300, purple.300)"
|
||||||
|
bgClip="text"
|
||||||
|
fontSize="md"
|
||||||
|
>
|
||||||
|
对话历史
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
{/* 右侧:新建对话 + 收起按钮 */}
|
||||||
|
<HStack spacing={2}>
|
||||||
|
{/* 新建对话按钮 */}
|
||||||
|
<Tooltip label="新建对话">
|
||||||
|
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
icon={<Plus className="w-4 h-4" />}
|
||||||
|
onClick={onNewSession}
|
||||||
|
aria-label="新建对话"
|
||||||
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
|
color="gray.300"
|
||||||
|
backdropFilter="blur(10px)"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
_hover={{
|
||||||
|
bg: 'rgba(59, 130, 246, 0.2)',
|
||||||
|
borderColor: 'blue.400',
|
||||||
|
color: 'blue.300',
|
||||||
|
boxShadow: '0 0 12px rgba(59, 130, 246, 0.3)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{/* 收起侧边栏按钮 */}
|
||||||
|
<Tooltip label="收起侧边栏">
|
||||||
|
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
icon={<ChevronLeft className="w-4 h-4" />}
|
||||||
|
onClick={onClose}
|
||||||
|
aria-label="收起侧边栏"
|
||||||
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
|
color="gray.300"
|
||||||
|
backdropFilter="blur(10px)"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
_hover={{
|
||||||
|
bg: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
borderColor: 'purple.400',
|
||||||
|
color: 'white',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</Tooltip>
|
||||||
|
</HStack>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
{/* 搜索框组件 */}
|
||||||
|
<SessionSearchBar value={searchQuery} onChange={setSearchQuery} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* ==================== 会话列表组件 ==================== */}
|
||||||
|
<SessionList
|
||||||
|
sessionGroups={sessionGroups}
|
||||||
|
currentSessionId={currentSessionId}
|
||||||
|
onSessionSwitch={onSessionSwitch}
|
||||||
|
isLoadingSessions={isLoadingSessions}
|
||||||
|
totalSessions={sessions.length}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* ==================== 用户信息卡片组件 ==================== */}
|
||||||
|
<UserInfoCard user={user} />
|
||||||
|
</Box>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LeftSidebar;
|
||||||
33
src/views/AgentChat/components/LeftSidebar/types.ts
Normal file
33
src/views/AgentChat/components/LeftSidebar/types.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
// src/views/AgentChat/components/LeftSidebar/types.ts
|
||||||
|
// LeftSidebar 组件的 TypeScript 类型定义
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话数据结构
|
||||||
|
*/
|
||||||
|
export interface Session {
|
||||||
|
session_id: string;
|
||||||
|
title?: string;
|
||||||
|
created_at?: string;
|
||||||
|
timestamp?: string;
|
||||||
|
message_count?: number;
|
||||||
|
updated_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按日期分组的会话数据
|
||||||
|
*/
|
||||||
|
export interface SessionGroups {
|
||||||
|
today: Session[];
|
||||||
|
yesterday: Session[];
|
||||||
|
thisWeek: Session[];
|
||||||
|
older: Session[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户信息
|
||||||
|
*/
|
||||||
|
export interface UserInfo {
|
||||||
|
avatar?: string;
|
||||||
|
nickname?: string;
|
||||||
|
subscription_type?: string;
|
||||||
|
}
|
||||||
118
src/views/AgentChat/components/RightSidebar/ModelSelector.tsx
Normal file
118
src/views/AgentChat/components/RightSidebar/ModelSelector.tsx
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
// src/views/AgentChat/components/RightSidebar/ModelSelector.tsx
|
||||||
|
// 模型选择组件
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
HStack,
|
||||||
|
VStack,
|
||||||
|
Box,
|
||||||
|
Text,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { Check } from 'lucide-react';
|
||||||
|
import { AVAILABLE_MODELS } from '../../constants/models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ModelSelector 组件的 Props 类型
|
||||||
|
*/
|
||||||
|
interface ModelSelectorProps {
|
||||||
|
/** 当前选中的模型 ID */
|
||||||
|
selectedModel: string;
|
||||||
|
/** 模型切换回调 */
|
||||||
|
onModelChange: (modelId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ModelSelector - 模型选择组件
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* 1. 渲染模型选择卡片列表
|
||||||
|
* 2. 显示模型图标、名称、描述
|
||||||
|
* 3. 高亮当前选中模型(紫色边框 + 发光效果)
|
||||||
|
* 4. 显示选中标记(Check 图标 + 弹簧动画)
|
||||||
|
*
|
||||||
|
* 设计特性:
|
||||||
|
* - 卡片渐进入场动画(延迟 `idx * 0.1`)
|
||||||
|
* - 悬停缩放 1.02x + 上移 2px
|
||||||
|
* - 选中态:紫色边框 + 发光效果
|
||||||
|
* - 模型图标渐变色背景
|
||||||
|
*/
|
||||||
|
const ModelSelector: React.FC<ModelSelectorProps> = ({ selectedModel, onModelChange }) => {
|
||||||
|
return (
|
||||||
|
<VStack spacing={3} align="stretch">
|
||||||
|
{AVAILABLE_MODELS.map((model, idx) => {
|
||||||
|
const isSelected = selectedModel === model.id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={model.id}
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: idx * 0.1 }}
|
||||||
|
whileHover={{ scale: 1.02, y: -2 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={() => onModelChange(model.id)}
|
||||||
|
bg={isSelected ? 'rgba(139, 92, 246, 0.15)' : 'rgba(255, 255, 255, 0.05)'}
|
||||||
|
backdropFilter="blur(12px)"
|
||||||
|
borderWidth={2}
|
||||||
|
borderColor={isSelected ? 'purple.400' : 'rgba(255, 255, 255, 0.1)'}
|
||||||
|
_hover={{
|
||||||
|
borderColor: isSelected ? 'purple.400' : 'rgba(255, 255, 255, 0.2)',
|
||||||
|
boxShadow: isSelected
|
||||||
|
? '0 8px 20px rgba(139, 92, 246, 0.4)'
|
||||||
|
: '0 4px 12px rgba(0, 0, 0, 0.3)',
|
||||||
|
}}
|
||||||
|
transition="all 0.3s"
|
||||||
|
>
|
||||||
|
<CardBody p={3}>
|
||||||
|
<HStack align="start" spacing={3}>
|
||||||
|
{/* 模型图标 */}
|
||||||
|
<Box
|
||||||
|
p={2}
|
||||||
|
borderRadius="lg"
|
||||||
|
bgGradient={
|
||||||
|
isSelected
|
||||||
|
? 'linear(to-br, purple.500, pink.500)'
|
||||||
|
: 'linear(to-br, rgba(139, 92, 246, 0.2), rgba(236, 72, 153, 0.2))'
|
||||||
|
}
|
||||||
|
boxShadow={isSelected ? '0 4px 12px rgba(139, 92, 246, 0.4)' : 'none'}
|
||||||
|
>
|
||||||
|
{model.icon}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 模型信息 */}
|
||||||
|
<Box flex={1}>
|
||||||
|
<Text fontWeight="semibold" fontSize="sm" color="gray.100">
|
||||||
|
{model.name}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="xs" color="gray.400" mt={1}>
|
||||||
|
{model.description}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 选中标记(Check 图标) */}
|
||||||
|
{isSelected && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
|
||||||
|
>
|
||||||
|
<Check className="w-5 h-5" color="#A78BFA" />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModelSelector;
|
||||||
119
src/views/AgentChat/components/RightSidebar/Statistics.tsx
Normal file
119
src/views/AgentChat/components/RightSidebar/Statistics.tsx
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
// src/views/AgentChat/components/RightSidebar/Statistics.tsx
|
||||||
|
// 统计信息组件
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
VStack,
|
||||||
|
Flex,
|
||||||
|
Box,
|
||||||
|
Text,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { MessageSquare, Activity, Code } from 'lucide-react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Statistics 组件的 Props 类型
|
||||||
|
*/
|
||||||
|
interface StatisticsProps {
|
||||||
|
/** 对话总数 */
|
||||||
|
sessionsCount: number;
|
||||||
|
/** 消息总数 */
|
||||||
|
messagesCount: number;
|
||||||
|
/** 已选工具数 */
|
||||||
|
selectedToolsCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Statistics - 统计信息组件
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* 1. 显示统计卡片(对话数、消息数、已选工具数)
|
||||||
|
* 2. 渐变色大数字展示
|
||||||
|
* 3. 图标装饰
|
||||||
|
*
|
||||||
|
* 设计特性:
|
||||||
|
* - 卡片渐进入场动画(延迟 `idx * 0.1`)
|
||||||
|
* - 毛玻璃效果卡片
|
||||||
|
* - 渐变色数字(蓝紫/紫粉/绿青)
|
||||||
|
* - 半透明图标装饰
|
||||||
|
*/
|
||||||
|
const Statistics: React.FC<StatisticsProps> = ({
|
||||||
|
sessionsCount,
|
||||||
|
messagesCount,
|
||||||
|
selectedToolsCount,
|
||||||
|
}) => {
|
||||||
|
const stats = [
|
||||||
|
{
|
||||||
|
label: '对话数',
|
||||||
|
value: sessionsCount,
|
||||||
|
gradient: 'linear(to-r, blue.400, purple.400)',
|
||||||
|
icon: MessageSquare,
|
||||||
|
iconColor: '#60A5FA',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '消息数',
|
||||||
|
value: messagesCount,
|
||||||
|
gradient: 'linear(to-r, purple.400, pink.400)',
|
||||||
|
icon: Activity,
|
||||||
|
iconColor: '#C084FC',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '已选工具',
|
||||||
|
value: selectedToolsCount,
|
||||||
|
gradient: 'linear(to-r, green.400, teal.400)',
|
||||||
|
icon: Code,
|
||||||
|
iconColor: '#34D399',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack spacing={4} align="stretch">
|
||||||
|
{stats.map((stat, idx) => {
|
||||||
|
const Icon = stat.icon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={stat.label}
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: idx * 0.1 }}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
|
backdropFilter="blur(12px)"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
boxShadow="0 8px 32px 0 rgba(31, 38, 135, 0.37)"
|
||||||
|
>
|
||||||
|
<CardBody p={4}>
|
||||||
|
<Flex align="center" justify="space-between">
|
||||||
|
{/* 左侧:标签和数值 */}
|
||||||
|
<Box>
|
||||||
|
<Text fontSize="xs" color="gray.400">
|
||||||
|
{stat.label}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
fontSize="2xl"
|
||||||
|
fontWeight="bold"
|
||||||
|
bgGradient={stat.gradient}
|
||||||
|
bgClip="text"
|
||||||
|
>
|
||||||
|
{stat.value}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 右侧:图标装饰 */}
|
||||||
|
<Icon className="w-8 h-8" color={stat.iconColor} style={{ opacity: 0.5 }} />
|
||||||
|
</Flex>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Statistics;
|
||||||
199
src/views/AgentChat/components/RightSidebar/ToolSelector.tsx
Normal file
199
src/views/AgentChat/components/RightSidebar/ToolSelector.tsx
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
// src/views/AgentChat/components/RightSidebar/ToolSelector.tsx
|
||||||
|
// 工具选择组件
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Badge,
|
||||||
|
Checkbox,
|
||||||
|
CheckboxGroup,
|
||||||
|
Accordion,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionButton,
|
||||||
|
AccordionPanel,
|
||||||
|
AccordionIcon,
|
||||||
|
HStack,
|
||||||
|
VStack,
|
||||||
|
Box,
|
||||||
|
Text,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { MCP_TOOLS, TOOL_CATEGORIES } from '../../constants/tools';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ToolSelector 组件的 Props 类型
|
||||||
|
*/
|
||||||
|
interface ToolSelectorProps {
|
||||||
|
/** 已选工具 ID 列表 */
|
||||||
|
selectedTools: string[];
|
||||||
|
/** 工具选择变化回调 */
|
||||||
|
onToolsChange: (tools: string[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ToolSelector - 工具选择组件
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* 1. 按分类展示工具列表(Accordion 手风琴)
|
||||||
|
* 2. 复选框选择/取消工具
|
||||||
|
* 3. 显示每个分类的已选/总数(如 "3/5")
|
||||||
|
* 4. 全选/清空按钮
|
||||||
|
*
|
||||||
|
* 设计特性:
|
||||||
|
* - 手风琴分类折叠
|
||||||
|
* - 悬停工具项右移 4px
|
||||||
|
* - 全选/清空按钮渐变色
|
||||||
|
* - 分类徽章显示选中数量
|
||||||
|
*/
|
||||||
|
const ToolSelector: React.FC<ToolSelectorProps> = ({ selectedTools, onToolsChange }) => {
|
||||||
|
/**
|
||||||
|
* 全选所有工具
|
||||||
|
*/
|
||||||
|
const handleSelectAll = () => {
|
||||||
|
onToolsChange(MCP_TOOLS.map((t) => t.id));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有选择
|
||||||
|
*/
|
||||||
|
const handleClearAll = () => {
|
||||||
|
onToolsChange([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* 工具分类手风琴 */}
|
||||||
|
<Accordion allowMultiple>
|
||||||
|
{Object.entries(TOOL_CATEGORIES).map(([category, tools], catIdx) => {
|
||||||
|
const selectedCount = tools.filter((t) => selectedTools.includes(t.id)).length;
|
||||||
|
const totalCount = tools.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={category}
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: catIdx * 0.05 }}
|
||||||
|
>
|
||||||
|
<AccordionItem
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
borderRadius="lg"
|
||||||
|
mb={2}
|
||||||
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
|
backdropFilter="blur(12px)"
|
||||||
|
_hover={{
|
||||||
|
bg: 'rgba(255, 255, 255, 0.08)',
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 手风琴标题 */}
|
||||||
|
<AccordionButton>
|
||||||
|
<HStack flex={1} justify="space-between" pr={2}>
|
||||||
|
<Text color="gray.100" fontSize="sm">
|
||||||
|
{category}
|
||||||
|
</Text>
|
||||||
|
<Badge
|
||||||
|
bgGradient="linear(to-r, blue.500, purple.500)"
|
||||||
|
color="white"
|
||||||
|
variant="subtle"
|
||||||
|
boxShadow="0 2px 8px rgba(139, 92, 246, 0.3)"
|
||||||
|
>
|
||||||
|
{selectedCount}/{totalCount}
|
||||||
|
</Badge>
|
||||||
|
</HStack>
|
||||||
|
<AccordionIcon color="gray.400" />
|
||||||
|
</AccordionButton>
|
||||||
|
|
||||||
|
{/* 手风琴内容 */}
|
||||||
|
<AccordionPanel pb={4}>
|
||||||
|
<CheckboxGroup value={selectedTools} onChange={onToolsChange}>
|
||||||
|
<VStack align="stretch" spacing={2}>
|
||||||
|
{tools.map((tool) => (
|
||||||
|
<motion.div
|
||||||
|
key={tool.id}
|
||||||
|
whileHover={{ x: 4 }}
|
||||||
|
transition={{ type: 'spring', stiffness: 300 }}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
value={tool.id}
|
||||||
|
colorScheme="purple"
|
||||||
|
p={2}
|
||||||
|
borderRadius="lg"
|
||||||
|
bg="rgba(255, 255, 255, 0.02)"
|
||||||
|
_hover={{ bg: 'rgba(255, 255, 255, 0.05)' }}
|
||||||
|
transition="background 0.2s"
|
||||||
|
>
|
||||||
|
<HStack spacing={2} align="start">
|
||||||
|
{/* 工具图标 */}
|
||||||
|
<Box color="purple.400" mt={0.5}>
|
||||||
|
{tool.icon}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 工具信息 */}
|
||||||
|
<Box>
|
||||||
|
<Text fontSize="sm" color="gray.200">
|
||||||
|
{tool.name}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="xs" color="gray.500">
|
||||||
|
{tool.description}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</HStack>
|
||||||
|
</Checkbox>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
</CheckboxGroup>
|
||||||
|
</AccordionPanel>
|
||||||
|
</AccordionItem>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
{/* 全选/清空按钮 */}
|
||||||
|
<HStack mt={4} spacing={2}>
|
||||||
|
{/* 全选按钮 */}
|
||||||
|
<motion.div flex={1} whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
w="full"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={handleSelectAll}
|
||||||
|
bgGradient="linear(to-r, blue.500, purple.500)"
|
||||||
|
color="white"
|
||||||
|
_hover={{
|
||||||
|
bgGradient: 'linear(to-r, blue.600, purple.600)',
|
||||||
|
boxShadow: '0 4px 12px rgba(139, 92, 246, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
全选
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* 清空按钮 */}
|
||||||
|
<motion.div flex={1} whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
w="full"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={handleClearAll}
|
||||||
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
|
color="gray.300"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
_hover={{
|
||||||
|
bg: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
清空
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
</HStack>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ToolSelector;
|
||||||
@@ -1,499 +0,0 @@
|
|||||||
// src/views/AgentChat/components/RightSidebar/index.js
|
|
||||||
// 右侧栏组件 - 配置中心
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
Badge,
|
|
||||||
Checkbox,
|
|
||||||
CheckboxGroup,
|
|
||||||
Tooltip,
|
|
||||||
IconButton,
|
|
||||||
Accordion,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionButton,
|
|
||||||
AccordionPanel,
|
|
||||||
AccordionIcon,
|
|
||||||
Tabs,
|
|
||||||
TabList,
|
|
||||||
TabPanels,
|
|
||||||
Tab,
|
|
||||||
TabPanel,
|
|
||||||
Card,
|
|
||||||
CardBody,
|
|
||||||
HStack,
|
|
||||||
VStack,
|
|
||||||
Flex,
|
|
||||||
Text,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import {
|
|
||||||
Settings,
|
|
||||||
ChevronRight,
|
|
||||||
Cpu,
|
|
||||||
Code,
|
|
||||||
BarChart3,
|
|
||||||
Check,
|
|
||||||
MessageSquare,
|
|
||||||
Activity,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { animations } from '../../constants/animations';
|
|
||||||
import { AVAILABLE_MODELS } from '../../constants/models';
|
|
||||||
import { MCP_TOOLS, TOOL_CATEGORIES } from '../../constants/tools';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RightSidebar - 右侧栏组件(配置中心)
|
|
||||||
*
|
|
||||||
* @param {Object} props
|
|
||||||
* @param {boolean} props.isOpen - 侧边栏是否展开
|
|
||||||
* @param {Function} props.onClose - 关闭侧边栏回调
|
|
||||||
* @param {string} props.selectedModel - 当前选中的模型 ID
|
|
||||||
* @param {Function} props.onModelChange - 模型切换回调
|
|
||||||
* @param {Array} props.selectedTools - 已选工具 ID 列表
|
|
||||||
* @param {Function} props.onToolsChange - 工具选择变化回调
|
|
||||||
* @param {number} props.sessionsCount - 会话总数
|
|
||||||
* @param {number} props.messagesCount - 消息总数
|
|
||||||
* @returns {JSX.Element|null}
|
|
||||||
*/
|
|
||||||
const RightSidebar = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
selectedModel,
|
|
||||||
onModelChange,
|
|
||||||
selectedTools,
|
|
||||||
onToolsChange,
|
|
||||||
sessionsCount,
|
|
||||||
messagesCount,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<AnimatePresence>
|
|
||||||
{isOpen && (
|
|
||||||
<motion.div
|
|
||||||
style={{ width: '320px', display: 'flex', flexDirection: 'column' }}
|
|
||||||
initial="initial"
|
|
||||||
animate="animate"
|
|
||||||
exit="exit"
|
|
||||||
variants={animations.slideInRight}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
w="320px"
|
|
||||||
h="100%"
|
|
||||||
display="flex"
|
|
||||||
flexDirection="column"
|
|
||||||
bg="rgba(17, 24, 39, 0.8)"
|
|
||||||
backdropFilter="blur(20px) saturate(180%)"
|
|
||||||
borderLeft="1px solid"
|
|
||||||
borderColor="rgba(255, 255, 255, 0.1)"
|
|
||||||
boxShadow="-4px 0 24px rgba(0, 0, 0, 0.3)"
|
|
||||||
>
|
|
||||||
{/* 标题栏 */}
|
|
||||||
<Box p={4} borderBottom="1px solid" borderColor="rgba(255, 255, 255, 0.1)">
|
|
||||||
<HStack justify="space-between">
|
|
||||||
<HStack spacing={2}>
|
|
||||||
<Settings className="w-5 h-5" color="#C084FC" />
|
|
||||||
<Text
|
|
||||||
fontWeight="semibold"
|
|
||||||
bgGradient="linear(to-r, purple.300, pink.300)"
|
|
||||||
bgClip="text"
|
|
||||||
fontSize="md"
|
|
||||||
>
|
|
||||||
配置中心
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
<Tooltip label="收起侧边栏">
|
|
||||||
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
|
|
||||||
<IconButton
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
icon={<ChevronRight className="w-4 h-4" />}
|
|
||||||
onClick={onClose}
|
|
||||||
bg="rgba(255, 255, 255, 0.05)"
|
|
||||||
color="gray.300"
|
|
||||||
backdropFilter="blur(10px)"
|
|
||||||
border="1px solid"
|
|
||||||
borderColor="rgba(255, 255, 255, 0.1)"
|
|
||||||
_hover={{
|
|
||||||
bg: 'rgba(255, 255, 255, 0.1)',
|
|
||||||
borderColor: 'purple.400',
|
|
||||||
color: 'white',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
</Tooltip>
|
|
||||||
</HStack>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Tab 面板 */}
|
|
||||||
<Box flex={1} overflowY="auto">
|
|
||||||
<Tabs colorScheme="purple" variant="line">
|
|
||||||
<TabList px={4} borderBottom="1px solid" borderColor="rgba(255, 255, 255, 0.1)">
|
|
||||||
<Tab
|
|
||||||
color="gray.400"
|
|
||||||
_selected={{
|
|
||||||
color: 'purple.400',
|
|
||||||
borderColor: 'purple.500',
|
|
||||||
boxShadow: '0 2px 8px rgba(139, 92, 246, 0.3)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<HStack spacing={2}>
|
|
||||||
<Cpu className="w-4 h-4" />
|
|
||||||
<Text>模型</Text>
|
|
||||||
</HStack>
|
|
||||||
</Tab>
|
|
||||||
<Tab
|
|
||||||
color="gray.400"
|
|
||||||
_selected={{
|
|
||||||
color: 'purple.400',
|
|
||||||
borderColor: 'purple.500',
|
|
||||||
boxShadow: '0 2px 8px rgba(139, 92, 246, 0.3)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<HStack spacing={2}>
|
|
||||||
<Code className="w-4 h-4" />
|
|
||||||
<Text>工具</Text>
|
|
||||||
{selectedTools.length > 0 && (
|
|
||||||
<Badge
|
|
||||||
bgGradient="linear(to-r, blue.500, purple.500)"
|
|
||||||
color="white"
|
|
||||||
borderRadius="full"
|
|
||||||
fontSize="xs"
|
|
||||||
px={2}
|
|
||||||
py={0.5}
|
|
||||||
boxShadow="0 2px 8px rgba(139, 92, 246, 0.3)"
|
|
||||||
>
|
|
||||||
{selectedTools.length}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</HStack>
|
|
||||||
</Tab>
|
|
||||||
<Tab
|
|
||||||
color="gray.400"
|
|
||||||
_selected={{
|
|
||||||
color: 'purple.400',
|
|
||||||
borderColor: 'purple.500',
|
|
||||||
boxShadow: '0 2px 8px rgba(139, 92, 246, 0.3)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<HStack spacing={2}>
|
|
||||||
<BarChart3 className="w-4 h-4" />
|
|
||||||
<Text>统计</Text>
|
|
||||||
</HStack>
|
|
||||||
</Tab>
|
|
||||||
</TabList>
|
|
||||||
|
|
||||||
<TabPanels>
|
|
||||||
{/* 模型选择 */}
|
|
||||||
<TabPanel p={4}>
|
|
||||||
<VStack spacing={3} align="stretch">
|
|
||||||
{AVAILABLE_MODELS.map((model, idx) => (
|
|
||||||
<motion.div
|
|
||||||
key={model.id}
|
|
||||||
initial={{ opacity: 0, y: 10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: idx * 0.1 }}
|
|
||||||
whileHover={{ scale: 1.02, y: -2 }}
|
|
||||||
whileTap={{ scale: 0.98 }}
|
|
||||||
>
|
|
||||||
<Card
|
|
||||||
cursor="pointer"
|
|
||||||
onClick={() => onModelChange(model.id)}
|
|
||||||
bg={
|
|
||||||
selectedModel === model.id
|
|
||||||
? 'rgba(139, 92, 246, 0.15)'
|
|
||||||
: 'rgba(255, 255, 255, 0.05)'
|
|
||||||
}
|
|
||||||
backdropFilter="blur(12px)"
|
|
||||||
borderWidth={2}
|
|
||||||
borderColor={
|
|
||||||
selectedModel === model.id ? 'purple.400' : 'rgba(255, 255, 255, 0.1)'
|
|
||||||
}
|
|
||||||
_hover={{
|
|
||||||
borderColor:
|
|
||||||
selectedModel === model.id ? 'purple.400' : 'rgba(255, 255, 255, 0.2)',
|
|
||||||
boxShadow:
|
|
||||||
selectedModel === model.id
|
|
||||||
? '0 8px 20px rgba(139, 92, 246, 0.4)'
|
|
||||||
: '0 4px 12px rgba(0, 0, 0, 0.3)',
|
|
||||||
}}
|
|
||||||
transition="all 0.3s"
|
|
||||||
>
|
|
||||||
<CardBody p={3}>
|
|
||||||
<HStack align="start" spacing={3}>
|
|
||||||
<Box
|
|
||||||
p={2}
|
|
||||||
borderRadius="lg"
|
|
||||||
bgGradient={
|
|
||||||
selectedModel === model.id
|
|
||||||
? 'linear(to-br, purple.500, pink.500)'
|
|
||||||
: 'linear(to-br, rgba(139, 92, 246, 0.2), rgba(236, 72, 153, 0.2))'
|
|
||||||
}
|
|
||||||
boxShadow={
|
|
||||||
selectedModel === model.id
|
|
||||||
? '0 4px 12px rgba(139, 92, 246, 0.4)'
|
|
||||||
: 'none'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{model.icon}
|
|
||||||
</Box>
|
|
||||||
<Box flex={1}>
|
|
||||||
<Text fontWeight="semibold" fontSize="sm" color="gray.100">
|
|
||||||
{model.name}
|
|
||||||
</Text>
|
|
||||||
<Text fontSize="xs" color="gray.400" mt={1}>
|
|
||||||
{model.description}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
{selectedModel === model.id && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ scale: 0 }}
|
|
||||||
animate={{ scale: 1 }}
|
|
||||||
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
|
|
||||||
>
|
|
||||||
<Check className="w-5 h-5" color="#A78BFA" />
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</HStack>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
))}
|
|
||||||
</VStack>
|
|
||||||
</TabPanel>
|
|
||||||
|
|
||||||
{/* 工具选择 */}
|
|
||||||
<TabPanel p={4}>
|
|
||||||
<Accordion allowMultiple>
|
|
||||||
{Object.entries(TOOL_CATEGORIES).map(([category, tools], catIdx) => (
|
|
||||||
<motion.div
|
|
||||||
key={category}
|
|
||||||
initial={{ opacity: 0, y: 10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: catIdx * 0.05 }}
|
|
||||||
>
|
|
||||||
<AccordionItem
|
|
||||||
border="1px solid"
|
|
||||||
borderColor="rgba(255, 255, 255, 0.1)"
|
|
||||||
borderRadius="lg"
|
|
||||||
mb={2}
|
|
||||||
bg="rgba(255, 255, 255, 0.05)"
|
|
||||||
backdropFilter="blur(12px)"
|
|
||||||
_hover={{
|
|
||||||
bg: 'rgba(255, 255, 255, 0.08)',
|
|
||||||
borderColor: 'rgba(255, 255, 255, 0.2)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AccordionButton>
|
|
||||||
<HStack flex={1} justify="space-between" pr={2}>
|
|
||||||
<Text color="gray.100" fontSize="sm">
|
|
||||||
{category}
|
|
||||||
</Text>
|
|
||||||
<Badge
|
|
||||||
bgGradient="linear(to-r, blue.500, purple.500)"
|
|
||||||
color="white"
|
|
||||||
variant="subtle"
|
|
||||||
boxShadow="0 2px 8px rgba(139, 92, 246, 0.3)"
|
|
||||||
>
|
|
||||||
{tools.filter((t) => selectedTools.includes(t.id)).length}/{tools.length}
|
|
||||||
</Badge>
|
|
||||||
</HStack>
|
|
||||||
<AccordionIcon color="gray.400" />
|
|
||||||
</AccordionButton>
|
|
||||||
<AccordionPanel pb={4}>
|
|
||||||
<CheckboxGroup value={selectedTools} onChange={onToolsChange}>
|
|
||||||
<VStack align="stretch" spacing={2}>
|
|
||||||
{tools.map((tool) => (
|
|
||||||
<motion.div
|
|
||||||
key={tool.id}
|
|
||||||
whileHover={{ x: 4 }}
|
|
||||||
transition={{ type: 'spring', stiffness: 300 }}
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
value={tool.id}
|
|
||||||
colorScheme="purple"
|
|
||||||
p={2}
|
|
||||||
borderRadius="lg"
|
|
||||||
bg="rgba(255, 255, 255, 0.02)"
|
|
||||||
_hover={{ bg: 'rgba(255, 255, 255, 0.05)' }}
|
|
||||||
transition="background 0.2s"
|
|
||||||
>
|
|
||||||
<HStack spacing={2} align="start">
|
|
||||||
<Box color="purple.400" mt={0.5}>
|
|
||||||
{tool.icon}
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fontSize="sm" color="gray.200">
|
|
||||||
{tool.name}
|
|
||||||
</Text>
|
|
||||||
<Text fontSize="xs" color="gray.500">
|
|
||||||
{tool.description}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
</HStack>
|
|
||||||
</Checkbox>
|
|
||||||
</motion.div>
|
|
||||||
))}
|
|
||||||
</VStack>
|
|
||||||
</CheckboxGroup>
|
|
||||||
</AccordionPanel>
|
|
||||||
</AccordionItem>
|
|
||||||
</motion.div>
|
|
||||||
))}
|
|
||||||
</Accordion>
|
|
||||||
|
|
||||||
<HStack mt={4} spacing={2}>
|
|
||||||
<motion.div flex={1} whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
w="full"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => onToolsChange(MCP_TOOLS.map((t) => t.id))}
|
|
||||||
bgGradient="linear(to-r, blue.500, purple.500)"
|
|
||||||
color="white"
|
|
||||||
_hover={{
|
|
||||||
bgGradient: 'linear(to-r, blue.600, purple.600)',
|
|
||||||
boxShadow: '0 4px 12px rgba(139, 92, 246, 0.4)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
全选
|
|
||||||
</Button>
|
|
||||||
</motion.div>
|
|
||||||
<motion.div flex={1} whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
w="full"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => onToolsChange([])}
|
|
||||||
bg="rgba(255, 255, 255, 0.05)"
|
|
||||||
color="gray.300"
|
|
||||||
border="1px solid"
|
|
||||||
borderColor="rgba(255, 255, 255, 0.1)"
|
|
||||||
_hover={{
|
|
||||||
bg: 'rgba(255, 255, 255, 0.1)',
|
|
||||||
borderColor: 'rgba(255, 255, 255, 0.2)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
清空
|
|
||||||
</Button>
|
|
||||||
</motion.div>
|
|
||||||
</HStack>
|
|
||||||
</TabPanel>
|
|
||||||
|
|
||||||
{/* 统计信息 */}
|
|
||||||
<TabPanel p={4}>
|
|
||||||
<VStack spacing={4} align="stretch">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0 }}
|
|
||||||
>
|
|
||||||
<Card
|
|
||||||
bg="rgba(255, 255, 255, 0.05)"
|
|
||||||
backdropFilter="blur(12px)"
|
|
||||||
border="1px solid"
|
|
||||||
borderColor="rgba(255, 255, 255, 0.1)"
|
|
||||||
boxShadow="0 8px 32px 0 rgba(31, 38, 135, 0.37)"
|
|
||||||
>
|
|
||||||
<CardBody p={4}>
|
|
||||||
<Flex align="center" justify="space-between">
|
|
||||||
<Box>
|
|
||||||
<Text fontSize="xs" color="gray.400">
|
|
||||||
对话数
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
fontSize="2xl"
|
|
||||||
fontWeight="bold"
|
|
||||||
bgGradient="linear(to-r, blue.400, purple.400)"
|
|
||||||
bgClip="text"
|
|
||||||
>
|
|
||||||
{sessionsCount}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
<MessageSquare
|
|
||||||
className="w-8 h-8"
|
|
||||||
color="#60A5FA"
|
|
||||||
style={{ opacity: 0.5 }}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.1 }}
|
|
||||||
>
|
|
||||||
<Card
|
|
||||||
bg="rgba(255, 255, 255, 0.05)"
|
|
||||||
backdropFilter="blur(12px)"
|
|
||||||
border="1px solid"
|
|
||||||
borderColor="rgba(255, 255, 255, 0.1)"
|
|
||||||
boxShadow="0 8px 32px 0 rgba(31, 38, 135, 0.37)"
|
|
||||||
>
|
|
||||||
<CardBody p={4}>
|
|
||||||
<Flex align="center" justify="space-between">
|
|
||||||
<Box>
|
|
||||||
<Text fontSize="xs" color="gray.400">
|
|
||||||
消息数
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
fontSize="2xl"
|
|
||||||
fontWeight="bold"
|
|
||||||
bgGradient="linear(to-r, purple.400, pink.400)"
|
|
||||||
bgClip="text"
|
|
||||||
>
|
|
||||||
{messagesCount}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
<Activity className="w-8 h-8" color="#C084FC" style={{ opacity: 0.5 }} />
|
|
||||||
</Flex>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.2 }}
|
|
||||||
>
|
|
||||||
<Card
|
|
||||||
bg="rgba(255, 255, 255, 0.05)"
|
|
||||||
backdropFilter="blur(12px)"
|
|
||||||
border="1px solid"
|
|
||||||
borderColor="rgba(255, 255, 255, 0.1)"
|
|
||||||
boxShadow="0 8px 32px 0 rgba(31, 38, 135, 0.37)"
|
|
||||||
>
|
|
||||||
<CardBody p={4}>
|
|
||||||
<Flex align="center" justify="space-between">
|
|
||||||
<Box>
|
|
||||||
<Text fontSize="xs" color="gray.400">
|
|
||||||
已选工具
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
fontSize="2xl"
|
|
||||||
fontWeight="bold"
|
|
||||||
bgGradient="linear(to-r, green.400, teal.400)"
|
|
||||||
bgClip="text"
|
|
||||||
>
|
|
||||||
{selectedTools.length}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
<Code className="w-8 h-8" color="#34D399" style={{ opacity: 0.5 }} />
|
|
||||||
</Flex>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
</VStack>
|
|
||||||
</TabPanel>
|
|
||||||
</TabPanels>
|
|
||||||
</Tabs>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RightSidebar;
|
|
||||||
240
src/views/AgentChat/components/RightSidebar/index.tsx
Normal file
240
src/views/AgentChat/components/RightSidebar/index.tsx
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
// src/views/AgentChat/components/RightSidebar/index.tsx
|
||||||
|
// 右侧栏组件 - 配置中心(重构版本)
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Tooltip,
|
||||||
|
IconButton,
|
||||||
|
Tabs,
|
||||||
|
TabList,
|
||||||
|
TabPanels,
|
||||||
|
Tab,
|
||||||
|
TabPanel,
|
||||||
|
Badge,
|
||||||
|
HStack,
|
||||||
|
Text,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import {
|
||||||
|
Settings,
|
||||||
|
ChevronRight,
|
||||||
|
Cpu,
|
||||||
|
Code,
|
||||||
|
BarChart3,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { animations } from '../../constants/animations';
|
||||||
|
import ModelSelector from './ModelSelector';
|
||||||
|
import ToolSelector from './ToolSelector';
|
||||||
|
import Statistics from './Statistics';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RightSidebar 组件的 Props 类型
|
||||||
|
*/
|
||||||
|
interface RightSidebarProps {
|
||||||
|
/** 侧边栏是否展开 */
|
||||||
|
isOpen: boolean;
|
||||||
|
/** 关闭侧边栏回调 */
|
||||||
|
onClose: () => void;
|
||||||
|
/** 当前选中的模型 ID */
|
||||||
|
selectedModel: string;
|
||||||
|
/** 模型切换回调 */
|
||||||
|
onModelChange: (modelId: string) => void;
|
||||||
|
/** 已选工具 ID 列表 */
|
||||||
|
selectedTools: string[];
|
||||||
|
/** 工具选择变化回调 */
|
||||||
|
onToolsChange: (tools: string[]) => void;
|
||||||
|
/** 会话总数 */
|
||||||
|
sessionsCount: number;
|
||||||
|
/** 消息总数 */
|
||||||
|
messagesCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RightSidebar - 右侧栏组件(配置中心)(重构版本)
|
||||||
|
*
|
||||||
|
* 架构改进:
|
||||||
|
* - 模型选择提取到 ModelSelector 组件(120 行)
|
||||||
|
* - 工具选择提取到 ToolSelector 组件(200 行)
|
||||||
|
* - 统计信息提取到 Statistics 组件(100 行)
|
||||||
|
* - 主组件只负责 Tabs 管理和布局组合(150 行)
|
||||||
|
*
|
||||||
|
* 主组件职责:
|
||||||
|
* 1. 管理 Tabs 切换(模型/工具/统计)
|
||||||
|
* 2. 渲染标题栏(配置中心 + 收起按钮)
|
||||||
|
* 3. 组合三个子组件(TabPanels)
|
||||||
|
* 4. 处理侧边栏动画(滑入/滑出)
|
||||||
|
*/
|
||||||
|
const RightSidebar: React.FC<RightSidebarProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
selectedModel,
|
||||||
|
onModelChange,
|
||||||
|
selectedTools,
|
||||||
|
onToolsChange,
|
||||||
|
sessionsCount,
|
||||||
|
messagesCount,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{isOpen && (
|
||||||
|
<motion.div
|
||||||
|
style={{ width: '320px', display: 'flex', flexDirection: 'column' }}
|
||||||
|
initial="initial"
|
||||||
|
animate="animate"
|
||||||
|
exit="exit"
|
||||||
|
variants={animations.slideInRight}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
w="320px"
|
||||||
|
h="100%"
|
||||||
|
display="flex"
|
||||||
|
flexDirection="column"
|
||||||
|
bg="rgba(17, 24, 39, 0.8)"
|
||||||
|
backdropFilter="blur(20px) saturate(180%)"
|
||||||
|
borderLeft="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
boxShadow="-4px 0 24px rgba(0, 0, 0, 0.3)"
|
||||||
|
>
|
||||||
|
{/* ==================== 标题栏 ==================== */}
|
||||||
|
<Box p={4} borderBottom="1px solid" borderColor="rgba(255, 255, 255, 0.1)">
|
||||||
|
<HStack justify="space-between">
|
||||||
|
{/* 左侧:标题 */}
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Settings className="w-5 h-5" color="#C084FC" />
|
||||||
|
<Text
|
||||||
|
fontWeight="semibold"
|
||||||
|
bgGradient="linear(to-r, purple.300, pink.300)"
|
||||||
|
bgClip="text"
|
||||||
|
fontSize="md"
|
||||||
|
>
|
||||||
|
配置中心
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
{/* 右侧:收起按钮 */}
|
||||||
|
<Tooltip label="收起侧边栏">
|
||||||
|
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
icon={<ChevronRight className="w-4 h-4" />}
|
||||||
|
onClick={onClose}
|
||||||
|
aria-label="收起侧边栏"
|
||||||
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
|
color="gray.300"
|
||||||
|
backdropFilter="blur(10px)"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
_hover={{
|
||||||
|
bg: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
borderColor: 'purple.400',
|
||||||
|
color: 'white',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</Tooltip>
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* ==================== Tab 面板 ==================== */}
|
||||||
|
<Box flex={1} overflowY="auto">
|
||||||
|
<Tabs colorScheme="purple" variant="line">
|
||||||
|
{/* Tab 标签栏 */}
|
||||||
|
<TabList px={4} borderBottom="1px solid" borderColor="rgba(255, 255, 255, 0.1)">
|
||||||
|
{/* 模型 Tab */}
|
||||||
|
<Tab
|
||||||
|
color="gray.400"
|
||||||
|
_selected={{
|
||||||
|
color: 'purple.400',
|
||||||
|
borderColor: 'purple.500',
|
||||||
|
boxShadow: '0 2px 8px rgba(139, 92, 246, 0.3)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Cpu className="w-4 h-4" />
|
||||||
|
<Text>模型</Text>
|
||||||
|
</HStack>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
{/* 工具 Tab */}
|
||||||
|
<Tab
|
||||||
|
color="gray.400"
|
||||||
|
_selected={{
|
||||||
|
color: 'purple.400',
|
||||||
|
borderColor: 'purple.500',
|
||||||
|
boxShadow: '0 2px 8px rgba(139, 92, 246, 0.3)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Code className="w-4 h-4" />
|
||||||
|
<Text>工具</Text>
|
||||||
|
{selectedTools.length > 0 && (
|
||||||
|
<Badge
|
||||||
|
bgGradient="linear(to-r, blue.500, purple.500)"
|
||||||
|
color="white"
|
||||||
|
borderRadius="full"
|
||||||
|
fontSize="xs"
|
||||||
|
px={2}
|
||||||
|
py={0.5}
|
||||||
|
boxShadow="0 2px 8px rgba(139, 92, 246, 0.3)"
|
||||||
|
>
|
||||||
|
{selectedTools.length}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
{/* 统计 Tab */}
|
||||||
|
<Tab
|
||||||
|
color="gray.400"
|
||||||
|
_selected={{
|
||||||
|
color: 'purple.400',
|
||||||
|
borderColor: 'purple.500',
|
||||||
|
boxShadow: '0 2px 8px rgba(139, 92, 246, 0.3)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<BarChart3 className="w-4 h-4" />
|
||||||
|
<Text>统计</Text>
|
||||||
|
</HStack>
|
||||||
|
</Tab>
|
||||||
|
</TabList>
|
||||||
|
|
||||||
|
{/* Tab 内容面板 */}
|
||||||
|
<TabPanels>
|
||||||
|
{/* 模型选择面板 */}
|
||||||
|
<TabPanel p={4}>
|
||||||
|
<ModelSelector
|
||||||
|
selectedModel={selectedModel}
|
||||||
|
onModelChange={onModelChange}
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
{/* 工具选择面板 */}
|
||||||
|
<TabPanel p={4}>
|
||||||
|
<ToolSelector
|
||||||
|
selectedTools={selectedTools}
|
||||||
|
onToolsChange={onToolsChange}
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
{/* 统计信息面板 */}
|
||||||
|
<TabPanel p={4}>
|
||||||
|
<Statistics
|
||||||
|
sessionsCount={sessionsCount}
|
||||||
|
messagesCount={messagesCount}
|
||||||
|
selectedToolsCount={selectedTools.length}
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
</TabPanels>
|
||||||
|
</Tabs>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RightSidebar;
|
||||||
27
src/views/AgentChat/components/RightSidebar/types.ts
Normal file
27
src/views/AgentChat/components/RightSidebar/types.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// src/views/AgentChat/components/RightSidebar/types.ts
|
||||||
|
// RightSidebar 组件的 TypeScript 类型定义
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模型数据结构(来自 constants/models)
|
||||||
|
*/
|
||||||
|
export interface Model {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具数据结构(来自 constants/tools)
|
||||||
|
*/
|
||||||
|
export interface Tool {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具分类(Map<分类名, 工具列表>)
|
||||||
|
*/
|
||||||
|
export type ToolCategories = Record<string, Tool[]>;
|
||||||
@@ -221,7 +221,7 @@ export const useAgentChat = ({
|
|||||||
loadSessions();
|
loadSessions();
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error('Agent chat error', error);
|
logger.error('useAgentChat', 'handleSendMessage', error as Error);
|
||||||
|
|
||||||
// 移除 "思考中" 和 "执行中" 消息
|
// 移除 "思考中" 和 "执行中" 消息
|
||||||
setMessages((prev) =>
|
setMessages((prev) =>
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export const useAgentSessions = ({
|
|||||||
setSessions(response.data.data);
|
setSessions(response.data.data);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('加载会话列表失败', error);
|
logger.error('useAgentSessions', 'loadSessions', error as Error);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingSessions(false);
|
setIsLoadingSessions(false);
|
||||||
}
|
}
|
||||||
@@ -135,7 +135,7 @@ export const useAgentSessions = ({
|
|||||||
setMessages(formattedMessages);
|
setMessages(formattedMessages);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('加载会话历史失败', error);
|
logger.error('useAgentSessions', 'loadSessionHistory', error as Error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setMessages]
|
[setMessages]
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
// src/views/AgentChat/utils/sessionUtils.js
|
// src/views/AgentChat/utils/sessionUtils.ts
|
||||||
// 会话管理工具函数
|
// 会话管理工具函数(TypeScript 版本)
|
||||||
|
|
||||||
|
import type { Session, SessionGroups } from '../components/LeftSidebar/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按日期分组会话列表
|
* 按日期分组会话列表
|
||||||
*
|
*
|
||||||
* @param {Array} sessions - 会话列表
|
* @param sessions - 会话列表
|
||||||
* @returns {Object} 分组后的会话对象 { today, yesterday, thisWeek, older }
|
* @returns 分组后的会话对象 { today, yesterday, thisWeek, older }
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const groups = groupSessionsByDate(sessions);
|
* const groups = groupSessionsByDate(sessions);
|
||||||
* console.log(groups.today); // 今天的会话
|
* console.log(groups.today); // 今天的会话
|
||||||
* console.log(groups.yesterday); // 昨天的会话
|
* console.log(groups.yesterday); // 昨天的会话
|
||||||
*/
|
*/
|
||||||
export const groupSessionsByDate = (sessions) => {
|
export const groupSessionsByDate = (sessions: Session[]): SessionGroups => {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const yesterday = new Date(today);
|
const yesterday = new Date(today);
|
||||||
yesterday.setDate(yesterday.getDate() - 1);
|
yesterday.setDate(yesterday.getDate() - 1);
|
||||||
const weekAgo = new Date(today);
|
const weekAgo = new Date(today);
|
||||||
weekAgo.setDate(weekAgo.getDate() - 7);
|
weekAgo.setDate(weekAgo.getDate() - 7);
|
||||||
|
|
||||||
const groups = {
|
const groups: SessionGroups = {
|
||||||
today: [],
|
today: [],
|
||||||
yesterday: [],
|
yesterday: [],
|
||||||
thisWeek: [],
|
thisWeek: [],
|
||||||
@@ -27,8 +29,8 @@ export const groupSessionsByDate = (sessions) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
sessions.forEach((session) => {
|
sessions.forEach((session) => {
|
||||||
const sessionDate = new Date(session.created_at || session.timestamp);
|
const sessionDate = new Date(session.created_at || session.timestamp || Date.now());
|
||||||
const daysDiff = Math.floor((today - sessionDate) / (1000 * 60 * 60 * 24));
|
const daysDiff = Math.floor((today.getTime() - sessionDate.getTime()) / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
if (daysDiff === 0) {
|
if (daysDiff === 0) {
|
||||||
groups.today.push(session);
|
groups.today.push(session);
|
||||||
@@ -45,7 +45,7 @@ import FullCalendar from '@fullcalendar/react';
|
|||||||
import dayGridPlugin from '@fullcalendar/daygrid';
|
import dayGridPlugin from '@fullcalendar/daygrid';
|
||||||
import interactionPlugin from '@fullcalendar/interaction';
|
import interactionPlugin from '@fullcalendar/interaction';
|
||||||
import { DateClickArg } from '@fullcalendar/interaction';
|
import { DateClickArg } from '@fullcalendar/interaction';
|
||||||
import { EventClickArg } from '@fullcalendar/common';
|
import type { EventClickArg } from '@fullcalendar/core';
|
||||||
import dayjs, { Dayjs } from 'dayjs';
|
import dayjs, { Dayjs } from 'dayjs';
|
||||||
import 'dayjs/locale/zh-cn';
|
import 'dayjs/locale/zh-cn';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user