<template>
|
<div class="relative">
|
<el-tooltip v-if="!isOpen" content="你好,我是AI助理,可以解答问题、推荐解决方案等" placement="right">
|
<div
|
class="flex flex-col items-center gap-1 cursor-pointer bg-white rounded-lg shadow-lg p-1 absolute bottom-4 right-4 opacity-80"
|
@click="openChat"
|
>
|
<img :src="assistantPic" class="size-10" alt="AI头像" />
|
<span class="text-lg text-center w-5">AI助手</span>
|
</div>
|
</el-tooltip>
|
<div v-else class="bg-white rounded-lg shadow-lg flex flex-col w-[400px] h-[600px] absolute bottom-4 right-4">
|
<!-- 头部 -->
|
<div class="h-12 flex items-center justify-between px-4 border-b">
|
<div class="text-base font-medium">WI水务智能助理</div>
|
<div class="flex items-center gap-2">
|
<!-- <el-icon class="cursor-pointer text-gray-400 hover:text-gray-600">
|
<Refresh />
|
</el-icon>
|
<el-icon class="cursor-pointer text-gray-400 hover:text-gray-600">
|
<FullScreen />
|
</el-icon>
|
<el-icon class="cursor-pointer text-gray-400 hover:text-gray-600">
|
<Star />
|
</el-icon> -->
|
<el-icon class="cursor-pointer text-gray-400 hover:text-gray-600" @click="closeChat">
|
<Close />
|
</el-icon>
|
</div>
|
</div>
|
|
<!-- 内容区 -->
|
<div class="flex-1 overflow-y-auto p-4">
|
<div v-if="isInit">
|
<!-- 欢迎语 -->
|
<div class="flex flex-col items-center gap-4 mt-8">
|
<img :src="assistantPic" class="w-16 h-16" alt="AI头像" />
|
<div class="text-lg">你好, 我是</div>
|
<div class="text-lg text-blue-500">WI水务智能助理</div>
|
</div>
|
<!-- 快捷问题 -->
|
<div class="mt-8 flex flex-col gap-3 mx-10">
|
<div
|
v-for="(item, index) in initQuestionList"
|
:key="index"
|
class="cursor-pointer hover:bg-gray-50 p-3 rounded-lg border border-solid border-gray-400"
|
@click="handleQuestionClick(item)"
|
>
|
{{ item }}
|
</div>
|
<!-- <div class="cursor-pointer hover:bg-gray-50 p-3 rounded-lg border border-solid border-gray-400">
|
阿里云产品怎么购买,重点有哪些功能?
|
</div>
|
<div class="cursor-pointer hover:bg-gray-50 p-3 rounded-lg border border-solid border-gray-400">AI 助理能为我做什么?</div> -->
|
</div>
|
</div>
|
<div v-else class="flex flex-col gap-4">
|
<!-- 对话内容 -->
|
<div class="flex flex-col gap-4" v-for="item in historyMessages" :key="item.id">
|
<!-- 用户提问 -->
|
<div class="flex gap-3" v-if="item.role === 'user'">
|
<img :src="userPic" class="w-10 h-10" alt="用户头像" />
|
<div class="flex-1 bg-blue-100">
|
<div class="p-4 rounded-lg">{{ item.content.value }}</div>
|
</div>
|
</div>
|
|
<!-- AI回答 -->
|
<div class="flex gap-3" v-else-if="item.role === 'assistant'">
|
<img :src="assistantPic" class="w-10 h-10" alt="AI头像" />
|
<div class="flex-1 bg-gray-100">
|
<div v-if="(item.content as AssistantContent)?.isLoading" class="p-4 rounded-lg flex items-center">
|
<el-icon class="animate-spin mr-2"><Loading /></el-icon>
|
AI正在思考中...
|
</div>
|
<div v-else class="p-4 rounded-lg">
|
{{ item.content.value }}
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<!-- 底部输入框 -->
|
<div class="p-4 border-t">
|
<ChatInput v-model="inputText" @sendClick="sendClick" />
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<script setup lang="ts" name="smallChat">
|
import { computed, onMounted, ref } from 'vue';
|
import ChatInput from './ChatInput.vue';
|
import type { ChatMessage } from './types';
|
import { AssistantContent } from './types';
|
import { agentStreamByPost, question_stream_reply } from '/@/api/ai/chat';
|
import { Logger } from '/@/model/logger/Logger';
|
import type { OLMap } from '/@/model/map/OLMap';
|
import assistantPic from '/static/images/role/assistant-200x192.png';
|
import userPic from '/static/images/role/user-200x206.png';
|
|
const props = defineProps<{
|
olMap?: OLMap;
|
}>();
|
const isOpen = ref(false);
|
const inputText = ref('');
|
const closeChat = () => {
|
isOpen.value = false;
|
};
|
const historyMessages = ref<ChatMessage[]>([]);
|
const isInit = computed(() => historyMessages.value.length === 0);
|
const initQuestionList = ref(null);
|
const getLastAssistantMessage = () => {
|
const last = historyMessages.value[historyMessages.value.length - 1];
|
const result = last.role === 'assistant' ? last : null;
|
return result as ChatMessage<AssistantContent>;
|
};
|
let streamIsOpen = false;
|
const startStream = () => {
|
agentStreamByPost(
|
{
|
agent_id: 'a_019471cdb0667a83956b76ac97283f1c',
|
},
|
(chunkRes) => {
|
if (!streamIsOpen) {
|
streamIsOpen = true;
|
}
|
Logger.info('agent stream response:\n\n' + JSON.stringify(chunkRes));
|
if (chunkRes.mode === 'question') {
|
if (chunkRes.type === 'json') {
|
if (!initQuestionList.value) {
|
initQuestionList.value = chunkRes.value?.options ?? [];
|
}
|
activeQuestionChunk = chunkRes;
|
}
|
}
|
if (chunkRes.mode === 'map') {
|
if (chunkRes.type === 'string') {
|
const jsonData = JSON.parse(chunkRes.value);
|
handleMapCommand(jsonData);
|
}
|
}
|
|
if (chunkRes.mode === 'finish') {
|
isOpen.value = false;
|
streamIsOpen = false;
|
const last = getLastAssistantMessage();
|
if (last) {
|
last.content.value = '已退出';
|
last.content.isLoading = false;
|
}
|
}
|
}
|
);
|
};
|
const handleMapCommand = (command: any) => {
|
if (!command) return;
|
const last = getLastAssistantMessage();
|
if (last) {
|
last.content.value = `已执行操作: ${command.operate}`;
|
switch (command.operate) {
|
case '放大':
|
props.olMap.zoomIn();
|
break;
|
|
case '缩小':
|
props.olMap.zoomOut();
|
break;
|
}
|
last.content.isLoading = false;
|
}
|
};
|
|
const scrollToBottom = () => {
|
const chatContainer = document.querySelector('.chat-container');
|
if (chatContainer) {
|
chatContainer.scrollTop = chatContainer.scrollHeight;
|
}
|
};
|
const openChat = () => {
|
isOpen.value = true;
|
if (!streamIsOpen) {
|
startStream();
|
}
|
};
|
const applyMessage = () => {
|
const time = Date.now().toString();
|
const userMessageId = `user-${time}`;
|
const userMessage: ChatMessage = {
|
id: userMessageId,
|
role: 'user',
|
content: {
|
value: inputText.value,
|
},
|
};
|
historyMessages.value.push(userMessage);
|
const assistantMessageId = `assistant-${time}`;
|
const assistantMessage: ChatMessage = {
|
id: assistantMessageId,
|
role: 'assistant',
|
content: {
|
value: '',
|
isLoading: true,
|
},
|
};
|
historyMessages.value.push(assistantMessage);
|
const userReactive = historyMessages.value.find((item) => item.id === userMessageId);
|
const assistantReactive = historyMessages.value.find((item) => item.id === assistantMessageId);
|
return [userReactive, assistantReactive];
|
};
|
|
const resetActive = () => {
|
activeQuestionChunk = null;
|
};
|
|
const refreshMessageList = () => {
|
historyMessages.value = historyMessages.value.concat([]);
|
};
|
const sendClick = async () => {
|
if (!activeQuestionChunk) return;
|
if (!inputText.value.trim()) return;
|
const lastMessage = historyMessages.value[historyMessages.value.length - 1];
|
if (lastMessage && lastMessage.role === 'assistant' && (lastMessage.content as AssistantContent).isLoading) return;
|
const [userMessage, assistantMessage] = applyMessage();
|
const question = inputText.value;
|
inputText.value = '';
|
|
const assistantContent = assistantMessage.content as AssistantContent;
|
const res = await question_stream_reply({
|
select: question,
|
reply_id: activeQuestionChunk.value.reply_id,
|
}).catch(() => {
|
assistantContent.isLoading = false;
|
assistantContent.value = 'AI回答失败';
|
});
|
resetActive();
|
};
|
|
const handleQuestionClick = (item: string) => {
|
inputText.value = item;
|
sendClick();
|
};
|
const isFinish = ref(false);
|
let activeQuestionChunk = null;
|
onMounted(() => {});
|
</script>
|
|
<style scoped lang="scss">
|
:deep(.el-input__wrapper) {
|
padding-right: 0;
|
}
|
</style>
|