wujingjing
2025-01-19 1a20b21d285bc008f6a45fad132bc808a377f853
src/components/chat/smallChat/index.vue
@@ -1,18 +1,9 @@
<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 ref="chatContainerRef" :style="chatContainerStyle" class="opacity-90 small-chat-container" @mousedown="startDrag">
      <div class="bg-white rounded-lg shadow-lg flex flex-col w-[400px] max-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 ref="chatHeaderRef" :style="handleStyle" class="small-chat-header h-12 flex items-center justify-between px-4 border-b">
            <div class="text-lg font-medium">WI水务智能助理</div>
            <div class="flex items-center gap-2">
               <!-- <el-icon class="cursor-pointer text-gray-400 hover:text-gray-600">
               <Refresh />
@@ -23,14 +14,14 @@
            <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">
               <!-- <el-icon class="cursor-pointer text-gray-400 hover:text-gray-600" @click="closeChat">
                  <Close />
               </el-icon>
               </el-icon> -->
            </div>
         </div>
         <!-- 内容区 -->
         <div class="flex-1 overflow-y-auto p-4">
         <div class="flex-1 overflow-y-auto p-4" ref="chatContentRef" v-show="showHistory">
            <div v-if="isInit">
               <!-- 欢迎语 -->
               <div class="flex flex-col items-center gap-4 mt-8">
@@ -48,10 +39,6 @@
                  >
                     {{ 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">
@@ -83,108 +70,113 @@
         </div>
         <!-- 底部输入框 -->
         <div class="p-4 border-t">
            <ChatInput v-model="inputText" @sendClick="sendClick" />
         <div class="p-2 border-t">
            <ChatInput v-model="inputText" @sendClick="sendClick" @toggleHistory="toggleHistory" :showHistory="showHistory" />
         </div>
      </div>
   </div>
</template>
<script setup lang="ts" name="smallChat">
import { computed, onMounted, ref } from 'vue';
import { computed, nextTick, 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 { agentStreamByPost } 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';
import { useDrag } from '/@/hooks/useDrag';
const props = defineProps<{
   olMap?: OLMap;
}>();
const isOpen = ref(false);
const inputText = ref('');
/** @description 对话完成前是否时初始状态 */
let lastIsInit = true;
const closeChat = () => {
   isOpen.value = false;
};
const chatHeaderRef = ref<HTMLDivElement>(null);
const chatContainerRef = ref<HTMLDivElement>(null);
const { startDrag, style: chatContainerStyle, handleStyle } = useDrag({
   handle: chatHeaderRef,
});
const historyMessages = ref<ChatMessage[]>([]);
const isInit = computed(() => historyMessages.value.length === 0);
const initQuestionList = ref(null);
const initQuestionList = ref(['放大', '缩小']);
const chatContentRef = ref<HTMLDivElement>(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 = () => {
const startStream = (question: string) => {
   if (lastIsInit) {
      showHistory.value = false;
   }
   let haveMapOperate = false;
   agentStreamByPost(
      {
         question: question,
         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') {
            haveMapOperate = true;
            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;
            if (!haveMapOperate) {
               refreshAssistantMessage({ value: `未识别到操作:${question}` });
            }
         }
      }
   );
   ).catch((error) => {
      Logger.error('agent stream error:\n\n' + error);
      refreshAssistantMessage();
   });
};
const refreshAssistantMessage = (content: Partial<AssistantContent> = { value: 'AI回答失败' }) => {
   const last = getLastAssistantMessage();
   if (last) {
      last.content.value = content.value;
      last.content.isLoading = content.isLoading;
   }
};
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;
   switch (command.operate) {
      case '放大':
         props.olMap.zoomIn();
         break;
         case '缩小':
            props.olMap.zoomOut();
            break;
      }
      last.content.isLoading = false;
      case '缩小':
         props.olMap.zoomOut();
         break;
   }
   refreshAssistantMessage({ value: `已执行操作: ${command.operate}` });
};
const scrollToBottom = () => {
   const chatContainer = document.querySelector('.chat-container');
   if (chatContainer) {
      chatContainer.scrollTop = chatContainer.scrollHeight;
   if (chatContentRef.value) {
      chatContentRef.value.scrollTop = chatContentRef.value.scrollHeight;
   }
};
const openChat = () => {
   isOpen.value = true;
   if (!streamIsOpen) {
      startStream();
   }
};
const applyMessage = () => {
   const time = Date.now().toString();
   const userMessageId = `user-${time}`;
@@ -206,44 +198,41 @@
      },
   };
   historyMessages.value.push(assistantMessage);
   nextTick(() => {
      scrollToBottom();
   });
   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;
   lastIsInit = isInit.value;
   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();
   startStream(question);
};
const handleQuestionClick = (item: string) => {
   inputText.value = item;
   sendClick();
};
const isFinish = ref(false);
let activeQuestionChunk = null;
//#region ====================== 历史显示控制 ======================
const showHistory = ref(true);
const toggleHistory = () => {
   showHistory.value = !showHistory.value;
   nextTick(() => {
      scrollToBottom();
   });
};
//#endregion
onMounted(() => {});
</script>