From 6926a43d8ad3115cfd6df021b9dd7dc973d6f5b8 Mon Sep 17 00:00:00 2001 From: rd <1344903914@qq.com> Date: Mon, 25 Aug 2025 18:01:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20chat=E9=A6=96=E9=A1=B5=E6=B8=B2?= =?UTF-8?q?=E6=9F=93=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/img/agent/icon-end.png | Bin 0 -> 777 bytes src/assets/img/agent/icon-loading.png | Bin 0 -> 702 bytes .../components/right-view/index.vue} | 0 .../components/sender-input/index.vue | 105 ++++++ src/components/xt-chat/chat-view/constants.ts | 31 ++ src/components/xt-chat/chat-view/index.vue | 138 ++++++++ .../xt-chat/chat-view}/style.scss | 13 +- .../xt-chat/chat-view/useChatHandler.tsx | 228 +++++++++++++ src/stores/modules/chat/index.ts | 9 +- src/types/chat.ts | 7 + src/utils/querySSE.ts | 4 +- .../components/conversation-detail/index.vue | 322 ------------------ src/views/home/components/created/index.vue | 3 - src/views/home/index.vue | 27 +- 14 files changed, 544 insertions(+), 343 deletions(-) create mode 100644 src/assets/img/agent/icon-end.png create mode 100644 src/assets/img/agent/icon-loading.png rename src/{views/home/components/conversation-detail/rightView.vue => components/xt-chat/chat-view/components/right-view/index.vue} (100%) create mode 100644 src/components/xt-chat/chat-view/components/sender-input/index.vue create mode 100644 src/components/xt-chat/chat-view/constants.ts create mode 100644 src/components/xt-chat/chat-view/index.vue rename src/{views/home/components/conversation-detail => components/xt-chat/chat-view}/style.scss (65%) create mode 100644 src/components/xt-chat/chat-view/useChatHandler.tsx create mode 100644 src/types/chat.ts delete mode 100644 src/views/home/components/conversation-detail/index.vue diff --git a/src/assets/img/agent/icon-end.png b/src/assets/img/agent/icon-end.png new file mode 100644 index 0000000000000000000000000000000000000000..894073e6309cf15f12837c89269c0dd89ee77043 GIT binary patch literal 777 zcmV+k1NQuhP)#PbfY?(-UNdkb0oZIGiB#1kDY|4e$~C0ZeBi zOY8;+c5FGe6TaH_V@tC4J!z%g6~JQ$LJjzS;3%aWV~m3+A^;P$T5{R%_amr?(gLQ_ zkH-eFr?oz#pB#&^5D*s)5nm4mgRqR4c7ZXcuqQcfv=RGRt&Y0uz_bNK^CDNk zcpS8ge9y}YCCL9aWm8?|+2cMA-v_C42SpDy1`eLy3Ey5tNY8qVf@W5*N-Kp680;rBQ0V76I_8srR(Cdvpvl%eeh z_8SMMqzHxTqz?HSvCaxmQiVnTY>W8glIrBlrJiNufJbRF@7NTHs6$RR!els>sX=4z z$QPGTPDEE}*{ed%ph&n#wUZ4c#0*M+MJOe{RwZYws7SF2@P5bW&rk+6>IANmysZi; zlMN|lzNJUy;&K<*_uI426|mkQMxVohfr3ou)Q7nx8CNdWuv-IyYz<%M*M-)akb@1a z+qv0sG>foKU{s2^Y|AFD$N*QfaYJ{Z?8Z!hH(cCmcJk`L)OS!4lnfjhixI8J^P-Ch zU0&{JR-ti5SG0SJt2u-G?QYt#ZKq|o{E|(Z{bqOOo1K|&#=*dsx|GD>xkM;QfY1pR)8r^~8VH(j zXj10w!?OE0?%i(G;(lGW_*3N-VmjKqb{>`8YfNGQcN|87U9R3oHnam^gC1!QE(9sc zU!FT0W~f{VBfj(Com2)F`aGZl{EoZG5$<@4o_KS~0Cl_Wd{}^GMki1o#GfPi1QF24 zUAaOXKEAqm1G>>3VpcG|x;hgNR55$JPWOpLhE_Ci4QOi4F(4|V%TYi7{{cr|1t&k? zFXhe>uv!EPYZJVPFcvEAb6pyItYmrM>@V=4COG?(zP@k^cv5j#xD6XD*@2s&;EYn< zYoL0`NEpDFddJ^JEAKVps3kB#-2sezZ^6vTPrIzyfZxAhCJxR_4af_#0n2w`W^u_b zYd2s8X6_a3^&rsG$s)K~wB@(fg2EbXbWTR!HjU-x{P-dh7a}V>eVcPG!k)GJQ3cOl zIMxJBOGz?A<70Wn(e=QMd6*GF>)H<^@2`Wp9Lp^G0r*y|y|00VO> +import { ref } from 'vue'; +import { Sender } from 'ant-design-x-vue'; +import { Tooltip } from 'ant-design-vue'; + +interface SenderInputProps { + modelValue?: string; + loading?: boolean; + placeholder?: string; +} + +export default { + name: 'SenderInput', + props: { + modelValue: { + type: String, + default: '', + }, + placeholder: { + type: String, + default: '随时告诉我你想做什么,比如查数据、发任务、写内容,我会立刻帮你完成。', + }, + loading: { + type: Boolean, + default: false, + }, + }, + emits: ['update:modelValue', 'submit', 'cancel'], + setup(props: SenderInputProps, { emit, expose }) { + const senderRef = ref(null); + const localSearchValue = ref(props.modelValue); + + // 监听外部value变化 + watch( + () => props.modelValue, + (newValue) => { + localSearchValue.value = newValue || ''; + }, + ); + + const handleSubmit = () => { + emit('submit', localSearchValue.value); + }; + const handleCancel = () => { + emit('cancel'); + }; + + const focus = () => { + senderRef.value?.focus?.(); + }; + + const renderActions = () => { + if (props.loading) { + return ( + +
+
+
+
+ ); + } + + return ( +
+ +
+ ); + }; + + expose({ + focus, + }); + + return () => ( +
+ emit('update:modelValue', value)} + onSubmit={handleSubmit} + class="h-full w-full mb-24px" + placeholder={props.placeholder} + actions={() => renderActions()} + /> +
+ ); + }, +}; + + + diff --git a/src/components/xt-chat/chat-view/constants.ts b/src/components/xt-chat/chat-view/constants.ts new file mode 100644 index 0000000..727e493 --- /dev/null +++ b/src/components/xt-chat/chat-view/constants.ts @@ -0,0 +1,31 @@ +import type { Ref } from 'vue'; +import type { BubbleListProps } from '@/components/xt-chat/xt-bubble/types'; + +// 定义角色常量 +export const QUESTION_ROLE = 'question'; +export const ANSWER_ROLE = 'text'; +export const FILE_ROLE = 'file'; +export const THOUGHT_ROLE = 'thought'; + +export const ROLE_STYLE = { + width: '600px', + margin: '0 auto', +}; + +export const ANSWER_STYLE = { + ...ROLE_STYLE, + paddingLeft: '12px', + borderLeft: '1px solid #E6E6E8', + position: 'relative', + left: '6px', +}; + +export interface UseChatHandlerReturn { + roles: BubbleListProps['roles']; + currentTaskId: Ref; + handleMessage: (parsedData: { event: string; data: any }) => void; + generateLoading: Ref; + conversationList: Ref; + showRightView: Ref; + rightViewContent: Ref; +} diff --git a/src/components/xt-chat/chat-view/index.vue b/src/components/xt-chat/chat-view/index.vue new file mode 100644 index 0000000..64d1cc3 --- /dev/null +++ b/src/components/xt-chat/chat-view/index.vue @@ -0,0 +1,138 @@ + + + diff --git a/src/views/home/components/conversation-detail/style.scss b/src/components/xt-chat/chat-view/style.scss similarity index 65% rename from src/views/home/components/conversation-detail/style.scss rename to src/components/xt-chat/chat-view/style.scss index 1915663..faf50ca 100644 --- a/src/views/home/components/conversation-detail/style.scss +++ b/src/components/xt-chat/chat-view/style.scss @@ -1,4 +1,4 @@ -.conversation-detail-wrap { +.chat-view-wrap { .cts { color: var(--Text-1, #737478); font-size: 14px; @@ -16,5 +16,16 @@ justify-content: center; align-items: center; } + &.process-row { + position: relative; + &::after { + top: 0; + left: 0; + position: absolute; + width: 1px; + height: 100%; + background: #e6e6e8; + } + } } } diff --git a/src/components/xt-chat/chat-view/useChatHandler.tsx b/src/components/xt-chat/chat-view/useChatHandler.tsx new file mode 100644 index 0000000..ad19154 --- /dev/null +++ b/src/components/xt-chat/chat-view/useChatHandler.tsx @@ -0,0 +1,228 @@ +import { ref, } from 'vue'; + +import type { BubbleListProps } from '@/components/xt-chat/xt-bubble/types'; +import markdownit from 'markdown-it'; +import { message as antdMessage } from 'ant-design-vue'; +import { IconFile, IconCaretUp, IconDownload, IconRefresh } from '@arco-design/web-vue/es/icon'; + +import { Tooltip } from 'ant-design-vue'; +import TextOverTips from '@/components/text-over-tips/index.vue'; +import { genRandomId } from '@/utils/tools'; + +import icon1 from "@/assets/img/agent/icon-end.png" +import icon2 from "@/assets/img/agent/icon-loading.png" + +import { useClipboard } from '@vueuse/core'; +import { QUESTION_ROLE, ANSWER_ROLE, FILE_ROLE, THOUGHT_ROLE, ROLE_STYLE, ANSWER_STYLE } from './constants'; +import type { UseChatHandlerReturn } from "./constants" + +/** + * 聊天处理器Hook + * @returns 包含角色配置、消息处理函数和对话列表的对象 + */ +export default function useChatHandler(): UseChatHandlerReturn { + // 在内部定义对话列表 + const { copy } = useClipboard(); + + const conversationList = ref([]); + const generateLoading = ref(false); + const currentTaskId = ref(null); + const showRightView = ref(false); + const rightViewContent = ref(''); + + // 初始化markdown + const md = markdownit({ + html: true, + breaks: true, + linkify: true, + typographer: true, + }); + + // 定义角色配置 + const roles: BubbleListProps['roles'] = { + [ANSWER_ROLE]: { + placement: 'start', + variant: 'borderless', + typing: { step: 2, interval: 100 }, + onTypingComplete: () => { + currentTaskId.value = null; + }, + style: ROLE_STYLE + }, + [FILE_ROLE]: { + placement: 'start', + variant: 'borderless', + typing: { step: 2, interval: 100 }, + messageRender: (items) => { + return items.map((item) => ( +
+ +
+ + 创建时间:08-04 12:40 +
+
+ )); + }, + style: ROLE_STYLE + }, + [THOUGHT_ROLE]: { + placement: 'start', + variant: 'borderless', + style: ROLE_STYLE + }, + [QUESTION_ROLE]: { + placement: 'end', + shape: 'round', + style: ROLE_STYLE + }, + }; + + // 下载处理 + const onDownload = (content: string) => { + console.log('onDownload', content); + // 这里可以添加实际的下载逻辑 + }; + + const onCopy = (content: string) => { + copy(content); + antdMessage.success('复制成功!'); + }; + + // 开始处理 + const handleStart = (data: any) => { + const { run_id } = data; + conversationList.value.push({ + run_id, + role: ANSWER_ROLE, + content: ( +
+ 智能思考 + +
+ ), + }); + }; + + // 节点更新处理 + const handleNodeUpdate = (data: any) => { + const { run_id, status, output } = data; + + switch (status) { + case 'TeamRunResponseContent': + conversationList.value.push({ + run_id, + content: data, + role: ANSWER_ROLE, + messageRender: (item) => ( +
+ + {item.message} +
+ ) + }); + break; + case 'TeamRunCompleted': + conversationList.value.push({ + run_id, + content: output, + role: ANSWER_ROLE, + messageRender: (content: string) =>
, + style: ANSWER_STYLE + }); + break; + } + }; + + // 最终结果处理 + const handleFinalResult = (data: any) => { + const { run_id, output } = data; + + if (showRightView) { + showRightView.value = true; + } + const _files = output?.files; + rightViewContent.value = _files?.[0]?.content || ''; + + conversationList.value.push({ + run_id, + id: currentTaskId.value, + role: FILE_ROLE, + content: _files, + style: ANSWER_STYLE, + footer: ({ item }: { item: any }) => { + const nonQuestionElements = conversationList.value.filter((item) => item.role !== QUESTION_ROLE); + const isLastAnswer = nonQuestionElements[nonQuestionElements.length - 1]?.id === item.id; + + // return ( + //
+ // onDownload(rightViewContent?.value || '')}> + // + // + // {isLastAnswer && onRefresh && ( + // onRefresh(currentTaskId.value!, conversationList.value.length)}> + // + // + // )} + //
+ // ); + }, + }); + }; + + // 重置生成状态 + const resetGenerateStatus = () => { + generateLoading.value = false; + }; + + // 错误处理 + const handleError = () => { + resetGenerateStatus(); + antdMessage.error('连接服务器失败'); + }; + + const onRefresh = (tempId: string, tempIndex: number) => { + generateLoading.value = true; + conversationList.value.splice(tempIndex, 1, { + id: tempId, + loading: true, + }); + }; + + // 消息处理主函数 + const handleMessage = (parsedData: { event: string; data: any }) => { + const { event, data } = parsedData; + switch (event) { + case 'start': + handleStart(data); + break; + case 'node_update': + handleNodeUpdate(data); + break; + case 'final_result': + handleFinalResult(data); + break; + case 'end': + resetGenerateStatus(); + break; + case 'error': + handleError(); + break; + default: + break; + } + }; + + return { + roles, + currentTaskId, + handleMessage, + generateLoading, + conversationList, + showRightView, + rightViewContent, + }; +} \ No newline at end of file diff --git a/src/stores/modules/chat/index.ts b/src/stores/modules/chat/index.ts index 2c84a9c..b1ff24b 100644 --- a/src/stores/modules/chat/index.ts +++ b/src/stores/modules/chat/index.ts @@ -1,5 +1,6 @@ import { defineStore } from 'pinia'; -import { createSession } from '@/api/all/chat'; +import { createSession, getAgentInfo } from '@/api/all/chat'; + import { handleUserHome } from '@/utils/user'; interface ChatState { @@ -24,6 +25,12 @@ export const useChatStore = defineStore('chat', { clearSearchValue() { this.searchValue = ''; }, + async getAgentInfo() { + const { code, data } = await getAgentInfo(); + if (code === 200) { + this.setAgentInfo(data); + } + }, setAgentInfo(agentInfo: agentInfo) { this.agentInfo = agentInfo; }, diff --git a/src/types/chat.ts b/src/types/chat.ts new file mode 100644 index 0000000..2116a91 --- /dev/null +++ b/src/types/chat.ts @@ -0,0 +1,7 @@ +declare global { + namespace CHAT { + export type TInputInfo = { + message: string; + }; + } +} diff --git a/src/utils/querySSE.ts b/src/utils/querySSE.ts index e9cb27a..7850465 100644 --- a/src/utils/querySSE.ts +++ b/src/utils/querySSE.ts @@ -63,11 +63,11 @@ export default async (config: SSEConfig, url: string = DEFAULT_SSE_URL): Promise } }, onerror(error: Error) { - console.error('SSE error:', error); + // console.error('SSE error:', error); handleError?.(error); }, onclose() { - console.log('SSE connection closed'); + // console.log('SSE connection closed'); handleClose?.(); }, async onopen(response: Response) { diff --git a/src/views/home/components/conversation-detail/index.vue b/src/views/home/components/conversation-detail/index.vue deleted file mode 100644 index 91c586a..0000000 --- a/src/views/home/components/conversation-detail/index.vue +++ /dev/null @@ -1,322 +0,0 @@ - - - diff --git a/src/views/home/components/created/index.vue b/src/views/home/components/created/index.vue index 7ac72cd..3705d91 100644 --- a/src/views/home/components/created/index.vue +++ b/src/views/home/components/created/index.vue @@ -43,9 +43,6 @@ export default { senderRef.value?.focus(); }; - onMounted(() => { - chatStore.clearSearchValue(); - }); return () => (
diff --git a/src/views/home/index.vue b/src/views/home/index.vue index 9443500..48f1dac 100644 --- a/src/views/home/index.vue +++ b/src/views/home/index.vue @@ -1,38 +1,37 @@