Files
lingji-work-fe/src/components/xt-chat/chat-view/useChatHandler.tsx

401 lines
14 KiB
TypeScript
Raw Normal View History

import { ref } from 'vue';
2025-08-25 18:01:04 +08:00
import type { BubbleListProps } from '@/components/xt-chat/xt-bubble/types';
import markdownit from 'markdown-it';
2025-08-27 17:41:10 +08:00
import { message as antdMessage } from 'ant-design-vue';
2025-09-25 15:26:42 +08:00
import SvgIcon from '@/components/svg-icon/index.vue';
2025-08-25 18:01:04 +08:00
import { Tooltip } from 'ant-design-vue';
import TextOverTips from '@/components/text-over-tips/index.vue';
2025-08-27 17:41:10 +08:00
import { genRandomId, exactFormatTime } from '@/utils/tools';
2025-08-25 18:01:04 +08:00
import icon1 from '@/assets/img/agent/icon-end.png';
import icon2 from '@/assets/img/agent/icon-loading.png';
2025-09-10 11:37:23 +08:00
import icon3 from '@/assets/img/agent/icon-unfold.png';
import icon4 from '@/assets/img/agent/icon-fold.png';
2025-08-25 18:01:04 +08:00
import { useClipboard } from '@vueuse/core';
import {
QUESTION_ROLE,
ANSWER_ROLE,
INTELLECTUAL_THINKING_ROLE,
LOADING_ROLE,
ROLE_STYLE,
EnumTeamRunStatus,
2025-08-27 18:07:36 +08:00
REMOTE_USER_ROLE,
REMOTE_ASSISTANT_ROLE,
2025-08-27 17:41:10 +08:00
FILE_TYPE_MAP,
} from './constants';
2025-08-28 14:46:57 +08:00
import type { UseChatHandlerReturn, UseChatHandlerOptions } from './constants';
2025-08-25 18:01:04 +08:00
/**
* Hook
* @returns
2025-08-25 18:01:04 +08:00
*/
2025-08-28 14:46:57 +08:00
export default function useChatHandler(options: UseChatHandlerOptions): UseChatHandlerReturn {
const { initSse } = options;
2025-08-25 18:01:04 +08:00
// 在内部定义对话列表
const { copy } = useClipboard();
const senderRef = ref(null);
2025-08-28 14:46:57 +08:00
const conversationList = ref<MESSAGE.Answer[]>([]);
2025-08-25 23:09:08 +08:00
const generateLoading = ref<boolean>(false);
const generateTeamRunTaskId = ref<string | null>(null);
2025-08-28 15:45:09 +08:00
2025-08-25 18:01:04 +08:00
const showRightView = ref(false);
2025-08-29 15:49:50 +08:00
const rightViewDataSource = ref<any>([]);
const rightPreviewData = ref<any>([]);
2025-08-25 18:01:04 +08:00
// 初始化markdown
2025-08-25 18:01:04 +08:00
const md = markdownit({
html: true,
breaks: true,
linkify: true,
typographer: true,
});
// 定义角色配置
2025-08-25 18:01:04 +08:00
const roles: BubbleListProps['roles'] = {
[LOADING_ROLE]: {
placement: 'start',
variant: 'borderless',
2025-09-10 11:37:23 +08:00
style: ROLE_STYLE,
},
[INTELLECTUAL_THINKING_ROLE]: {
placement: 'start',
variant: 'borderless',
typing: { step: 2, interval: 100 },
style: ROLE_STYLE,
},
2025-08-25 18:01:04 +08:00
[ANSWER_ROLE]: {
placement: 'start',
variant: 'borderless',
typing: { step: 2, interval: 100 },
style: ROLE_STYLE,
2025-08-25 18:01:04 +08:00
},
2025-08-27 18:07:36 +08:00
[QUESTION_ROLE]: {
placement: 'end',
shape: 'round',
style: ROLE_STYLE,
2025-08-29 15:49:50 +08:00
messageRender: (message: string) => {
2025-09-09 15:59:31 +08:00
return <div style={{ 'max-width': 'var(--max-question-width)' }}>{message}</div>;
2025-08-29 15:49:50 +08:00
},
2025-08-25 18:01:04 +08:00
},
2025-08-27 18:07:36 +08:00
[REMOTE_USER_ROLE]: {
2025-08-25 18:01:04 +08:00
placement: 'end',
shape: 'round',
style: ROLE_STYLE,
2025-08-29 15:49:50 +08:00
messageRender: (message: string) => {
2025-09-09 15:59:31 +08:00
return <div style={{ 'max-width': 'var(--max-question-width)' }}>{message}</div>;
2025-08-29 15:49:50 +08:00
},
2025-08-25 18:01:04 +08:00
},
2025-08-27 18:07:36 +08:00
[REMOTE_ASSISTANT_ROLE]: {
placement: 'start',
variant: 'borderless',
style: ROLE_STYLE,
2025-08-27 18:12:14 +08:00
messageRender: (message: string) => {
2025-09-09 15:59:31 +08:00
return (
<div class="markdown-wrap" style={{ 'max-width': 'var(--max-content-width)' }} v-html={md.render(message)} />
);
2025-08-28 12:03:52 +08:00
},
footer: (params) => {
const { content, item } = params as { content: string; item: MESSAGE.Answer };
2025-09-10 11:37:23 +08:00
const isLastRunTask = conversationList.value[conversationList.value.length - 1].run_id === item.run_id;
2025-08-28 12:03:52 +08:00
return (
<div class="flex items-center">
2025-09-09 15:59:31 +08:00
<Tooltip title="复制" onClick={() => onCopy(content)} align={{ offset: [0, 4] }}>
2025-09-25 15:26:42 +08:00
<div class="action-box flex items-center">
<SvgIcon name="xt-copy" size="16" class="color-#737478"/>
</div>
2025-08-28 12:03:52 +08:00
</Tooltip>
2025-09-10 11:37:23 +08:00
{isLastRunTask && (
<Tooltip title="重新生成" onClick={() => handleRemoteRefresh(item)} align={{ offset: [0, 4] }}>
2025-09-25 15:26:42 +08:00
<div class="action-box ml-12px flex items-center">
<SvgIcon name="xt-refresh" size="16" class="color-#737478"/>
</div>
2025-08-28 12:03:52 +08:00
</Tooltip>
)}
</div>
);
2025-08-27 18:12:14 +08:00
},
2025-08-27 18:07:36 +08:00
},
2025-08-25 18:01:04 +08:00
};
// 下载处理
const onDownload = () => {
2025-08-29 15:49:50 +08:00
console.log('onDownload', rightViewDataSource.value);
2025-08-25 18:01:04 +08:00
};
const onCopy = (content: string) => {
copy(content);
antdMessage.success('复制成功!');
};
// 重置生成状态
const resetGenerateStatus = () => {
generateLoading.value = false;
generateTeamRunTaskId.value = null;
};
2025-08-28 12:03:52 +08:00
const handleRemoteRefresh = (item: MESSAGE.Answer) => {
generateLoading.value = true;
const targetIndex = conversationList.value.findIndex(
(v) => v.teamRunTaskId === item.teamRunTaskId && v.run_id === item.run_id && v.role === REMOTE_ASSISTANT_ROLE,
);
const message = conversationList.value[targetIndex - 1]?.content;
conversationList.value.splice(targetIndex, 1);
initSse({ message });
};
2025-09-10 11:37:23 +08:00
const onRefresh = (teamRunTaskId: string) => {
2025-08-27 12:04:23 +08:00
generateLoading.value = true;
2025-08-28 12:03:52 +08:00
2025-09-10 11:37:23 +08:00
const targetIndex = conversationList.value.findIndex((v) => v.teamRunTaskId === teamRunTaskId);
conversationList.value = conversationList.value.filter((item) => item.teamRunTaskId !== teamRunTaskId);
2025-08-28 12:03:52 +08:00
const message = conversationList.value[targetIndex - 1]?.content;
initSse({ message });
2025-08-27 12:04:23 +08:00
};
2025-09-10 11:37:23 +08:00
const getAllTeamRunTask = (teamRunTaskId: string) => {
return conversationList.value.filter((item) => item.role === ANSWER_ROLE && item.teamRunTaskId === teamRunTaskId);
2025-08-27 17:18:47 +08:00
};
2025-08-28 12:03:52 +08:00
// 设置当前对话所有思考过程任务展开收起状态
2025-09-10 11:37:23 +08:00
const setRunTaskCollapse = (teamRunTaskId: string, isExpand: boolean) => {
getAllTeamRunTask(teamRunTaskId).forEach((item) => {
item.content.isExpand = isExpand;
2025-08-27 17:18:47 +08:00
});
};
const getTeamRunTask = (teamRunTaskId: string) => {
return conversationList.value.find((item) => item.teamRunTaskId === teamRunTaskId);
};
2025-08-27 12:04:23 +08:00
// 过程节点开始
const handleRunTaskStart = (data: MESSAGE.Answer) => {
const { run_id } = data;
2025-09-10 11:37:23 +08:00
const _intelligentThinkingData = getTeamRunTask(generateTeamRunTaskId.value)?.content?.intelligentThinkingData;
const _target = _intelligentThinkingData?.find((item: MESSAGE.Answer) => item.run_id === run_id);
if (!_target) {
_intelligentThinkingData?.push(data);
}
2025-08-25 18:01:04 +08:00
};
2025-08-27 12:04:23 +08:00
// 过程节点更新
const handleRunTaskUpdate = (data: MESSAGE.Answer) => {
const { run_id, output } = data;
2025-09-10 11:37:23 +08:00
const _intelligentThinkingData = getTeamRunTask(generateTeamRunTaskId.value)?.content?.intelligentThinkingData;
const _target = _intelligentThinkingData?.find((item: MESSAGE.Answer) => item.run_id === run_id);
2025-08-25 18:01:04 +08:00
2025-09-10 11:37:23 +08:00
if (_target) {
_target.runStatus = EnumTeamRunStatus.RunResponseContent;
_target.output += output;
2025-08-27 12:04:23 +08:00
}
2025-08-25 18:01:04 +08:00
};
2025-08-27 12:04:23 +08:00
// 过程节点结束
const handleRunTaskEnd = (data: MESSAGE.Answer) => {
2025-09-10 11:37:23 +08:00
const { run_id, output } = data;
const _intelligentThinkingData = getTeamRunTask(generateTeamRunTaskId.value)?.content?.intelligentThinkingData;
const _target = _intelligentThinkingData?.find((item: MESSAGE.Answer) => item.run_id === run_id);
if (_target) {
_target.runStatus = EnumTeamRunStatus.RunCompleted;
_target.output += output;
}
};
2025-08-25 18:01:04 +08:00
2025-09-10 11:37:23 +08:00
const renderThoughtChain = (data: MESSAGE.Answer, index: number, messageData: MESSAGE.Answer) => {
const { node, output, runStatus } = data;
const isRulCompleted = runStatus === EnumTeamRunStatus.RunCompleted;
2025-08-27 12:04:23 +08:00
2025-09-10 11:37:23 +08:00
let outputEleClass: string = `thought-chain-output border-l-#E6E6E8 border-l-1px pl-12px relative left-8px mb-4px markdown-wrap`;
index === messageData.intelligentThinkingData.length - 1 && (outputEleClass += ' hasLine pb-12px pt-4px');
return (
<div class="relative thought-chain-item">
<div class="flex items-center mb-4px">
<img src={isRulCompleted ? icon1 : icon2} width={16} height={16} class="mr-4px" />
<div class="color-#211F24 !lh-20px">{node}</div>
</div>
<div v-html={md.render(output)} class={outputEleClass} />
</div>
);
2025-08-25 18:01:04 +08:00
};
2025-08-27 12:04:23 +08:00
// 任务开始
const handleTeamRunTaskStart = (data: MESSAGE.Answer) => {
2025-09-10 11:37:23 +08:00
const { run_id: teamRunTaskId, output } = data;
generateTeamRunTaskId.value = teamRunTaskId;
2025-08-27 12:04:23 +08:00
conversationList.value.push({
2025-09-10 11:37:23 +08:00
teamRunTaskId,
key: teamRunTaskId,
output,
2025-08-27 12:04:23 +08:00
role: ANSWER_ROLE,
2025-09-10 11:37:23 +08:00
content: {
...data,
teamRunStatus: EnumTeamRunStatus.TeamRunStarted,
teamRunTaskId,
intelligentThinkingData: [], // 智能思考过程数据
isExpand: true, // 是否展开思考过程
},
messageRender: (messageData: MESSAGE.Answer) => {
const { output, isExpand, teamRunTaskId, intelligentThinkingData, customRender, teamRunStatus } = messageData;
const isEnd = teamRunStatus === EnumTeamRunStatus.TeamRunCompleted;
const hasIntelligentThinking = intelligentThinkingData.length > 0;
return (
<>
<section
class={`intelligent-thinking-wrap mb-8px flex-col ${
hasIntelligentThinking && !isEnd ? 'max-h-160px overflow-hidden' : ''
}`}
style={{ display: hasIntelligentThinking ? 'flex' : 'none' }}
>
<div class="intelligent-thinking-header flex justify-between">
<span class="cts font-family-regular color-#8A70FE"></span>
<img
src={isExpand ? icon4 : icon3}
class="cursor-pointer"
width={24}
height={24}
onClick={() => setRunTaskCollapse(teamRunTaskId, !isExpand)}
/>
</div>
<div
class="intelligent-thinking-content px-16px flex-1 overflow-y-auto"
style={{ display: isExpand ? 'block' : 'none' }}
>
{intelligentThinkingData.map((item: MESSAGE.Answer, index: number) =>
renderThoughtChain(item, index, messageData),
)}
</div>
</section>
<div v-html={md.render(output ?? '')} class="markdown-wrap" />
{customRender?.()}
</>
);
2025-08-27 12:04:23 +08:00
},
});
2025-08-25 18:01:04 +08:00
};
2025-08-27 12:04:23 +08:00
// 任务更新
const handleTeamRunTaskUpdate = (data: MESSAGE.Answer) => {
2025-09-10 11:37:23 +08:00
const { run_id: teamRunTaskId, output } = data;
const existingItem = conversationList.value.find((item) => item.teamRunTaskId === teamRunTaskId);
if (existingItem) {
existingItem.content.output += output;
2025-08-27 17:18:47 +08:00
existingItem.content.teamRunStatus = EnumTeamRunStatus.TeamRunResponseContent;
}
};
2025-08-27 12:04:23 +08:00
// 任务结束
const handleTeamRunTaskEnd = (data: MESSAGE.Answer) => {
2025-08-27 17:18:47 +08:00
resetGenerateStatus();
2025-08-28 15:45:09 +08:00
const { run_id: teamRunTaskId, extra_data, output } = data;
2025-09-10 11:37:23 +08:00
const existingItem = conversationList.value.find((item) => item.teamRunTaskId === teamRunTaskId);
if (existingItem) {
existingItem.content.extra_data = extra_data;
existingItem.content.output += output;
existingItem.content.teamRunStatus = EnumTeamRunStatus.TeamRunCompleted;
const _hasRunTask = existingItem.content.intelligentThinkingData.length > 0;
if (_hasRunTask) {
setRunTaskCollapse(teamRunTaskId, false);
const _targetData = extra_data?.data?.find((item: any) => item.task_type === '任务管理');
if (_targetData) {
showRightView.value = true;
rightViewDataSource.value = extra_data.data;
rightPreviewData.value = _targetData;
}
existingItem.content.customRender = () => {
return (
<>
{_targetData && (
<div class="file-card mt-10px">
2025-09-25 15:26:42 +08:00
<SvgIcon name="xt-file" size="14" class="color-#6D4CFE w-24px h-24px mr-16px"/>
2025-09-10 11:37:23 +08:00
<div>
<TextOverTips
context={FILE_TYPE_MAP?.[_targetData.file_type] ?? '-'}
class="font-family-medium color-#211F24 text-14px font-400 lh-22px mb-4px"
/>
<span class="color-#939499 font-family-regular text-12px font-400 lh-22px">
{exactFormatTime(dayjs().unix())}
</span>
</div>
</div>
)}
</>
);
};
2025-08-28 12:03:52 +08:00
}
2025-09-10 11:37:23 +08:00
existingItem.footer = () => {
const isLastRunTask = conversationList.value[conversationList.value.length - 1].teamRunTaskId === teamRunTaskId;
2025-08-29 14:52:04 +08:00
return (
2025-09-10 11:37:23 +08:00
<div class="flex items-center">
{!extra_data && (
// ? (
// <Tooltip title="下载" onClick={onDownload} align={{ offset: [0, 4] }}>
// <div class="action-box">
// <IconDownload size={16} class="color-#737478 mr-12px" />
// </div>
// </Tooltip>
// ) :
<Tooltip title="复制" onClick={() => onCopy(existingItem.content.output)} align={{ offset: [0, 4] }}>
<div class="action-box">
2025-09-25 15:26:42 +08:00
<SvgIcon name="xt-copy" size="16" class="color-#737478"/>
2025-08-29 14:52:04 +08:00
</div>
2025-09-10 11:37:23 +08:00
</Tooltip>
2025-08-29 14:52:04 +08:00
)}
2025-09-10 11:37:23 +08:00
{isLastRunTask && (
<Tooltip title="重新生成" onClick={() => onRefresh(teamRunTaskId)} align={{ offset: [0, 4] }}>
2025-09-25 15:26:42 +08:00
<div class="action-box ml-12px flex items-center">
<SvgIcon name="xt-refresh" size="16" class="color-#737478"/>
2025-09-10 11:37:23 +08:00
</div>
</Tooltip>
)}
</div>
2025-08-29 14:52:04 +08:00
);
2025-09-09 15:59:31 +08:00
};
}
};
2025-08-27 12:04:23 +08:00
// 消息处理主函数
const handleMessage = (parsedData: { event: string; data: MESSAGE.Answer }) => {
const { data } = parsedData;
const { status } = data;
switch (status) {
2025-08-27 12:04:23 +08:00
case EnumTeamRunStatus.RunStarted:
handleRunTaskStart(data);
break;
case EnumTeamRunStatus.RunResponseContent:
handleRunTaskUpdate(data);
break;
case EnumTeamRunStatus.RunCompleted:
handleRunTaskEnd(data);
break;
case EnumTeamRunStatus.TeamRunStarted:
2025-08-27 12:04:23 +08:00
handleTeamRunTaskStart(data);
2025-08-25 18:01:04 +08:00
break;
case EnumTeamRunStatus.TeamRunResponseContent:
2025-08-27 12:04:23 +08:00
handleTeamRunTaskUpdate(data);
2025-08-25 18:01:04 +08:00
break;
case EnumTeamRunStatus.TeamRunCompleted:
2025-08-27 12:04:23 +08:00
handleTeamRunTaskEnd(data);
2025-08-25 18:01:04 +08:00
break;
default:
break;
}
};
return {
roles,
senderRef,
generateTeamRunTaskId,
2025-08-25 18:01:04 +08:00
handleMessage,
generateLoading,
conversationList,
showRightView,
2025-08-29 15:49:50 +08:00
rightViewDataSource,
2025-09-09 15:59:31 +08:00
rightPreviewData,
2025-08-25 18:01:04 +08:00
};
}