Files
lingji-work-fe/src/components/xt-chat/chat-view/useChatHandler.tsx
2025-09-25 15:26:42 +08:00

401 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 SvgIcon from '@/components/svg-icon/index.vue';
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 icon3 from '@/assets/img/agent/icon-unfold.png';
import icon4 from '@/assets/img/agent/icon-fold.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,
},
[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 },
style: ROLE_STYLE,
},
[QUESTION_ROLE]: {
placement: 'end',
shape: 'round',
style: ROLE_STYLE,
messageRender: (message: string) => {
return <div style={{ 'max-width': 'var(--max-question-width)' }}>{message}</div>;
},
},
[REMOTE_USER_ROLE]: {
placement: 'end',
shape: 'round',
style: ROLE_STYLE,
messageRender: (message: string) => {
return <div style={{ 'max-width': 'var(--max-question-width)' }}>{message}</div>;
},
},
[REMOTE_ASSISTANT_ROLE]: {
placement: 'start',
variant: 'borderless',
style: ROLE_STYLE,
messageRender: (message: string) => {
return (
<div class="markdown-wrap" style={{ 'max-width': 'var(--max-content-width)' }} v-html={md.render(message)} />
);
},
footer: (params) => {
const { content, item } = params as { content: string; item: MESSAGE.Answer };
const isLastRunTask = 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 flex items-center">
<SvgIcon name="xt-copy" size="16" class="color-#737478"/>
</div>
</Tooltip>
{isLastRunTask && (
<Tooltip title="重新生成" onClick={() => handleRemoteRefresh(item)} align={{ offset: [0, 4] }}>
<div class="action-box ml-12px flex items-center">
<SvgIcon name="xt-refresh" 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 = (teamRunTaskId: string) => {
generateLoading.value = true;
const targetIndex = conversationList.value.findIndex((v) => v.teamRunTaskId === teamRunTaskId);
conversationList.value = conversationList.value.filter((item) => item.teamRunTaskId !== teamRunTaskId);
const message = conversationList.value[targetIndex - 1]?.content;
initSse({ message });
};
const getAllTeamRunTask = (teamRunTaskId: string) => {
return conversationList.value.filter((item) => item.role === ANSWER_ROLE && item.teamRunTaskId === teamRunTaskId);
};
// 设置当前对话所有思考过程任务展开收起状态
const setRunTaskCollapse = (teamRunTaskId: string, isExpand: boolean) => {
getAllTeamRunTask(teamRunTaskId).forEach((item) => {
item.content.isExpand = isExpand;
});
};
const getTeamRunTask = (teamRunTaskId: string) => {
return conversationList.value.find((item) => item.teamRunTaskId === teamRunTaskId);
};
// 过程节点开始
const handleRunTaskStart = (data: MESSAGE.Answer) => {
const { run_id } = data;
const _intelligentThinkingData = getTeamRunTask(generateTeamRunTaskId.value)?.content?.intelligentThinkingData;
const _target = _intelligentThinkingData?.find((item: MESSAGE.Answer) => item.run_id === run_id);
if (!_target) {
_intelligentThinkingData?.push(data);
}
};
// 过程节点更新
const handleRunTaskUpdate = (data: MESSAGE.Answer) => {
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.RunResponseContent;
_target.output += output;
}
};
// 过程节点结束
const handleRunTaskEnd = (data: MESSAGE.Answer) => {
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;
}
};
const renderThoughtChain = (data: MESSAGE.Answer, index: number, messageData: MESSAGE.Answer) => {
const { node, output, runStatus } = 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`;
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>
);
};
// 任务开始
const handleTeamRunTaskStart = (data: MESSAGE.Answer) => {
const { run_id: teamRunTaskId, output } = data;
generateTeamRunTaskId.value = teamRunTaskId;
conversationList.value.push({
teamRunTaskId,
key: teamRunTaskId,
output,
role: ANSWER_ROLE,
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?.()}
</>
);
},
});
};
// 任务更新
const handleTeamRunTaskUpdate = (data: MESSAGE.Answer) => {
const { run_id: teamRunTaskId, output } = data;
const existingItem = conversationList.value.find((item) => item.teamRunTaskId === teamRunTaskId);
if (existingItem) {
existingItem.content.output += output;
existingItem.content.teamRunStatus = EnumTeamRunStatus.TeamRunResponseContent;
}
};
// 任务结束
const handleTeamRunTaskEnd = (data: MESSAGE.Answer) => {
resetGenerateStatus();
const { run_id: teamRunTaskId, extra_data, output } = data;
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">
<SvgIcon name="xt-file" size="14" class="color-#6D4CFE w-24px h-24px mr-16px"/>
<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>
)}
</>
);
};
}
existingItem.footer = () => {
const isLastRunTask = 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(existingItem.content.output)} align={{ offset: [0, 4] }}>
<div class="action-box">
<SvgIcon name="xt-copy" size="16" class="color-#737478"/>
</div>
</Tooltip>
)}
{isLastRunTask && (
<Tooltip title="重新生成" onClick={() => onRefresh(teamRunTaskId)} align={{ offset: [0, 4] }}>
<div class="action-box ml-12px flex items-center">
<SvgIcon name="xt-refresh" 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,
};
}