Files
lingji-work-fe/src/components/xt-chat/chat-view/useChatHandler.tsx
2025-09-09 14:34:30 +08:00

424 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
IconCaretDown,
IconRefresh,
IconCopy,
} from '@arco-design/web-vue/es/icon';
import { Tooltip } from 'ant-design-vue';
import TextOverTips from '@/components/text-over-tips/index.vue';
import { genRandomId, exactFormatTime } 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,
INTELLECTUAL_THINKING_ROLE,
LOADING_ROLE,
ROLE_STYLE,
EnumTeamRunStatus,
REMOTE_USER_ROLE,
REMOTE_ASSISTANT_ROLE,
FILE_TYPE_MAP,
} from './constants';
import type { UseChatHandlerReturn, UseChatHandlerOptions } from './constants';
/**
* 聊天处理器Hook
* @returns 包含角色配置、消息处理函数和对话列表的对象
*/
export default function useChatHandler(options: UseChatHandlerOptions): UseChatHandlerReturn {
const { initSse } = options;
// 在内部定义对话列表
const { copy } = useClipboard();
const senderRef = ref(null);
const conversationList = ref<MESSAGE.Answer[]>([]);
const generateLoading = ref<boolean>(false);
const generateTeamRunTaskId = ref<string | null>(null);
const showRightView = ref(false);
const rightViewDataSource = ref<any>([]);
const rightPreviewData = ref<any>([]);
// 初始化markdown
const md = markdownit({
html: true,
breaks: true,
linkify: true,
typographer: true,
});
// 定义角色配置
const roles: BubbleListProps['roles'] = {
[LOADING_ROLE]: {
placement: 'start',
variant: 'borderless',
style: { ...ROLE_STYLE, paddingLeft: '20px' },
},
[INTELLECTUAL_THINKING_ROLE]: {
placement: 'start',
variant: 'borderless',
typing: { step: 2, interval: 100 },
style: ROLE_STYLE,
},
[ANSWER_ROLE]: {
placement: 'start',
variant: 'borderless',
typing: { step: 2, interval: 100 },
// onTypingComplete: () => {
// generateTeamRunTaskId.value = null;
// },
style: ROLE_STYLE,
},
[QUESTION_ROLE]: {
placement: 'end',
shape: 'round',
style: ROLE_STYLE,
messageRender: (message: string) => {
return <div class="max-w-400px">
{message}
</div>;
},
},
[REMOTE_USER_ROLE]: {
placement: 'end',
shape: 'round',
style: ROLE_STYLE,
messageRender: (message: string) => {
return <div class="max-w-600px">
{message}
</div>;
},
},
[REMOTE_ASSISTANT_ROLE]: {
placement: 'start',
variant: 'borderless',
style: ROLE_STYLE,
messageRender: (message: string) => {
return <div class="max-w-600px markdown-wrap" v-html={md.render(message)} />;
},
footer: (params) => {
const { content, item } = params as { content: string; item: MESSAGE.Answer };
const isShow = conversationList.value[conversationList.value.length - 1].run_id === item.run_id;
return (
<div class="flex items-center">
<Tooltip title="复制" onClick={() => onCopy(content)} align={{ offset: [0, 4] }} >
<div class="action-box">
<IconCopy size={16} class="color-#737478" />
</div>
</Tooltip>
{isShow && (
<Tooltip title="重新生成" onClick={() => handleRemoteRefresh(item)} align={{ offset: [0, 4] }}>
<div class="action-box ml-12px">
<IconRefresh size={16} class="color-#737478 " />
</div>
</Tooltip>
)}
</div>
);
},
},
};
// 下载处理
const onDownload = () => {
console.log('onDownload', rightViewDataSource.value);
};
const onCopy = (content: string) => {
copy(content);
antdMessage.success('复制成功!');
};
// 重置生成状态
const resetGenerateStatus = () => {
generateLoading.value = false;
generateTeamRunTaskId.value = null;
};
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 });
};
const onRefresh = (run_id: string) => {
generateLoading.value = true;
const targetIndex = conversationList.value.findIndex((v) => v.teamRunTaskId === run_id);
conversationList.value = conversationList.value.filter((item) => item.teamRunTaskId !== run_id);
const message = conversationList.value[targetIndex - 1]?.content;
initSse({ message });
};
const getAllRunTask = (teamRunTaskId: string) => {
return conversationList.value.filter(
(item) => item.role === ANSWER_ROLE && item.teamRunTaskId === teamRunTaskId && !item.isTeamRunTask,
);
};
const getRunTask = (run_id: string) => {
return conversationList.value.find((item) => item.run_id === run_id && !item.isTeamRunTask);
};
// 设置当前对话所有思考过程任务展开收起状态
const setRunTaskCollapse = (teamRunTaskId: string, isCollapse: boolean) => {
getAllRunTask(teamRunTaskId).forEach((item) => {
item.content.isCollapse = isCollapse;
});
};
// 获取同一个对话下的最后一个run_task
const getLastRunTask = (teamRunTaskId: string) => {
const allRunTask = getAllRunTask(teamRunTaskId);
return allRunTask[allRunTask.length - 1] ?? {};
};
const getFirstRunTask = (teamRunTaskId: string) => {
const allRunTask = getAllRunTask(teamRunTaskId);
return allRunTask[0] ?? {};
};
// 判断当前对话是否含有过程任务
const hasRunTask = (teamRunTaskId: string) => {
return conversationList.value.some((item) => item.teamRunTaskId === teamRunTaskId && !item.isTeamRunTask);
};
const getTeamRunTask = (teamRunTaskId: string) => {
return conversationList.value.find((item) => item.teamRunTaskId === teamRunTaskId);
};
const isLastRunTask = (data: MESSAGE.Answer): boolean => {
const { teamRunTaskId, run_id } = data;
return getLastRunTask(teamRunTaskId).run_id === run_id;
};
const isFirstRunTask = (data: MESSAGE.Answer): boolean => {
const { teamRunTaskId, run_id } = data;
return getFirstRunTask(teamRunTaskId).run_id === run_id;
};
// 过程节点开始
const handleRunTaskStart = (data: MESSAGE.Answer) => {
const { run_id } = data;
// generateTeamRunTaskId.value = run_id;
conversationList.value.push({
run_id,
key: run_id,
teamRunTaskId: generateTeamRunTaskId.value,
content: { ...data, runStatus: EnumTeamRunStatus.RunStarted, teamRunTaskId: generateTeamRunTaskId.value },
output: data.output,
role: ANSWER_ROLE,
messageRender: (data: MESSAGE.Answer) => {
const { node, output, runStatus, isCollapse = true, customRender, teamRunTaskId } = data;
const isRulCompleted = runStatus === EnumTeamRunStatus.RunCompleted;
let outputEleClass: string = `thought-chain-output border-l-#E6E6E8 border-l-1px pl-12px relative left-8px mb-4px markdown-wrap`;
!isLastRunTask(data) && (outputEleClass += ' hasLine pb-12px pt-4px');
return (
<>
{isFirstRunTask(data) && (
<div
class="flex items-center mb-8px cursor-pointer"
onClick={() => setRunTaskCollapse(teamRunTaskId, !isCollapse)}
>
<span class="font-family-medium color-#211F24 text-14px font-400 lh-22px mr-4px"></span>
{isCollapse ? (
<IconCaretUp size={12} class="color-#211F24 " />
) : (
<IconCaretDown size={12} class="color-#211F24" />
)}
</div>
)}
<div class="relative thought-chain-item" style={{ display: isCollapse ? 'block' : 'none' }}>
<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>
{customRender?.()}
</>
);
},
});
};
// 过程节点更新
const handleRunTaskUpdate = (data: MESSAGE.Answer) => {
const { run_id, output } = data;
const existingItem = conversationList.value.find((item) => item.run_id === run_id);
if (existingItem && output) {
existingItem.content.output += output;
existingItem.content.runStatus = EnumTeamRunStatus.RunResponseContent;
}
};
// 过程节点结束
const handleRunTaskEnd = (data: MESSAGE.Answer) => {
const { output } = data;
const existingItem = getRunTask(data.run_id);
if (existingItem) {
existingItem.content.output += output;
existingItem.content.runStatus = EnumTeamRunStatus.RunCompleted;
}
};
// 任务开始
const handleTeamRunTaskStart = (data: MESSAGE.Answer) => {
const { run_id } = data;
generateTeamRunTaskId.value = run_id;
conversationList.value.push({
run_id,
isTeamRunTask: true,
teamRunTaskId: generateTeamRunTaskId.value,
key: run_id,
content: { ...data, teamRunStatus: EnumTeamRunStatus.TeamRunStarted, teamRunTaskId: run_id },
output: data.output,
role: ANSWER_ROLE,
messageRender: (data: MESSAGE.Answer) => {
return <div v-html={md.render(data.output ?? '')} class="markdown-wrap" />;
},
});
};
// 任务更新
const handleTeamRunTaskUpdate = (data: MESSAGE.Answer) => {
const { run_id, output } = data;
const existingItem = conversationList.value.find((item) => item.run_id === run_id);
if (existingItem && output) {
existingItem.content.output += output;
existingItem.content.teamRunStatus = EnumTeamRunStatus.TeamRunResponseContent;
}
};
// 任务结束
const handleTeamRunTaskEnd = (data: MESSAGE.Answer) => {
resetGenerateStatus();
const { run_id: teamRunTaskId, extra_data, output } = data;
const _hasRunTask = hasRunTask(teamRunTaskId);
const _targetTask = _hasRunTask ? getLastRunTask(teamRunTaskId) : getTeamRunTask(teamRunTaskId);
if (isEmpty(_targetTask)) {
return;
}
// 含有思考过程,折叠思考过程,展示结果
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;
}
_targetTask.content.customRender = () => {
return (
<>
<div v-html={md.render(output)} class="markdown-wrap" />
{_targetData && (
<div class="file-card mt-10px">
<IconFile class="w-24px h-24px mr-16px color-#6D4CFE" />
<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>
)}
</>
);
}
} else {
_targetTask.content.teamRunStatus = EnumTeamRunStatus.TeamRunCompleted;
}
_targetTask.footer = () => {
const isShow = conversationList.value[conversationList.value.length - 1].teamRunTaskId === teamRunTaskId;
return (
<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(_targetTask.content.output)} align={{ offset: [0, 4] }}>
<div class="action-box">
<IconCopy size={16} class="color-#737478" />
</div>
</Tooltip>
)}
{isShow && (
<Tooltip title="重新生成" onClick={() => onRefresh(teamRunTaskId)} align={{ offset: [0, 4] }}>
<div class="action-box ml-12px">
<IconRefresh size={16} class="color-#737478 " />
</div>
</Tooltip>
)}
</div>
);
};
};
// 消息处理主函数
const handleMessage = (parsedData: { event: string; data: MESSAGE.Answer }) => {
const { data } = parsedData;
const { status } = data;
switch (status) {
case EnumTeamRunStatus.RunStarted:
handleRunTaskStart(data);
break;
case EnumTeamRunStatus.RunResponseContent:
handleRunTaskUpdate(data);
break;
case EnumTeamRunStatus.RunCompleted:
handleRunTaskEnd(data);
break;
case EnumTeamRunStatus.TeamRunStarted:
handleTeamRunTaskStart(data);
break;
case EnumTeamRunStatus.TeamRunResponseContent:
handleTeamRunTaskUpdate(data);
break;
case EnumTeamRunStatus.TeamRunCompleted:
handleTeamRunTaskEnd(data);
break;
default:
break;
}
};
return {
roles,
senderRef,
generateTeamRunTaskId,
handleMessage,
generateLoading,
conversationList,
showRightView,
rightViewDataSource,
rightPreviewData
};
}