wujingjing
2025-02-11 8c6fbc506e9a2c8cde56a794d8319cc55f4be666
src/components/chat/smallChat/index.vue
@@ -1,6 +1,6 @@
<template>
   <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-[370px] max-h-[600px] absolute bottom-4 right-4">
      <div class="bg-white rounded-lg shadow-lg flex flex-col w-[370px] max-h-[540px] absolute bottom-4 right-4">
         <!-- 头部 -->
         <div
            ref="chatHeaderRef"
@@ -8,7 +8,7 @@
            class="small-chat-header h-12 flex items-center justify-between px-4"
            style="border-bottom: 1px solid #e0e0e0"
         >
            <div class="text-lg font-bold">WI水务智能助手</div>
            <div class="text-lg font-bold py-2">WI水务智能助手</div>
            <div class="flex items-center gap-2">
               <!-- <el-icon class="cursor-pointer text-gray-400 hover:text-gray-600">
               <Refresh />
@@ -73,19 +73,50 @@
                  </div> -->
               </div>
            </div>
            <div v-else class="flex flex-col gap-4">
            <div v-else class="flex flex-col gap-1.5">
               <!-- 对话内容 -->
               <div class="flex flex-col gap-4" v-for="item in historyMessages" :key="item.id">
               <div class="flex flex-col gap-4" v-for="(item, index) in historyMessages" :key="item.id">
                  <!-- 用户提问 -->
                  <div class="flex gap-3" v-if="item.role === 'user'">
                  <div class="flex gap-3 items-center" 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 class="flex-1 bg-blue-100 rounded-lg">
                        <div class="p-3 flex items-center">
                           <span>
                              {{ item.content.value }}
                           </span>
                           <!-- #region ====================== 回复反馈 ======================-->
                           <el-icon v-if="(historyMessages[index+1].content as AssistantContent).isLoading" class="ml-2 animate-spin"
                              ><Loading
                           /></el-icon>
                           <template v-else>
                              <span
                                 v-if="(historyMessages[index+1].content as AssistantContent).isError"
                                 class="flex items-center text-nowrap ml-4 text-danger before:content-['('] after:content-[')']"
                              >
                                 {{ (historyMessages[index + 1].content as AssistantContent).value }}
                                 <el-tooltip
                                    v-if="(historyMessages[index + 1].content as AssistantContent).reason"
                                    :content="(historyMessages[index + 1].content as AssistantContent).reason"
                                    placement="top"
                                 >
                                    <el-icon class="flex-center cursor-pointer ml-1">
                                       <question-filled />
                                    </el-icon>
                                 </el-tooltip>
                              </span>
                              <span v-else class="ml-4 text-success text-nowrap before:content-['('] after:content-[')']">
                                 {{ (historyMessages[index + 1].content as AssistantContent).value }}
                              </span>
                           </template>
                           <!-- #endregion -->
                        </div>
                     </div>
                  </div>
                  <!-- AI回答 -->
                  <div class="flex gap-3" v-else-if="item.role === 'assistant'">
                  <!-- <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">
@@ -96,7 +127,7 @@
                           {{ item.content.value }}
                        </div>
                     </div>
                  </div>
                  </div> -->
               </div>
            </div>
         </div>
@@ -106,6 +137,9 @@
            <ChatInput v-model="inputText" @sendClick="sendClick" @toggleHistory="toggleHistory" :showHistory="showHistory" />
         </div>
      </div>
      <Teleport to="body">
         <WorkOrderDlg v-model="optDlgIsShow" :item="optDlgMapRow" @insert="submitDlg" @cancelSubmit="cancelSubmit"></WorkOrderDlg>
      </Teleport>
   </div>
</template>
@@ -120,7 +154,8 @@
import assistantPic from '/static/images/role/assistant-200x192.png';
import userPic from '/static/images/role/user-200x206.png';
import { useDrag } from '/@/hooks/useDrag';
import { cloneDeep, defaults } from 'lodash-es';
import WorkOrderDlg from './WorkOrderDlg.vue';
const props = defineProps<{
   olMap?: OLMap;
}>();
@@ -141,8 +176,16 @@
   handleStyle,
} = useDrag({
   handle: chatHeaderRef,
   startPos: {
      x: 200,
   },
});
const cancelSubmit = (reason) => {
   refreshAssistantMessage({ reason: reason });
};
const submitDlg = () => {
   refreshAssistantMessage({ value: `成功`, isError: false });
};
const historyMessages = ref<ChatMessage[]>([]);
const isInit = computed(() => historyMessages.value.length === 0);
const initQuestionList = ref([
@@ -150,6 +193,7 @@
   { title: '地图缩放', question: '放大' },
   { title: '设备显隐', question: '隐藏设备' },
   { title: '设备聚焦', question: '聚焦设备' },
   { title: '创建工单', question: '松福大道DN800松岗联通监测设备没有数据,创建一个设备维修工单,请及时派人维修。' },
]);
const chatContentRef = ref<HTMLDivElement>(null);
@@ -158,6 +202,18 @@
   const result = last.role === 'assistant' ? last : null;
   return result as ChatMessage<AssistantContent>;
};
//#region ====================== 添加工单 ======================
const optDlgIsShow = ref(false);
const optDlgMapRow = ref(null);
const openOptDlg = (row?: any) => {
   optDlgMapRow.value = row;
   optDlgIsShow.value = true;
};
const submit = () => {};
//#endregion
const mockCommand = (question: string) => {
   if (question === `切换${gaoDeSourceTypeMap[GaoDeSourceType.Vector]}`) {
@@ -181,10 +237,20 @@
   }
};
const handleCreateWorkOrder = (formData: any) => {
   openOptDlg(formData ?? {});
};
const startStream = (question: string) => {
   if (lastIsInit) {
      showHistory.value = false;
   }
   // if (question === '松福大道DN800松岗联通监测设备没有数据,创建一个设备维修工单,请及时派人维修。') {
   //    setTimeout(() => {
   //       openOptDlg();
   //    }, 400);
   //    return;
   // }
   // mockCommand(question);
   // return;
@@ -203,30 +269,48 @@
               const jsonData = JSON.parse(chunkRes.value);
               handleMapCommand(jsonData);
            }
         } else if (chunkRes.mode === 'create_work_order') {
            haveMapOperate = true;
            if(chunkRes.type==='string'){
               const jsonData = JSON.parse(chunkRes.value);
               handleCreateWorkOrder(jsonData)
            }
         }
         if (chunkRes.mode === 'finish') {
            if (!haveMapOperate) {
               refreshAssistantMessage({ value: `未识别到操作:${question}`, isError: true });
               refreshAssistantMessage({ reason: `未识别到操作:"${question}"` });
            }
         }
      }
   ).catch((error) => {
      Logger.error('agent stream error:\n\n' + error);
      refreshAssistantMessage();
      refreshAssistantMessage({ reason: 'AI回答失败' });
   });
};
const refreshAssistantMessage = (content: Partial<AssistantContent> = { value: 'AI回答失败', isError: true }) => {
const refreshAssistantMessage = (
   content: Partial<AssistantContent> = { value: '失败', isError: true, reason: '', isLoading: false }
) => {
   const cloneContent = cloneDeep(content);
   const last = getLastAssistantMessage();
   content = defaults(cloneContent, {
      value: '失败',
      isError: true,
      reason: '',
      isLoading: false,
   });
   if (last) {
      last.content.value = content.value;
      last.content.isLoading = content.isLoading;
      last.content.isError = content.isError ?? false;
      for (const key in content) {
         if (Object.prototype.hasOwnProperty.call(content, key)) {
            last.content[key] = content[key];
         }
      }
   }
};
const handleMapCommand = (command: any) => {
   if (!command) return;
   // openOptDlg();
   switch (command.operate) {
      case '放大':
         props.olMap.zoomIn();
@@ -247,16 +331,28 @@
         break;
      case '显示设备':
         props.olMap.toggleMarkerOverlayVisible(true);
         const equipOverlay = props.olMap.getEquipOverlay();
         if (equipOverlay) {
            equipOverlay.isVisible = true;
            // 强制触发更新
            props.olMap.layerInfo.value = props.olMap.layerInfo.value.concat([]);
            // props.olMap.toggleMarkerOverlayVisible(true);
         }
         break;
      case '隐藏设备':
         props.olMap.toggleMarkerOverlayVisible(false);
         const equipOverlay1 = props.olMap.getEquipOverlay();
         if (equipOverlay1) {
            equipOverlay1.isVisible = false;
            // 强制触发更新
            props.olMap.layerInfo.value = props.olMap.layerInfo.value.concat([]);
            // props.olMap.toggleMarkerOverlayVisible(false);
         }
         break;
      case '聚焦设备':
         props.olMap.adjustViewToMarkers();
         break;
   }
   refreshAssistantMessage({ value: `${command.operate}成功` });
   refreshAssistantMessage({ value: `成功`, isError: false });
};
const scrollToBottom = () => {