Files
lingji-work-fe/src/views/home/components/conversation-detail/index.vue
2025-08-25 11:49:58 +08:00

323 lines
9.6 KiB
Vue
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.

<script lang="tsx">
import { message, Tooltip } from 'ant-design-vue';
import { BubbleList } from '@/components/xt-chat/xt-bubble';
import SenderInput from '../sender-input/index.vue';
import { Typography } from 'ant-design-vue';
import RightView from './rightView.vue';
import TextOverTips from '@/components/text-over-tips/index.vue';
import { useRoute } from 'vue-router';
import markdownit from 'markdown-it';
import { useClipboard } from '@vueuse/core';
import { genRandomId } from '@/utils/tools';
import { useChatStore } from '@/stores/modules/chat';
import { getHeaders } from '@/api/all/chat';
import type { BubbleListProps } from '@/components/xt-chat/xt-bubble/types';
import querySSE from '@/utils/querySSE';
const QUESTION_ROLE = 'question';
const ANSWER_ROLE = 'text';
const FILE_ROLE = 'file';
const THOUGHT_ROLE = 'thought'; // 新增思考过程角色常量
export default {
setup(props, { emit, expose }) {
const chatStore = useChatStore();
const route = useRoute();
const { copy } = useClipboard();
const senderRef = ref(null);
const rightViewRef = ref(null);
const bubbleListRef = ref<any>(null);
const searchValue = ref('');
const generateLoading = ref(false);
const conversationList = ref([]);
const currentAnswerId = ref<string | null>(null);
const showRightView = ref(false);
const rightViewContent = ref('');
const md = markdownit({
html: true,
breaks: true,
linkify: true,
typographer: true,
});
const conversationId = computed(() => {
return route.params.conversationId;
});
const onCopy = (content: string) => {
copy(content);
message.success('复制成功!');
};
const onRefresh = (tempId: string, tempIndex: number) => {
generateLoading.value = true;
conversationList.value.splice(tempIndex, 1, {
id: tempId,
loading: true,
role: ANSWER_ROLE,
});
};
const handleSubmit = () => {
if (generateLoading.value) {
message.warning('停止生成后可发送');
return;
}
generateLoading.value = true;
conversationList.value.push({
role: QUESTION_ROLE,
content: searchValue.value,
});
const tempId = genRandomId();
const tempIndex = conversationList.value.length;
conversationList.value.push({
id: tempId,
loading: true,
role: ANSWER_ROLE,
});
currentAnswerId.value = tempId;
initSse();
};
const handleCancel = () => {
generateLoading.value = false;
// 中止当前正在输出的回答
if (currentAnswerId.value && bubbleListRef.value?.abortTypingByKey) {
bubbleListRef.value.abortTypingByKey(currentAnswerId.value);
}
if (showRightView.value) {
rightViewRef.value?.abortTyping?.();
}
message.info('取消生成');
};
const roles: BubbleListProps['roles'] = {
[ANSWER_ROLE]: {
placement: 'start',
variant: 'borderless',
typing: { step: 2, interval: 100 },
onTypingComplete: () => {
console.log('onTypingComplete');
generateLoading.value = false;
currentAnswerId.value = null;
},
style: {
width: '600px',
margin: '0 auto',
},
},
[FILE_ROLE]: {
placement: 'start',
variant: 'borderless',
typing: { step: 2, interval: 100 },
messageRender: (items) => {
return items.map((item) => (
<div class="file-card">
<icon-file class="w-17px h-20px mr-20px color-#6D4CFE" />
<div>
<TextOverTips
context={item.name}
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">创建时间08-04 12:40</span>
</div>
</div>
));
},
style: {
width: '600px',
margin: '0 auto',
},
},
// 新增思考过程角色配置
[THOUGHT_ROLE]: {
placement: 'start',
variant: 'borderless',
style: {
width: '600px',
margin: '0 auto',
},
},
[QUESTION_ROLE]: {
placement: 'end',
shape: 'round',
style: {
width: '600px',
margin: '0 auto',
},
},
};
const onDownload = (content) => {
console.log('onDownload', content);
};
const initSse = () => {
const handleStart = () => {
conversationList.value.push({
role: ANSWER_ROLE,
content: (
<div class="flex items-center ">
<span class="font-family-medium color-#211F24 text-14px font-400 lh-22px mr-4px">智能思考</span>
<icon-caret-up size={16} class="color-#211F24" />
</div>
),
});
};
const handleNodeUpdate = (data) => {
switch (data.status) {
case 'running':
conversationList.value.push({
content: data.message,
role: ANSWER_ROLE,
});
break;
case 'success':
conversationList.value.push({
content: data.output,
role: ANSWER_ROLE,
messageRender: (content) => <div v-html={md.render(content)}></div>,
});
break;
}
};
const handleFinalResult = (data) => {
showRightView.value = true;
const _files = data.output?.files;
rightViewContent.value = _files[0]?.content || '';
conversationList.value.push({
id: currentAnswerId.value,
role: FILE_ROLE,
content: _files,
footer: ({ item }) => {
const nonQuestionElements = conversationList.value.filter((item) => item.role !== QUESTION_ROLE);
const isLastAnswer = nonQuestionElements[nonQuestionElements.length - 1]?.id === item.id;
return (
<div class="flex items-center">
<Tooltip title="下载" onClick={() => onDownload(rightViewContent.value)}>
<icon-download size={16} class="color-#737478 cursor-pointer" />
</Tooltip>
{isLastAnswer && (
<Tooltip
title="重新生成"
onClick={() => onRefresh(currentAnswerId.value, conversationList.value.length)}
>
<icon-refresh size={16} class="color-#737478 cursor-pointer" />
</Tooltip>
)}
</div>
);
},
});
};
const handleEnd = () => {
generateLoading.value = false;
};
const handleError = () => {
generateLoading.value = false;
message.error('连接服务器失败');
};
try {
const url = `http://localhost:3000/agent/input?content=${searchValue.value}&session_id=${conversationId.value}&agent_id=${chatStore.agentInfo?.agent_id}`;
searchValue.value = '';
querySSE(
{
handleMessage(parsedData) {
const { event, data } = parsedData;
switch (event) {
case 'start':
handleStart();
break;
case 'node_update':
handleNodeUpdate(data);
break;
case 'final_result':
handleFinalResult(data);
break;
case 'end':
handleEnd();
break;
case 'error':
handleError();
break;
default:
break;
}
},
async handleOpen(response) {
console.log('onopen', response);
},
},
url,
);
} catch (error) {
console.error('Failed to initialize SSE:', error);
message.error('初始化连接失败');
generateLoading.value = false;
}
};
onMounted(() => {
searchValue.value = chatStore.searchValue;
chatStore.clearSearchValue();
searchValue.value && initSse();
});
return () => (
<div class="conversation-detail-wrap w-full h-full flex">
<section class="flex-1 flex flex-col pt-20px justify-center relative px-16px">
{/* <div class="w-full h-full flex "> */}
<div class="flex-1 overflow-hidden pb-20px">
<BubbleList ref={bubbleListRef} roles={roles} items={conversationList.value} />
</div>
<div class="w-full flex flex-col justify-center items-center">
<SenderInput
class="w-600px"
ref={senderRef}
placeholder="继续追问..."
loading={generateLoading.value}
v-model={searchValue.value}
onSubmit={handleSubmit}
onCancel={handleCancel}
data-ne="123"
/>
<p class="cts !color-#939499 text-12px !lh-20px my-4px">内容由AI生成仅供参考</p>
</div>
{/* </div> */}
</section>
{/* 右侧展示区域 */}
{showRightView.value && (
<RightView
ref={rightViewRef}
rightViewContent={rightViewContent.value}
showRightView={showRightView.value}
onClose={() => (showRightView.value = false)}
/>
)}
</div>
);
},
};
</script>
<style lang="scss" scoped>
@import './style.scss';
</style>