Explorar o código

[Feat 0000]新版本智能问答模块开发

bobo04052021@163.com hai 1 día
pai
achega
da1bb1e14d

+ 913 - 156
src/components/AIChat/MiniChat.vue

@@ -1,56 +1,135 @@
 <!-- eslint-disable vue/no-v-html -->
 <template>
-  <div class="mini-chat">
+  <div class="btn" @click="showAIChat">
+    <div style="display: flex; flex-direction: row" class="btn-header">
+      <img src="@/assets/images/vent/home/wakeBtn.png" />
+    </div>
+  </div>
+  <div v-if="isShowChatBroad" class="mini-chat">
     <!-- 左侧折叠区域 -->
-    <div class="left-side">
-      <SvgIcon name="add" size="20" />
-      <Popover trigger="click" :overlay-inner-style="{ padding: '1px' }">
-        <template #content>
-          <AIChat style="width: 700px; height: 500px" :visible="true" />
-        </template>
-        <SvgIcon :name="dialogVisible ? 'zoom-out' : 'zoom-in'" size="20" @click="openDialog" />
-      </Popover>
+    <div class="left-side" :class="{ collapsed: isFold }" id="leftSide">
+      <div
+        v-if="isFold"
+        class="historyBtn"
+        :style="{ backgroundImage: `url(${isFold ? '/src/assets/images/vent/home/history.svg' : '/src/assets/images/vent/home/history.svg'})` }"
+        @click="addNew"
+      ></div>
+      <div v-else class="historyBtn1">
+        <span
+          class="btn-text-bg"
+          :style="{
+            backgroundImage: `url(${!isFold ? '/src/assets/images/vent/home/history.svg' : ''})`,
+          }"
+        ></span>
+        <span v-if="!isFold" class="btn-text">历史对话</span>
+        <a-list style="width: 130px" :split="false" :data-source="sessionHistory" :scroll="200" class="custom-list">
+          <template #renderItem="{ item }">
+            <a-list-item
+              class="session-item"
+              :style="{
+                padding: '8px 10px 0 8px',
+                color: '#5e7081',
+                fontSize: '10px',
+                position: 'relative', // 新增定位
+              }"
+            >
+              <!-- 新增flex布局容器 -->
+              <div style="width: 100%">
+                <div v-if="editingId !== item.id" class="text-container">
+                  <span class="edit-text" @click="sessionsHistory(item.id)">{{ item.name }}</span>
+                  <div class="btn-container">
+                    <EditOutlined class="edit-icon" @click="startEditing(item)" />
+                    <DeleteOutlined class="delete-icon" @click="startDelete(item)" />
+                  </div>
+                </div>
+                <!-- 输入框 -->
+                <a-input
+                  size="small"
+                  v-else
+                  v-model:value="editText"
+                  v-focus
+                  @blur="handleSave(item)"
+                  @keyup.enter="handleSave(item)"
+                  class="edit-input"
+                />
+              </div>
+            </a-list-item>
+          </template>
+        </a-list>
+      </div>
+      <div
+        class="foldBtn"
+        :style="{ backgroundImage: `url(${isFold ? '/src/assets/images/vent/home/Fold.svg' : '/src/assets/images/vent/home/unfold.svg'})` }"
+        @click="fold"
+      ></div>
     </div>
     <!-- 右侧对话框 -->
     <div class="right-side">
       <!-- 对话区域 -->
-      <div ref="dialogRef" class="dialog-area">
+      <div class="dialog-area">
         <div
-          v-for="message in store.getMessageHistory"
+          v-for="message in messageHistory"
           :key="message.id"
           class="flex items-center w-100%"
           :style="{ alignSelf: message.type === 'user' ? 'flex-end' : 'flex-start' }"
         >
           <template v-if="message.type === 'user'">
             <div class="flex-grow-1"></div>
-            <div class="ask-message">{{ message.content }}</div>
+            <div class="message-wrapper user-message-wrapper">
+              <div class="ask-message">{{ message.content }}</div>
+              <CopyOutlined class="copy-icon" @click="copyToClipboard(message.content)" title="复制消息" />
+            </div>
           </template>
           <template v-else>
-            <SvgIcon size="30" class="ml-2px mr-2px" name="ai-logo" />
-            <div class="answer-message">
-              <div v-if="message.contentR1" class="color-gray font-size-12px" v-html="formatMessage(message.contentR1)"> </div>
-              <div v-html="formatMessage(message.content)"> </div>
+            <SvgIcon size="80" class="ml-2px mr-2px" name="ai-logo" />
+            <div class="message-wrapper ai-message-wrapper">
+              <div class="answer-message">
+                <div v-if="message.contentR1" class="color-gray font-size-12px" v-html="message.contentR1"> </div>
+                <div v-else v-html="message.content"> </div>
+              </div>
+              <CopyOutlined class="copy-icon" @click="copyToClipboard(message.contentR1 || message.content)" title="复制消息" />
             </div>
           </template>
         </div>
       </div>
-      <!-- 底部操作栏 -->
+      <!-- 底部输入区 -->
       <div class="input-area">
-        <TextArea v-model:value="inputText" placeholder="请输入你的问题" />
-        <div class="action-bar">
-          <!-- 左侧深度思考按钮 -->
-          <div class="think-btn" :class="{ active: store.deepseekR1Enable }" @click="toggleThinking"> <span>深度思考</span> </div>
-
-          <!-- 右侧操作按钮 -->
-          <Space>
-            <SvgIcon name="send-image" />
-            <SvgIcon name="send-file" />
-            <Button type="primary" shape="circle" size="small" :loading="store.streaming" @click="handleSend">
-              <template #icon>
-                <SvgIcon name="send" />
-              </template>
-            </Button>
-          </Space>
+        <a-input v-model:value="inputText" placeholder="请输入你的问题" @keyup.enter="handleSend(inputText)" class="ant-input" />
+        <div class="ctrl-btn">
+          <div class="input-controls">
+            <button class="control-btn">深度学习</button>
+            <button class="control-btn" @click="stopReq()">停止响应</button>
+          </div>
+          <div class="action-bar">
+            <Space>
+              <Button class="control-btn1" size="small" @click="showModal()">
+                <template #icon>
+                  <SvgIcon name="send-file" />
+                </template>
+              </Button>
+              <Button class="control-btn2" size="small" @click="handleSend(inputText)">
+                <template #icon>
+                  <SvgIcon name="send" />
+                </template>
+              </Button>
+            </Space>
+          </div>
+        </div>
+        <!-- 右侧文件上传区 -->
+        <div v-if="open" class="file-upload">
+          <!-- 输入框区域,包含确认按钮 -->
+          <div class="input-container">
+            <a-input v-model:value="filePath" placeholder="输入文件连接" class="file-input" @pressEnter="handlePathConfirm" />
+            <button class="confirm-btn" @click="handlePathConfirm">确认</button>
+          </div>
+          <!-- 上传按钮 -->
+          <!-- <a-upload> <button class="upload-btn" @click="customUpload">从本地上传</button></a-upload> -->
+          <a-upload class="custom-upload" name="file" :multiple="false">
+            <a-button class="upload-btn" @click="customUpload">
+              <UploadOutlined></UploadOutlined>
+              从本地上传
+            </a-button>
+          </a-upload>
         </div>
       </div>
     </div>
@@ -58,151 +137,829 @@
 </template>
 
 <script lang="ts" setup>
-  import { ref, onMounted } from 'vue';
-  import { SvgIcon } from '../Icon';
-  import { Space, Button, Popover, Input } from 'ant-design-vue';
-  import AIChat from './index.vue';
-  import { useAIChat } from '/@/store/modules/AIChat';
-  const TextArea = Input.TextArea; // 直接导入TextArea组件使用时打包报错
-  const dialogRef = ref<HTMLElement | null>(null);
-  const dialogVisible = ref(true);
-  const inputText = ref(''); // 输入框内容
-  const store = useAIChat(); //获取用户信息
-
-  const openDialog = () => {
-    dialogVisible.value = !dialogVisible.value;
-  };
-  //启用深度思考
-  const toggleThinking = () => {
-    store.deepseekR1Enable = !store.deepseekR1Enable;
-  };
-
-  //获取消息列表
-  async function handleSend() {
-    store
-      .sendQuestion(inputText.value, () => {
-        if (dialogRef.value) {
-          dialogRef.value.scrollTop = dialogRef.value.scrollHeight;
+import { ref, onMounted } from 'vue';
+import { SvgIcon } from '../Icon';
+import { Space, Button, Modal, Input, message } from 'ant-design-vue';
+// import AIChat from './index.vue';
+import { useUserStore } from '/@/store/modules/user';
+import { EditOutlined, DeleteOutlined, UploadOutlined, CopyOutlined } from '@ant-design/icons-vue';
+import { createVNode } from 'vue';
+const TextArea = Input.TextArea; // 直接导入TextArea组件使用时打包报错
+const inputText = ref(''); // 输入框内容
+const sessionHistory = ref([]);
+const isShowChatBroad = ref(false);
+const editingId = ref<number | null>(null);
+const editText = ref('');
+const currentSessionID = ref('');
+const taskID = ref('');
+const open = ref<boolean>(false);
+interface ListItem {
+  id: number;
+  name?: string;
+}
+interface Message {
+  id: string; // 唯一标识(可用时间戳生成)
+  type: 'user' | 'system' | 'response';
+  content: string;
+  /** 深度思考时的文本 */
+  contentR1: string;
+  timestamp: number; // 排序依据
+}
+// 定义消息历史数组类型
+const messageHistory = ref<Message[]>([]);
+const isFold = ref(true); // 是否折叠
+const userid = useUserStore().getUserInfo.id as string;
+const filePath = ref(''); // 绑定输入框值
+const showConfirmBtn = ref(false); // 控制确认按钮显示状态
+const fileList = ref([]);
+function showAIChat() {
+  isShowChatBroad.value = !isShowChatBroad.value;
+}
+//获取消息列表
+// async function handleSend(data) {
+//   messageHistory.value.push({
+//     id: `user_${Date.now()}`,
+//     type: 'user',
+//     content: data,
+//     contentR1: '',
+//     timestamp: Date.now(),
+//   });
+//   // 发送 POST 请求
+//   fetch('http://39.97.59.228:8000/v1/chat-messages', {
+//     method: 'POST',
+//     headers: {
+//       'Content-Type': 'application/json',
+//       Authorization: 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd',
+//     },
+//     body: JSON.stringify({
+//       conversation_id: currentSessionID.value,
+//       query: data,
+//       response_mode: 'streaming',
+//       user: userid,
+//       inputs: {},
+//     }),
+//   }).then((response) => {
+//     const decoder = new TextDecoder('utf-8');
+//     let buffer = [];
+//     // 获取可读流
+//     const reader = response.body.getReader();
+//     const newMessage = {
+//       id: `response_${Date.now()}`,
+//       type: 'response' as any,
+//       content: '',
+//       contentR1: '',
+//       timestamp: Date.now(),
+//     };
+//     messageHistory.value.push(newMessage);
+//     // 读取数据
+//     function read() {
+//       return reader.read().then(({ done, value }) => {
+//         if (done) {
+//           return buffer;
+//         }
+//         // 解码数据块
+//         const chunk = decoder.decode(value, { stream: false });
+//         // 处理每段数据
+//         const processedData = processStreamChunk(chunk);
+//         buffer = buffer.concat(processedData);
+//         // 继续读取
+//         return read();
+//       });
+//     }
+//     // 开始读取
+//     return read();
+//     function processStreamChunk(chunk) {
+//       try {
+//         // 移除 "data: " 前缀
+//         const jsonStr = chunk.replace('data: ', '');
+//         const data = JSON.parse(jsonStr);
+//         const targetMessage = messageHistory.value.find((msg) => msg.id === newMessage.id);
+//         if (!targetMessage) return;
+//         // 根据事件类型分发处理
+//         switch (data.event) {
+//           case 'message':
+//             if (!taskID.value && !currentSessionID.value) {
+//               taskID.value = data.task_id;
+//               currentSessionID.value = data.conversation_id;
+//             }
+//             targetMessage.content += data.answer; // 追加内容
+//             break;
+//         }
+//         return data;
+//       } catch (error) {
+//         // 请求失败时设置系统消息
+//         return null;
+//       }
+//     }
+//   });
+//   inputText.value = '';
+// }
+// 复制消息
+function copyToClipboard(text) {
+  if (!text || text.trim() === '') {
+    message.warn('没有可复制的内容');
+    return;
+  }
+  // 2. 创建临时textarea 元素
+  const textarea = document.createElement('textarea');
+  textarea.value = text;
+  textarea.style.position = 'fixed';
+  textarea.style.top = '-999px';
+  textarea.style.left = '-999px';
+  textarea.style.width = '200px';
+  textarea.style.height = '200px';
+  document.body.appendChild(textarea);
+  try {
+    textarea.select();
+    textarea.setSelectionRange(0, text.length);
+    const isSuccessful = document.execCommand('copy');
+    if (isSuccessful) {
+      message.success('复制成功!');
+    } else {
+      throw new Error('复制命令执行失败');
+    }
+  } catch (err) {
+    console.error('复制失败:', err);
+    message.error('复制失败,请手动复制');
+  } finally {
+    document.body.removeChild(textarea);
+  }
+}
+async function handleSend(data) {
+  inputText.value = '';
+  messageHistory.value.push({
+    id: `user_${Date.now()}`,
+    type: 'user',
+    content: data,
+    contentR1: '',
+    timestamp: Date.now(),
+  });
+  try {
+    const response = await fetch('http://39.97.59.228:8000/v1/chat-messages', {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+        Authorization: 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd',
+      },
+      body: JSON.stringify({
+        conversation_id: currentSessionID.value,
+        query: data,
+        response_mode: 'streaming',
+        user: userid,
+        inputs: {},
+      }),
+    });
+
+    if (!response.ok) {
+      throw new Error(`HTTP error! status: ${response.status}`);
+    }
+
+    const decoder = new TextDecoder('utf-8');
+    const reader = response.body.getReader();
+    let textBuffer = ''; // 使用字符串缓冲区来累积数据
+    const newMessage = {
+      id: `response_${Date.now()}`,
+      type: 'response',
+      content: '',
+      contentR1: '',
+      timestamp: Date.now(),
+    };
+    messageHistory.value.push(newMessage);
+    while (true) {
+      const { done, value } = await reader.read();
+      if (done) {
+        if (textBuffer) {
+          processLine(textBuffer);
+        }
+        break;
+      }
+      textBuffer += decoder.decode(value, { stream: true });
+      // 处理每一行数据
+      let lineIndex;
+      while ((lineIndex = textBuffer.indexOf('\n')) !== -1) {
+        const line = textBuffer.substring(0, lineIndex).trim();
+        textBuffer = textBuffer.substring(lineIndex + 1);
+
+        if (line) {
+          processLine(line);
         }
-      })
-      .then(() => {
-        inputText.value = '';
+      }
+    }
+    function processLine(line) {
+      if (line.startsWith('data: ')) {
+        try {
+          const jsonStr = line.substring('data: '.length);
+          const data = JSON.parse(jsonStr);
+          switch (data.event) {
+            case 'message':
+              if (data.answer) {
+                const targetMessage = messageHistory.value.find((msg) => msg.id === newMessage.id);
+                if (targetMessage) {
+                  targetMessage.content += data.answer;
+                }
+              }
+              if (data.task_id && !taskID.value) taskID.value = data.task_id;
+              if (data.conversation_id && !currentSessionID.value) currentSessionID.value = data.conversation_id;
+              break;
+          }
+        } catch (error) {
+          console.warn('Error parsing stream chunk:', error, 'Chunk:', line);
+        }
+      }
+    }
+  } catch (error) {
+    console.error('Error in handleSend:', error);
+    // 在 UI 上显示错误信息
+    messageHistory.value.push({
+      id: `system_${Date.now()}`,
+      type: 'system',
+      content: '请求错误',
+      contentR1: '',
+      timestamp: Date.now(),
+    });
+  }
+}
+// 上传文件
+const showModal = () => {
+  open.value = !open.value;
+};
+async function customUpload(data) {
+  const formData = new FormData();
+  if (!data) {
+    return message.warn('请选择文件');
+  }
+  // 添加文件参数
+  formData.append('file', data.file);
+  // 添加用户标识参数
+  formData.append('user', userid);
+  try {
+    let response = await fetch(`http://39.97.59.228:8000/v1/files/upload`, {
+      method: 'POST',
+      headers: {
+        Authorization: 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd',
+      },
+      body: formData,
+    });
+    // if (response) {
+    //   message.success('上传成功');
+    // }
+    console.log(response, '123');
+    if (!response) {
+      throw new Error('Network response was not ok');
+    }
+  } catch (error) {
+    console.error('保存失败:', error);
+  }
+}
+//停止响应
+async function stopReq() {
+  try {
+    let response = await fetch(`http://39.97.59.228:8000/v1/chat-messages/${taskID}/stop`, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+        Authorization: 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd',
+      },
+      body: JSON.stringify({
+        user: userid,
+      }),
+    });
+    if (!response) {
+      throw new Error('Network response was not ok');
+    }
+  } catch (error) {
+    console.error('保存失败:', error);
+  }
+}
+//获取具体会话记录
+async function sessionsHistory(id: string) {
+  console.log(id, '123');
+  try {
+    let response = await fetch(`http://39.97.59.228:8000/v1/messages?conversation_id=${id}&user=${userid}`, {
+      method: 'GET',
+      headers: {
+        'Content-Type': 'application/json',
+        Authorization: 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd',
+      },
+    });
+    const data = await response.json();
+    console.log(data, '123');
+    if (data.data.length > 0) {
+      messageHistory.value = [];
+      data.data.forEach((item: any) => {
+        messageHistory.value.push({
+          id: `user_${Date.now()}`,
+          type: 'user',
+          content: item.query,
+          contentR1: '',
+          timestamp: Date.now(),
+        });
+        messageHistory.value.push({
+          id: `system_${Date.now()}`,
+          type: 'system',
+          content: item.answer,
+          contentR1: '',
+          timestamp: Date.now(),
+        });
       });
+    }
+    if (!response.ok) {
+      throw new Error('Network response was not ok');
+    }
+  } catch (error) {
+    console.error('保存失败:', error);
   }
-
-  //格式化消息
-  function formatMessage(text: string) {
-    let formatted = text
-      // 处理换行
-      .replace(/\n\n/g, '<br>')
-      .replace(/\n###/g, '<br> ')
-      .replace(/###/g, '')
-      .replace(/---/g, '')
-      // 处理粗体
-      .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
-      // 处理斜体
-      .replace(/\*(.*?)\*/g, '<em>$1</em>')
-      // 处理行内代码
-      .replace(/`([^`]+)`/g, '<code>$1</code>');
-    return formatted;
-  }
-
-  // 初始化按钮定位
-  onMounted(() => {
-    // store.getSessionHistory();
+  editingId.value = null;
+}
+//编辑标题
+const startEditing = (item: ListItem) => {
+  editingId.value = item.id;
+  editText.value = item.name || '';
+};
+// 输入框确认按钮点击事件
+async function handlePathConfirm() {
+  const formData = new FormData();
+  // 添加文件参数
+  formData.append('file', filePath.value);
+  // 添加用户标识参数
+  formData.append('user', userid);
+  try {
+    let response = await fetch(`http://39.97.59.228:8000/v1/files/upload`, {
+      method: 'POST',
+      headers: {
+        Authorization: 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd',
+      },
+      body: formData,
+    });
+    if (!response.ok) {
+      throw new Error('Network response was not ok');
+    }
+  } catch (error) {
+    console.error('保存失败:', error);
+  }
+  console.log('确认的文件路径:', filePath.value);
+  // 这里可以添加路径验证、保存等逻辑
+  filePath.value = ''; // 可选:确认后清空输入框
+  showConfirmBtn.value = false;
+}
+// 保存修改
+const handleSave = async (item: ListItem) => {
+  try {
+    let response = await fetch(`http://39.97.59.228:8000/v1/conversations/${item.id}/name`, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+        Authorization: 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd',
+      },
+      body: JSON.stringify({
+        name: editText.value,
+        user: userid,
+      }),
+    });
+    if (!response.ok) {
+      throw new Error('Network response was not ok');
+    }
+    item.name = editText.value;
+  } catch (error) {
+    console.error('保存失败:', error);
+  }
+  editingId.value = null;
+};
+// 删除会话
+const startDelete = async (item: ListItem) => {
+  Modal.confirm({
+    title: '确认删除',
+    content: `确定要删除会话 "${item.name || '新会话'}" 吗?此操作不可撤销。`,
+    okText: '确认',
+    cancelText: '取消',
+    onOk: async () => {
+      // 原有删除逻辑不变
+      try {
+        let response = await fetch(`http://39.97.59.228:8000/v1/conversations/${item.id}`, {
+          method: 'DELETE',
+          headers: {
+            'Content-Type': 'application/json',
+            Authorization: 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd',
+          },
+          body: JSON.stringify({
+            user: userid,
+          }),
+        });
+        if (!response.ok) {
+          throw new Error('Network response was not ok');
+        }
+        getHistoryList();
+      } catch (error) {
+        console.error('删除失败:', error);
+        Modal.error({
+          title: '删除失败',
+          content: '删除会话时出现错误,请稍后重试。',
+        });
+      }
+    },
   });
+};
+const fold = () => {
+  isFold.value = !isFold.value;
+  if (!isFold.value) {
+    getHistoryList();
+  }
+};
+// 获取历史会话列表
+async function getHistoryList() {
+  let response = await fetch(`http://39.97.59.228:8000/v1/conversations?user=${userid}`, {
+    method: 'get',
+    headers: {
+      'Content-Type': 'application/json',
+      Authorization: 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd',
+    },
+  });
+  const data = await response.json();
+  sessionHistory.value = data.data;
+  console.log(sessionHistory.value, '123');
+}
+
+// 初始化按钮定位
+onMounted(() => {
+  getHistoryList();
+});
 </script>
 
 <style lang="less" scoped>
-  .mini-chat {
+.btn-header {
+  width: 40px;
+  height: 40px;
+  margin-right: 5px;
+  margin-bottom: 5px;
+}
+.mini-chat {
+  display: flex;
+  width: 500px;
+  height: 400px;
+  border-radius: 4px;
+  position: fixed;
+  top: 60px;
+  right: 20px;
+  background-color: rgb(255, 255, 255);
+  background: url('../../assets/images/warn-dialog-bg.png') no-repeat center;
+  background-size: 100% 100%;
+  z-index: 9999999;
+  color: #fff;
+}
+
+.left-side {
+  background: #0c2842;
+  transition: width 0.5s ease; /* 平滑过渡动画 */
+  width: 140px; /* 展开时宽度 */
+  position: relative; /* 用于按钮定位 */
+}
+.left-side.collapsed {
+  width: 40px; /* 折叠时宽度 */
+}
+
+.custom-list {
+  height: 325px;
+  overflow-y: auto;
+}
+.text-container {
+  display: flex;
+  justify-content: space-between;
+  width: 100%;
+  overflow: hidden;
+}
+.btn-container {
+  display: flex;
+}
+.jeecg-layout-header-action span[role='img'] {
+  padding: 0;
+}
+.text-ellipsis {
+  flex: 1;
+}
+.edit-text {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  width: 90px;
+  color: #fff;
+  font-size: 12px;
+  cursor: pointer;
+}
+.edit-icon {
+  flex-shrink: 0;
+  margin-left: auto;
+  line-height: 23px;
+}
+.delete-icon {
+  flex-shrink: 0;
+  margin-left: auto;
+  line-height: 23px;
+}
+.edit-icon:hover {
+  color: #1890ff !important;
+  cursor: pointer;
+}
+.delete-icon:hover {
+  color: #1890ff !important;
+  cursor: pointer;
+}
+.edit-input {
+  font-size: 10px;
+}
+.btn-text-bg {
+  width: 14px;
+  height: 14px;
+  position: absolute;
+  background-size: 100% 100%;
+  right: 10px;
+  top: 10px;
+  left: 10px;
+  bottom: 10px;
+}
+.btn-text {
+  margin-left: 3px;
+  font-size: 12px;
+  color: #fff;
+  white-space: nowrap;
+  margin-left: 30px;
+  line-height: 35px;
+}
+.historyBtn {
+  width: 20px;
+  height: 20px;
+  position: absolute;
+  background-size: 100% 100%;
+  background-position: center;
+  padding: 2px;
+  right: 10px;
+  top: 10px;
+}
+.historyBtn1 {
+  width: 20px;
+  height: 20px;
+  position: absolute;
+  background-size: 100% 100%;
+  background-position: center;
+  left: 3px;
+}
+.divider0 {
+  border-bottom: 1px solid #1074c1;
+  width: auto;
+  margin: 0 10px;
+  height: 13%;
+  display: block;
+  background: transparent;
+}
+.foldBtn {
+  width: 20px;
+  height: 20px;
+  position: absolute;
+  background-size: 100% 100%;
+  background-position: center;
+  padding: 2px;
+  right: 10px;
+  bottom: 10px;
+  cursor: pointer;
+}
+
+.right-side {
+  flex: 1; /* 占据剩余空间 */
+  display: flex;
+  flex-direction: column;
+
+  .dialog-area {
+    flex: 1; /* 占据剩余空间 */
+    gap: 10px; /* 消息块间隔统一控制 */
+    overflow-y: auto; /* 垂直滚动条 */
+    padding: 5px;
     display: flex;
+    flex-direction: column;
+    color: #fff;
+
+    .ask-message {
+      padding: 10px;
+      border-radius: 5px;
+      background: #0c2842;
+      max-width: 80%;
+    }
+    .answer-message {
+      padding: 10px;
+      border-radius: 5px;
+      background: #0c2842;
+      max-width: 90%;
+    }
   }
 
-  .left-side {
-    width: 40px; /* 折叠时宽度 */
-    background: #0c2842;
-    transition: width 0.5s ease; /* 平滑过渡动画 */
+  .input-area {
+    margin: 10px 10px 20px 10px;
+    padding: 10px;
+    background-color: #043256;
+    border: 1px solid #2cb6ff;
+    border-radius: 5px;
     display: flex;
     flex-direction: column;
-    justify-content: space-around;
+    justify-content: space-between;
+    gap: 5px;
+    height: 25%;
+  }
+  /* 文件列表容器 */
+  .uploaded-files {
+    padding: 8px;
+    background-color: #f5f5f5;
+    border-radius: 4px;
+    min-height: 40px;
+  }
+
+  /* 单个文件项 */
+  .file-item {
+    display: inline-flex;
     align-items: center;
+    padding: 4px 8px;
+    margin-right: 8px;
+    margin-bottom: 8px;
+    background-color: #fff;
+    border: 1px solid #e9e9e9;
+    border-radius: 4px;
   }
 
-  .right-side {
-    flex: 1; /* 占据剩余空间 */
-    background: #09172c;
+  /* 文件名 */
+  .file-name {
+    margin-left: 8px;
+    margin-right: 8px;
+    max-width: 150px;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+
+  .ant-input {
+    background-color: rgba(255, 255, 255, 0) !important;
+    border: none;
+    outline: none;
+  }
+  .ant-input:focus {
+    border: none; /* 聚焦时无边框 */
+    outline: none; /* 聚焦时无轮廓 */
+    box-shadow: none; /* 移除可能存在的阴影效果 */
+  }
+  .ctrl-btn {
+    display: flex;
+    flex-direction: row;
+    justify-content: space-between;
+  }
+  .question-input {
+    background-color: #1e293b !important;
+    border-color: #334155 !important;
+    color: #e2e8f0 !important;
+    border-radius: 8px !important;
+    padding: 12px 16px !important;
+    font-size: 14px !important;
+  }
+
+  .question-input::placeholder {
+    color: #64748b !important;
+  }
+
+  .control-btn {
+    height: 25px;
+    background-color: #043256;
+    border: 1px solid #2cb6ff;
+    color: #fff;
+    font-size: 10px;
+    margin-right: 10px;
+    cursor: pointer;
+    transition: all 0.2s;
+  }
+
+  .control-btn:hover {
+    background-color: #043256;
+    color: #e2e8f0;
+  }
+  .control-btn1 {
+    height: 20px;
+    background-color: #234a6b;
+    border: 1px solid #234a6b;
+    color: #fff;
+    font-size: 10px;
+    margin-right: 10px;
+    cursor: pointer;
+    transition: all 0.2s;
+  }
+  .control-btn2 {
+    height: 20px;
+    background-color: #2cb6ff;
+    border: 1px solid #2cb6ff;
+    color: #fff;
+    font-size: 10px;
+    margin-right: 10px;
+    cursor: pointer;
+    transition: all 0.2s;
+  }
+  /* 文件上传区 */
+  .file-upload {
+    position: absolute;
+    right: 20px;
+    bottom: 70px;
+    width: 180px;
     display: flex;
     flex-direction: column;
+    gap: 10px;
+    border: 1px solid #2cb6ff;
+    background-color: #234a6b;
+    border-radius: 6px;
+    padding: 10px;
+  }
 
-    .dialog-area {
-      flex: 1; /* 占据剩余空间 */
-      gap: 10px; /* 消息块间隔统一控制 */
-      overflow-y: auto; /* 垂直滚动条 */
-      padding: 5px;
-      display: flex;
-      flex-direction: column;
-      color: #fff;
-
-      .ask-message {
-        padding: 10px;
-        border-radius: 5px;
-        background: #0c2842;
-        max-width: 80%;
-      }
-      .answer-message {
-        padding: 10px;
-        border-radius: 5px;
-        background: #0c2842;
-        max-width: 90%;
-      }
-    }
+  .input-container {
+    position: relative;
+    display: flex;
+    align-items: center;
+    width: 100%;
+  }
 
-    .input-area {
-      background-color: #043256;
-      padding: 5px;
-
-      textarea {
-        background-color: transparent;
-        width: 100%;
-        height: 40px;
-        border: none;
-        resize: none;
-        outline: none;
-        overflow: hidden;
-        padding: 10px; /* 统一内边距 */
-        color: #fff;
-      }
+  .file-input {
+    flex: 1;
+    background-color: #234a6b;
+    border-color: #2cb6ff !important;
+    color: #e2e8f0 !important;
+    border-radius: 6px !important;
+    font-size: 10px !important;
+    padding-right: 70px !important;
+    height: 36px !important;
+    width: 100% !important;
+  }
 
-      .action-bar {
-        height: 30px;
-        display: flex;
-        align-items: center;
-        justify-content: space-between;
-
-        .think-btn {
-          border: 1px solid #ccc;
-          width: 100px;
-          height: 20px;
-          line-height: 20px;
-          text-align: center;
-          border-radius: 5px;
-          cursor: pointer;
-          background: transparent;
-          color: white;
-          transition: background 0.3s;
-        }
+  .confirm-btn {
+    position: absolute;
+    right: 5px;
+    background-color: #2cb6ff;
+    border: none;
+    color: #fff;
+    border-radius: 4px;
+    font-size: 12px;
+    padding: 4px 10px;
+    cursor: pointer;
+    transition: all 0.2s;
+    height: 28px;
+  }
 
-        .think-btn.active {
-          background: #1890ff;
-          color: white;
-          border-color: #1890ff;
-        }
-      }
-    }
+  .confirm-btn:hover {
+    background-color: #2cb6ff;
   }
-</style>
-<style>
-  .zxm-popover-inner-content {
-    padding: 1px;
+
+  .custom-upload {
+    width: 100%;
+    padding: 0 !important;
   }
+
+  .upload-btn {
+    background-color: #234a6b !important;
+    border: 1px solid #2188c3 !important;
+    color: #dbeafe !important;
+    border-radius: 6px !important;
+    font-size: 12px !important;
+    cursor: pointer;
+    transition: all 0.2s;
+    padding: 8px 0 !important;
+    width: 190% !important;
+    height: 36px !important;
+    box-sizing: border-box !important;
+  }
+  .upload-btn:hover {
+    background-color: #1f84bd !important;
+    color: #fff !important;
+  }
+  .custom-upload .ant-upload-select:hover .ant-btn {
+    border-color: #1f84bd !important;
+  }
+}
+</style>
+<style scoped>
+.zxm-popover-inner-content {
+  padding: 1px;
+}
+.message-wrapper {
+  display: flex;
+  align-items: flex-start;
+  position: relative;
+}
+.user-message-wrapper {
+  flex-direction: row-reverse;
+}
+.ai-message-wrapper {
+  flex-direction: row;
+}
+/* 复制图标样式 */
+.copy-icon {
+  font-size: 16px;
+  cursor: pointer;
+  margin: 4px 8px;
+  color: #fff;
+}
+.message-wrapper:hover .copy-icon {
+  opacity: 1;
+}
+.copy-icon:hover {
+  color: #1890ff;
+}
 </style>

+ 3 - 0
src/layouts/default/header/index.vue

@@ -60,6 +60,7 @@
       <!-- 公司端不显示语音播报功能 weatherBroadcast.vue-->
       <WeatherBroadcast v-if="sysOrgCode != 'sdmtjtgsd' && isShowQy && portValue != '8062'" />
       <VoiceBroadcast v-if="sysOrgCode != 'sdmtjtgsd' && portValue != '8062'" />
+      <AIChat></AIChat>
       <VoiceBroadcastGsd v-if="sysOrgCode == 'sdmtjtgsd'" />
       <UserDropDown v-if="showUserDropdown" :theme="getHeaderTheme" />
       <LoginSelect ref="loginSelectRef" @success="loginSelectOk" />
@@ -96,6 +97,7 @@ import { useLocale } from '/@/locales/useLocale';
 import WeatherBroadcast from './components/weatherBroadcast.vue';
 import VoiceBroadcast from './components/VoiceBroadcast.vue';
 import VoiceBroadcastGsd from './components/VoiceBroadcastGsd.vue';
+import AIChat from '/@/components/AIChat/MiniChat.vue';
 
 import LoginSelect from '/@/views/sys/login/LoginSelect.vue';
 import { useUserStore } from '/@/store/modules/user';
@@ -121,6 +123,7 @@ export default defineComponent({
     LockScreen,
     LoginSelect,
     VoiceBroadcast,
+    AIChat,
     VoiceBroadcastGsd,
     WeatherBroadcast,
     SettingDrawer: createAsyncComponent(() => import('/@/layouts/default/setting/index.vue'), {

+ 3 - 0
src/views/vent/dataCenter/APICenter/ApiAddModal.vue

@@ -10,6 +10,7 @@ import { BasicForm, useForm } from '/@/components/Form/index';
 import { formAddSchema } from './apiManger.data';
 import { apiManageList, AddOrEdit, apiManageQueryByID } from './apiManger.api';
 const isAdd = ref(true);
+const currentRecordId = ref('');
 // 声明Emits
 const emit = defineEmits(['success', 'register']);
 //表单配置
@@ -24,6 +25,7 @@ const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data
   setModalProps({ confirmLoading: false });
   //获取详情
   data.record = await apiManageQueryByID({ id: data?.record.id });
+  currentRecordId.value = data.record.id;
   isAdd.value = !!data?.isUpdate;
   await setFieldsValue({
     ...data.record,
@@ -33,6 +35,7 @@ const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data
 async function handleSubmit() {
   try {
     let values = await validate();
+    values.id = currentRecordId.value;
     setModalProps({ confirmLoading: true });
     //提交表单
     await AddOrEdit(values, unref(isAdd.value));