266 lines
6.8 KiB
Vue
266 lines
6.8 KiB
Vue
<script setup lang="ts">
|
|
import type { BubbleProps } from 'vue-element-plus-x/types/Bubble';
|
|
import type { ThinkingStatus } from 'vue-element-plus-x/types/Thinking';
|
|
|
|
import { inject, ref } from 'vue';
|
|
|
|
import { cloneDeep, uuid } from '@easyflow/utils';
|
|
|
|
import { Paperclip, Promotion } from '@element-plus/icons-vue';
|
|
import { ElButton, ElIcon } from 'element-plus';
|
|
|
|
import { sseClient } from '#/api/request';
|
|
import SendingIcon from '#/components/icons/SendingIcon.vue';
|
|
import ChatFileUploader from '#/components/upload/ChatFileUploader.vue';
|
|
// import PaperclipIcon from '#/components/icons/PaperclipIcon.vue';
|
|
|
|
type Think = {
|
|
reasoning_content?: string;
|
|
thinkingStatus?: ThinkingStatus;
|
|
thinlCollapse?: boolean;
|
|
};
|
|
|
|
type Tool = {
|
|
id: string;
|
|
name: string;
|
|
result: string;
|
|
status: 'TOOL_CALL' | 'TOOL_RESULT';
|
|
};
|
|
|
|
type MessageItem = BubbleProps & {
|
|
chains?: (Think | Tool)[];
|
|
key: string;
|
|
role: 'assistant' | 'user';
|
|
};
|
|
|
|
interface Props {
|
|
conversationId: string | undefined;
|
|
bot: any;
|
|
addMessage: (message: MessageItem) => void;
|
|
updateLastMessage: (item: any) => void;
|
|
stopThinking: () => void;
|
|
}
|
|
|
|
const props = defineProps<Props>();
|
|
const senderValue = ref('');
|
|
const btnLoading = ref(false);
|
|
const getSessionList = inject<any>('getSessionList');
|
|
const clearSenderFiles = () => {
|
|
files.value = [];
|
|
attachmentsRef.value?.clearFiles();
|
|
openCloseHeader();
|
|
};
|
|
function sendMessage() {
|
|
if (getDisabled()) {
|
|
return;
|
|
}
|
|
const data = {
|
|
conversationId: props.conversationId,
|
|
prompt: senderValue.value,
|
|
botId: props.bot.id,
|
|
attachments: attachmentsRef.value?.getFileList(),
|
|
};
|
|
clearSenderFiles();
|
|
btnLoading.value = true;
|
|
props.addMessage({
|
|
key: uuid(),
|
|
role: 'user',
|
|
placement: 'end',
|
|
content: senderValue.value,
|
|
typing: true,
|
|
});
|
|
props.addMessage({
|
|
key: uuid(),
|
|
role: 'assistant',
|
|
placement: 'start',
|
|
content: '',
|
|
loading: true,
|
|
typing: true,
|
|
});
|
|
senderValue.value = '';
|
|
|
|
let content = '';
|
|
|
|
sseClient.post('/userCenter/bot/chat', data, {
|
|
onMessage(res) {
|
|
if (!res.data) {
|
|
return;
|
|
}
|
|
const sseData = JSON.parse(res.data);
|
|
const delta = sseData.payload?.delta;
|
|
|
|
if (res.event === 'done') {
|
|
btnLoading.value = false;
|
|
getSessionList();
|
|
}
|
|
|
|
// 处理系统错误
|
|
if (
|
|
sseData?.domain === 'SYSTEM' &&
|
|
sseData.payload?.code === 'SYSTEM_ERROR'
|
|
) {
|
|
const errorMessage = sseData.payload.message;
|
|
props.updateLastMessage({
|
|
content: errorMessage,
|
|
loading: false,
|
|
typing: false,
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (sseData?.domain === 'TOOL') {
|
|
props.updateLastMessage((message: MessageItem) => {
|
|
const chains = cloneDeep(message.chains ?? []);
|
|
const index = chains.findIndex(
|
|
(chain) =>
|
|
isTool(chain) && chain.id === sseData?.payload?.tool_call_id,
|
|
);
|
|
|
|
if (index === -1) {
|
|
chains.push({
|
|
id: sseData?.payload?.tool_call_id,
|
|
name: sseData?.payload?.name,
|
|
status: sseData?.type,
|
|
result:
|
|
sseData?.type === 'TOOL_CALL'
|
|
? sseData?.payload?.arguments
|
|
: sseData?.payload?.result,
|
|
});
|
|
} else {
|
|
chains[index] = {
|
|
...chains[index]!,
|
|
status: sseData?.type,
|
|
result:
|
|
sseData?.type === 'TOOL_CALL'
|
|
? sseData?.payload?.arguments
|
|
: sseData?.payload?.result,
|
|
};
|
|
}
|
|
return { chains };
|
|
});
|
|
props.stopThinking();
|
|
return;
|
|
}
|
|
|
|
if (sseData.type === 'THINKING') {
|
|
props.updateLastMessage((message: MessageItem) => {
|
|
const chains = cloneDeep(message.chains ?? []);
|
|
const index = chains.findIndex(
|
|
(chain) => isThink(chain) && chain.thinkingStatus === 'thinking',
|
|
);
|
|
|
|
if (index === -1) {
|
|
chains.push({
|
|
thinkingStatus: 'thinking',
|
|
thinlCollapse: true,
|
|
reasoning_content: delta,
|
|
});
|
|
} else {
|
|
const think = chains[index]! as Think;
|
|
chains[index] = {
|
|
...think,
|
|
reasoning_content: think.reasoning_content + delta,
|
|
};
|
|
}
|
|
return { chains };
|
|
});
|
|
} else if (sseData.type === 'MESSAGE') {
|
|
props.updateLastMessage({
|
|
thinkingStatus: 'end',
|
|
loading: false,
|
|
content: (content += delta),
|
|
});
|
|
props.stopThinking();
|
|
}
|
|
},
|
|
onError(err) {
|
|
console.error(err);
|
|
btnLoading.value = false;
|
|
},
|
|
onFinished() {
|
|
senderValue.value = '';
|
|
btnLoading.value = false;
|
|
props.updateLastMessage({ loading: false });
|
|
props.stopThinking();
|
|
},
|
|
});
|
|
}
|
|
const isTool = (item: Think | Tool) => {
|
|
return 'id' in item;
|
|
};
|
|
const isThink = (item: Think | Tool): item is Think => {
|
|
return !('id' in item);
|
|
};
|
|
function getDisabled() {
|
|
return !senderValue.value || !props.conversationId;
|
|
}
|
|
const stopSse = () => {
|
|
sseClient.abort();
|
|
btnLoading.value = false;
|
|
};
|
|
const showHeaderFlog = ref(false);
|
|
const attachmentsRef = ref();
|
|
const senderRef = ref();
|
|
const files = ref<any[]>([]);
|
|
function handlePasteFile(_: any, fileList: FileList) {
|
|
showHeaderFlog.value = true;
|
|
senderRef.value?.openHeader();
|
|
files.value = [...fileList];
|
|
}
|
|
function openCloseHeader() {
|
|
if (showHeaderFlog.value) {
|
|
senderRef.value?.closeHeader();
|
|
files.value = [];
|
|
} else {
|
|
senderRef.value?.openHeader();
|
|
}
|
|
showHeaderFlog.value = !showHeaderFlog.value;
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<ElSender
|
|
ref="senderRef"
|
|
v-model="senderValue"
|
|
variant="updown"
|
|
:auto-size="{ minRows: 2, maxRows: 5 }"
|
|
clearable
|
|
allow-speech
|
|
placeholder="发送消息"
|
|
@keyup.enter="sendMessage"
|
|
@paste-file="handlePasteFile"
|
|
>
|
|
<!-- 自定义 prefix 前缀 -->
|
|
<!-- <template #prefix>
|
|
</template> -->
|
|
<!-- 自定义头部内容 -->
|
|
<template #header>
|
|
<ChatFileUploader
|
|
ref="attachmentsRef"
|
|
:external-files="files"
|
|
@delete-all="openCloseHeader"
|
|
:max-size="10"
|
|
/>
|
|
</template>
|
|
<template #action-list>
|
|
<div class="flex items-center gap-2">
|
|
<ElButton circle @click="openCloseHeader">
|
|
<ElIcon><Paperclip /></ElIcon>
|
|
</ElButton>
|
|
<!-- <ElButton :icon="PaperclipIcon" link /> -->
|
|
<ElButton v-if="btnLoading" circle @click="stopSse">
|
|
<ElIcon size="30" color="#409eff"><SendingIcon /></ElIcon>
|
|
</ElButton>
|
|
<ElButton
|
|
v-else
|
|
type="primary"
|
|
:icon="Promotion"
|
|
:disabled="getDisabled()"
|
|
@click="sendMessage"
|
|
round
|
|
/>
|
|
</div>
|
|
</template>
|
|
</ElSender>
|
|
</template>
|