Browse Source

[Feat 0000] 添加可配置首页适用的mini aichat

houzekong 2 months ago
parent
commit
6033472aef
3 changed files with 495 additions and 15 deletions
  1. 1 0
      src/assets/icons/ai-logo.svg
  2. 486 0
      src/components/AIChat/MiniChat.vue
  3. 8 15
      src/components/AIChat/index.vue

File diff suppressed because it is too large
+ 1 - 0
src/assets/icons/ai-logo.svg


+ 486 - 0
src/components/AIChat/MiniChat.vue

@@ -0,0 +1,486 @@
+<template>
+  <div class="mini-chat">
+    <!-- 左侧折叠区域 -->
+    <div class="left-side">
+      <SvgIcon name="add" size="20" />
+      <SvgIcon name="zoom-out" size="20" />
+    </div>
+    <!-- 右侧对话框 -->
+    <div class="right-side">
+      <!-- 对话区域 -->
+      <div class="dialog-area">
+        <div
+          v-for="message in sortedMessages"
+          :key="message.id"
+          class="flex items-center"
+          :style="{ alignSelf: message.type === 'user' ? 'flex-end' : 'flex-start' }"
+        >
+          <SvgIcon v-if="message.type !== 'user'" size="30" class="ml-2px mr-2px" name="ai-logo" />
+          <div v-if="message.type !== 'user'" class="answer-message" v-html="formatMessage(message.content)"></div>
+          <div v-if="message.type === 'user'" class="ask-message">{{ message.content }}</div>
+        </div>
+      </div>
+      <!-- 底部操作栏 -->
+      <div class="input-area">
+        <Textarea v-model:value="inputText" placeholder="请输入你的问题" />
+        <div class="action-bar">
+          <!-- 左侧深度思考按钮 -->
+          <div class="think-btn" :class="{ active: isThinking }" @click="toggleThinking"> <span>深度思考</span> </div>
+
+          <!-- 右侧操作按钮 -->
+          <Space>
+            <SvgIcon name="send-image" />
+            <SvgIcon name="send-file" />
+            <Button type="primary" shape="circle" size="small" :loading="spinning" @click="handleSend">
+              <template #icon>
+                <SvgIcon name="send" />
+              </template>
+            </Button>
+          </Space>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+  import { ref, onMounted, unref, nextTick, computed } from 'vue';
+  import { useUserStore } from '/@/store/modules/user';
+  import { EditOutlined } from '@ant-design/icons-vue';
+  import { SvgIcon } from '../Icon';
+  import { CheckableTag, Space, Button, Textarea, Switch } from 'ant-design-vue';
+  // import SelfComponent from '/@/components/AIChat/index.vue';
+  // 响应式变量声明
+  const dialogVisible = ref(false);
+  const isFold = ref(true); // 是否折叠
+  const inputText = ref(''); // 输入框内容
+  const historySessions = ref([]); // 消会话历史
+  const spinning = ref(false); // 加载状态
+  const systemMessage = ref(''); // 系统返回信息
+  const session_id = ref(''); // 会话id
+  const hasCreated = ref(false); // 标志位,防止重复调用create接口
+  const hasAdd = ref(false); // 标志位,防止重复调用create接口
+  const userStore = useUserStore(); //获取用户信息
+  const editingId = ref<number | null>(null);
+  const editText = ref('');
+  const isThinking = ref(false);
+  interface ListItem {
+    id: number;
+    title?: string;
+  }
+  let userId = unref(userStore.getUserInfo).id;
+  // const userId = ref(0);
+  type MessageItem = {
+    id: string; // 唯一标识(可用时间戳生成)
+    type: 'user' | 'system';
+    content: string;
+    timestamp: number; // 排序依据
+  };
+  const messageList = ref<MessageItem[]>([]);
+  const sortedMessages = computed(() => {
+    const list = messageList.value;
+    return list.sort((a, b) => a.timestamp - b.timestamp);
+  });
+  const vFocus = {
+    mounted: (el: HTMLElement) => el.querySelector('input')?.focus(),
+  };
+  const scrollToBottom = () => {
+    const dialogArea = document.querySelector('.dialog-area');
+    if (dialogArea) {
+      dialogArea.scrollTop = dialogArea.scrollHeight;
+    }
+  };
+
+  const openMenu = () => {
+    dialogVisible.value = !dialogVisible.value;
+    if (dialogVisible.value) {
+      // addNew();
+      hasCreated.value = true;
+    }
+  };
+  const fold = () => {
+    // isFold.value = !isFold.value;
+    // if (!isFold.value) {
+    //   sessionsHistoryList();
+    // }
+  };
+  //启用深度思考
+  const toggleThinking = () => {
+    isThinking.value = !isThinking.value;
+  };
+  //创建新对话
+  async function addNew() {
+    hasAdd.value = !hasAdd.value;
+    const params = {
+      user_id: userId,
+    };
+    let response = await fetch('http://182.92.126.35:6005/sessions/create', {
+      method: 'post',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify(params),
+    });
+    const data = await response.json();
+    session_id.value = data.id;
+    messageList.value = [];
+  }
+  //编辑标题
+  const startEditing = (item: ListItem) => {
+    editingId.value = item.id;
+    editText.value = item.title || '';
+  };
+
+  // 保存修改
+  const handleSave = async (item: ListItem) => {
+    const params = {
+      chat_session_id: item.id,
+      new_title: editText.value,
+    };
+    try {
+      let response = await fetch('http://182.92.126.35:6005/sessions/change_title', {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify(params),
+      });
+      if (!response.ok) {
+        throw new Error('Network response was not ok');
+      }
+      item.title = editText.value;
+    } catch (error) {
+      console.error('保存失败:', error);
+    }
+    editingId.value = null;
+  };
+  //获取消息列表
+  async function handleSend() {
+    if (session_id.value === '') {
+      await addNew();
+      createSessionTitle({ session_id: session_id.value, title: inputText.value });
+      sendMessage1();
+    } else {
+      createSessionTitle({ session_id: session_id.value, title: inputText.value });
+      sendMessage1();
+    }
+  }
+  //发送消息
+  async function sendMessage() {
+    spinning.value = true;
+    // 添加用户消息
+    messageList.value.push({
+      id: `user_${Date.now()}`,
+      type: 'user',
+      content: inputText.value,
+      timestamp: Date.now(),
+    });
+    const params = {
+      chat_session_id: session_id.value,
+      prompt: inputText.value,
+      ref_file_ids: [],
+      thinking_enabled: false,
+    };
+    inputText.value = ''; // 清空输入框
+    //将用户输入的内容发送到后端
+    try {
+      // 将用户输入的内容发送到后端
+      let response = await fetch('http://182.92.126.35:6005/chat', {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify(params),
+      });
+
+      if (!response.ok) {
+        throw new Error('Network response was not ok');
+      }
+
+      const data = await response.json();
+      const assistantReply = data.reply.content; // 获取助手回复
+      // formatMessage(assistantReply);
+      systemMessage.value = assistantReply;
+
+      // 添加系统回答
+      messageList.value.push({
+        id: `system_${Date.now()}`,
+        type: 'system',
+        content: systemMessage.value,
+        timestamp: Date.now(),
+      });
+    } catch (error) {
+      // 请求失败时设置系统消息为"服务器异常"
+      systemMessage.value = '服务器异常';
+      console.error('请求失败:', error);
+    } finally {
+      spinning.value = false; // 无论请求成功与否,都停止加载指示器
+    }
+  }
+  //发送消息  流式响应
+  const sendMessage1 = async () => {
+    spinning.value = true; // 开始加载
+    messageList.value.push({
+      id: `user_${Date.now()}`,
+      type: 'user',
+      content: inputText.value,
+      timestamp: Date.now(),
+    });
+
+    // 构造请求参数
+    const params = {
+      chat_session_id: session_id.value, // 替换为实际的会话 ID
+      prompt: inputText.value,
+      ref_file_ids: [],
+      thinking_enabled: isThinking.value,
+    };
+    inputText.value = ''; // 清空输入框
+    try {
+      // 发送 POST 请求
+      const response = await fetch('http://182.92.126.35:6005/chat_stream', {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify(params),
+      });
+
+      // 检查响应是否成功
+      if (!response.ok) {
+        throw new Error('Network response was not ok');
+      }
+
+      // 获取可读流
+      const reader = response.body.getReader();
+
+      // 创建一条新的消息对象
+      const newMessage = {
+        id: `response_${Date.now()}`,
+        type: 'response', // 消息类型
+        content: '',
+        timestamp: Date.now(), // 时间戳用来排序
+      };
+
+      // 将新消息添加到消息列表
+      messageList.value.push(newMessage);
+
+      // 读取流式数据
+      while (true) {
+        const { done, value } = await reader.read();
+        if (done) {
+          console.log('Stream complete');
+          break;
+        }
+
+        // 将流数据转换为字符串
+        const chunk = new TextDecoder().decode(value);
+        console.log('Received chunk:', chunk);
+
+        // 使用正则表达式匹配完整的 JSON 对象
+        const jsonRegex = /{.*?}/g;
+        const matches = chunk.match(jsonRegex);
+
+        if (matches) {
+          matches.forEach((match) => {
+            try {
+              const data = JSON.parse(match);
+              if (data.type === 'text') {
+                // 找到当前消息对象并更新 content
+                const targetMessage = messageList.value.find((msg) => msg.id === newMessage.id);
+                if (targetMessage) {
+                  targetMessage.content += data.content; // 追加内容
+                  scrollToBottom();
+                }
+              }
+            } catch (error) {
+              console.error('Failed to parse JSON:', error);
+            }
+          });
+        }
+      }
+    } catch (error) {
+      // 请求失败时设置系统消息
+      if (!response || !response.ok) {
+        systemMessage.value = '服务器异常';
+        messageList.value.push({
+          id: `system_${Date.now()}`,
+          type: 'system',
+          content: systemMessage.value,
+          timestamp: Date.now(),
+        });
+        console.error('请求失败:', error);
+      }
+    } finally {
+      spinning.value = false; // 停止加载
+    }
+  };
+  //创建标题
+  async function createSessionTitle({ session_id, title }) {
+    const params = {
+      chat_session_id: session_id,
+      prompt: title,
+    };
+    let response = await fetch('http://182.92.126.35:6005/sessions/title', {
+      method: 'post',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify(params),
+    });
+    const data = await response.json();
+  }
+  //获取会话历史
+  async function sessionsHistoryList() {
+    const params = {
+      user_id: userId,
+    };
+    let response = await fetch(`http://182.92.126.35:6005/sessions`, {
+      method: 'post',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify(params),
+    });
+    const data = await response.json();
+    historySessions.value = data.chat_sessions;
+  }
+  //获取具体会话记录
+  async function sessionsHistory(id: string) {
+    let response = await fetch(`http://182.92.126.35:6005/sessions/history_chat/?chat_session_id=${id}`, {
+      method: 'get',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+    });
+    const data = await response.json();
+    if (data.chat_messages.length > 0) {
+      messageList.value = [];
+      data.chat_messages.forEach((item: any) => {
+        // role== user 用户提问
+        if (item.role === 'user') {
+          messageList.value.push({
+            id: `user_${Date.now()}`,
+            type: 'user',
+            content: item.content,
+            timestamp: Date.now(),
+          });
+        } else {
+          // role== assistant 机器回答
+          messageList.value.push({
+            id: `system_${Date.now()}`,
+            type: 'system',
+            content: item.content,
+            timestamp: Date.now(),
+          });
+        }
+      });
+    }
+  }
+  //格式化消息
+  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(() => {
+    sessionsHistoryList();
+  });
+</script>
+
+<style lang="less" scoped>
+  .mini-chat {
+    display: flex;
+  }
+
+  .left-side {
+    width: 40px; /* 折叠时宽度 */
+    background: #0c2842;
+    transition: width 0.5s ease; /* 平滑过渡动画 */
+    display: flex;
+    flex-direction: column;
+    justify-content: space-around;
+    align-items: center;
+  }
+
+  .right-side {
+    flex: 1; /* 占据剩余空间 */
+    background: #09172c;
+    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;
+      }
+      .answer-message {
+        padding: 10px;
+        border-radius: 5px;
+        background: #0c2842;
+      }
+    }
+
+    .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;
+      }
+
+      .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;
+        }
+
+        .think-btn.active {
+          background: #1890ff;
+          color: white;
+          border-color: #1890ff;
+        }
+      }
+    }
+  }
+</style>

+ 8 - 15
src/components/AIChat/index.vue

@@ -152,7 +152,7 @@
     id: number;
     id: number;
     title?: string;
     title?: string;
   }
   }
-  defineProps({
+  const props = defineProps({
     visible: {
     visible: {
       type: Boolean,
       type: Boolean,
       required: true,
       required: true,
@@ -183,14 +183,12 @@
     }
     }
   };
   };
 
 
-  // const openMenu = () => {
-  //   dialogVisible.value = props.modelValue;
-  //   // console.log(props.dialogVisible, 'ssssssssss');
-  //   if (dialogVisible.value) {
-  //     // addNew();
-  //     hasCreated.value = true;
-  //   }
-  // };
+  const openMenu = () => {
+    if (props.visible) {
+      addNew();
+      hasCreated.value = true;
+    }
+  };
   const fold = () => {
   const fold = () => {
     isFold.value = !isFold.value;
     isFold.value = !isFold.value;
     if (!isFold.value) {
     if (!isFold.value) {
@@ -362,13 +360,11 @@
       while (true) {
       while (true) {
         const { done, value } = await reader.read();
         const { done, value } = await reader.read();
         if (done) {
         if (done) {
-          console.log('Stream complete');
           break;
           break;
         }
         }
 
 
         // 将流数据转换为字符串
         // 将流数据转换为字符串
         const chunk = new TextDecoder().decode(value);
         const chunk = new TextDecoder().decode(value);
-        console.log('Received chunk:', chunk);
 
 
         // 使用正则表达式匹配完整的 JSON 对象
         // 使用正则表达式匹配完整的 JSON 对象
         const jsonRegex = /{.*?}/g;
         const jsonRegex = /{.*?}/g;
@@ -377,7 +373,6 @@
           matches.forEach((match) => {
           matches.forEach((match) => {
             try {
             try {
               const data = JSON.parse(match);
               const data = JSON.parse(match);
-              console.log(data.type, '数据类型11111111111111111');
               if (data.type === 'thinking') {
               if (data.type === 'thinking') {
                 // 找到当前消息对象并更新 content
                 // 找到当前消息对象并更新 content
                 const targetMessage = messageList.value.find((msg) => msg.id === newMessage.id);
                 const targetMessage = messageList.value.find((msg) => msg.id === newMessage.id);
@@ -494,8 +489,6 @@
               timestamp: Date.now(),
               timestamp: Date.now(),
             });
             });
           }
           }
-
-          console.log(item.content);
         }
         }
       });
       });
     }
     }
@@ -525,7 +518,7 @@
   // 初始化按钮定位
   // 初始化按钮定位
   onMounted(() => {
   onMounted(() => {
     sessionsHistoryList();
     sessionsHistoryList();
-    console.log('ssssssssss');
+    openMenu();
   });
   });
 </script>
 </script>
 
 

Some files were not shown because too many files changed in this diff