From af2b8531e92b64679c43167093d7271d1678bf5a Mon Sep 17 00:00:00 2001 From: zhaoying Date: Tue, 16 Dec 2025 11:27:13 +0800 Subject: [PATCH] components: Add Chat component --- web/src/components/Chat/ChatContent.tsx | 84 +++++++++++++++++++++++++ web/src/components/Chat/ChatInput.tsx | 80 +++++++++++++++++++++++ web/src/components/Chat/index.tsx | 47 ++++++++++++++ web/src/components/Chat/types.ts | 84 +++++++++++++++++++++++++ 4 files changed, 295 insertions(+) create mode 100644 web/src/components/Chat/ChatContent.tsx create mode 100644 web/src/components/Chat/ChatInput.tsx create mode 100644 web/src/components/Chat/index.tsx create mode 100644 web/src/components/Chat/types.ts diff --git a/web/src/components/Chat/ChatContent.tsx b/web/src/components/Chat/ChatContent.tsx new file mode 100644 index 00000000..18771bb1 --- /dev/null +++ b/web/src/components/Chat/ChatContent.tsx @@ -0,0 +1,84 @@ +/* + * @Author: ZhaoYing + * @Date: 2025-12-10 16:46:17 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2025-12-11 13:40:18 + */ +import { type FC, useRef, useEffect } from 'react' +import clsx from 'clsx' +import Markdown from '@/components/Markdown' +import type { ChatContentProps } from './types' + +/** + * 聊天内容显示组件 + * 负责渲染聊天消息列表,支持不同角色的消息样式和自动滚动 + */ +const ChatContent: FC = ({ + classNames, + contentClassNames, + data = [], + streamLoading = false, + empty, + labelPosition = 'bottom', + labelFormat, + errorDesc +}) => { + // 滚动容器引用,用于控制自动滚动到底部 + const scrollContainerRef = useRef<(HTMLDivElement | null)>(null) + + // 当数据变化时,自动滚动到底部显示最新消息 + useEffect(() => { + setTimeout(() => { + if (scrollContainerRef.current) { + scrollContainerRef.current.scrollTop = scrollContainerRef.current.scrollHeight; + } + }, 0); + }, [data]) + return ( +
+ {data.length === 0 + ? empty // 显示空状态 + : data.map((item, index) => ( +
+ {/* 流式加载时且内容为空则不显示 */} + {streamLoading && item.content === '' + ? null + : <> + {/* 顶部标签(如时间戳、用户名等) */} + {labelPosition === 'top' && +
+ {labelFormat(item)} +
+ } + {/* 消息气泡框 */} +
+ {/* 使用Markdown组件渲染消息内容 */} + +
+ {/* 底部标签(如时间戳、用户名等) */} + {labelPosition === 'bottom' && +
+ {labelFormat(item)} +
+ } + + } +
+ )) + } +
+ ) +} + +export default ChatContent diff --git a/web/src/components/Chat/ChatInput.tsx b/web/src/components/Chat/ChatInput.tsx new file mode 100644 index 00000000..8dd19410 --- /dev/null +++ b/web/src/components/Chat/ChatInput.tsx @@ -0,0 +1,80 @@ +/* + * @Author: ZhaoYing + * @Date: 2025-12-10 16:46:14 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2025-12-10 16:49:13 + */ +import { useEffect } from 'react' +import { Flex, Input, Form } from 'antd' +import SendIcon from '@/assets/images/conversation/send.svg' +import SendDisabledIcon from '@/assets/images/conversation/sendDisabled.svg' +import LoadingIcon from '@/assets/images/conversation/loading.svg' +import type { ChatInputProps } from './types' + +/** + * 聊天输入框组件 + * 提供消息输入、发送功能,支持键盘快捷键和加载状态显示 + */ +const ChatInput = ({ message, onChange, onSend, loading, children }: ChatInputProps) => { + const [form] = Form.useForm() + // 监听表单值变化,用于控制发送按钮状态 + const values = Form.useWatch([], form); + + // 当外部message为空时,清空表单 + useEffect(() => { + if (!message) { + form.setFieldsValue({ + message: undefined, + }) + } + }, [form, message]) + + // 当加载状态时,清空输入框 + useEffect(() => { + if (loading) { + form.setFieldsValue({ + message: undefined, + }) + } + }, [loading]) + + return ( +
+ + {/* 消息输入表单 */} +
+ + onChange(e.target.value)} + onKeyDown={(e) => { + // Enter键发送,Shift+Enter换行 + if (e.key === 'Enter' && !e.shiftKey && (e.target as HTMLTextAreaElement).value?.trim() !== '' && !loading) { + e.preventDefault(); + onSend(); + } + }} + /> + +
+ + {/* 底部操作区域 */} + + {/* 子组件内容(如按钮等) */} + {children} + {/* 发送按钮 - 根据状态显示不同图标 */} + {loading + ? + : !values || !values?.message || values?.message?.trim() === '' + ? + : + } + +
+
+ ) +} + +export default ChatInput diff --git a/web/src/components/Chat/index.tsx b/web/src/components/Chat/index.tsx new file mode 100644 index 00000000..7db29bfc --- /dev/null +++ b/web/src/components/Chat/index.tsx @@ -0,0 +1,47 @@ +/* + * @Author: ZhaoYing + * @Date: 2025-12-10 16:46:09 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2025-12-11 13:43:51 + */ +import { type FC } from 'react' +import ChatInput from './ChatInput' +import type { ChatProps } from './types' +import ChatContent from './ChatContent' + +/** + * 聊天组件 - 主要组件,由内容区域和输入框组成 + * 提供完整的聊天界面功能,包括消息显示和输入交互 + */ +const Chat: FC = ({ + empty, + data, + onChange, + onSend, + streamLoading = false, + loading, + contentClassName = '', + children, + labelFormat, + errorDesc +}) => { + return ( +
+ {/* 聊天内容显示区域 */} + + + {/* 聊天输入框区域 */} + + {children} + +
+ ) +} +export default Chat diff --git a/web/src/components/Chat/types.ts b/web/src/components/Chat/types.ts new file mode 100644 index 00000000..851a8ccc --- /dev/null +++ b/web/src/components/Chat/types.ts @@ -0,0 +1,84 @@ +/* + * @Author: ZhaoYing + * @Date: 2025-12-10 16:45:54 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2025-12-11 13:43:52 + */ +import { type ReactNode } from 'react' + +/** + * 聊天消息项接口 + */ +export interface ChatItem { + /** 消息唯一标识 */ + id?: string; + /** 会话ID */ + conversation_id?: string | null; + /** 消息角色:用户或助手 */ + role?: 'user' | 'assistant'; + /** 消息内容 */ + content?: string | null; + /** 创建时间 */ + created_at?: number | string +} + +/** + * 聊天组件主要属性接口 + */ +export interface ChatProps { + /** 空状态显示内容 */ + empty?: ReactNode; + /** 聊天数据列表 */ + data: ChatItem[]; + /** 输入内容变化回调 */ + onChange: (message: string) => void; + /** 发送消息回调 */ + onSend: () => void; + /** 流式加载状态 */ + streamLoading?: boolean; + /** 加载状态 */ + loading: boolean; + /** 内容区域自定义样式类名 */ + contentClassName?: string; + /** 子组件内容 */ + children?: ReactNode; + /** 标签格式化函数 */ + labelFormat: (item: ChatItem) => any; + errorDesc?: string; +} + +/** + * 聊天输入框组件属性接口 + */ +export interface ChatInputProps { + /** 当前输入消息 */ + message?: string; + /** 输入内容变化回调 */ + onChange: (message: string) => void; + /** 发送消息回调 */ + onSend: () => void; + /** 加载状态 */ + loading: boolean; + /** 子组件内容 */ + children?: ReactNode; +} + +/** + * 聊天内容区域组件属性接口 + */ +export interface ChatContentProps { + /** 自定义样式类名 */ + classNames?: string | Record; + contentClassNames?: string | Record; + /** 聊天数据列表 */ + data: ChatItem[]; + /** 流式加载状态 */ + streamLoading: boolean; + /** 空状态显示内容 */ + empty?: ReactNode; + /** 标签位置:顶部或底部 */ + labelPosition?: 'top' | 'bottom'; + /** 标签格式化函数 */ + labelFormat: (item: ChatItem) => any; + errorDesc?: string; +} \ No newline at end of file