Pārlūkot izejas kodu

Merge branch 'master' of http://59.110.91.129:3000/zhangmengying/AIClass

丸子 9 mēneši atpakaļ
vecāks
revīzija
31731b791a
5 mainītis faili ar 349 papildinājumiem un 59 dzēšanām
  1. 90 0
      package-lock.json
  2. 3 0
      package.json
  3. 2 1
      src/api/questions.js
  4. 192 0
      src/components/MarkdownView/index.vue
  5. 62 58
      src/views/AIQuestions.vue

+ 90 - 0
package-lock.json

@@ -11,11 +11,14 @@
         "@microsoft/fetch-event-source": "^2.0.1",
         "axios": "^1.10.0",
         "element-plus": "^2.10.2",
+        "highlight.js": "^11.11.1",
+        "markdown-it": "^14.1.0",
         "router": "^2.2.0",
         "vue": "^3.5.17",
         "vue-router": "^4.5.1"
       },
       "devDependencies": {
+        "@types/markdown-it": "^14.1.2",
         "@types/node": "^24.0.10",
         "@vitejs/plugin-vue": "^6.0.0",
         "path": "^0.12.7",
@@ -1164,6 +1167,13 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@types/linkify-it": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
+      "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@types/lodash": {
       "version": "4.17.19",
       "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.19.tgz",
@@ -1179,6 +1189,24 @@
         "@types/lodash": "*"
       }
     },
+    "node_modules/@types/markdown-it": {
+      "version": "14.1.2",
+      "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
+      "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/linkify-it": "^5",
+        "@types/mdurl": "^2"
+      }
+    },
+    "node_modules/@types/mdurl": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
+      "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@types/node": {
       "version": "24.0.10",
       "resolved": "https://registry.npmmirror.com/@types/node/-/node-24.0.10.tgz",
@@ -1406,6 +1434,12 @@
         }
       }
     },
+    "node_modules/argparse": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+      "license": "Python-2.0"
+    },
     "node_modules/async-validator": {
       "version": "4.2.5",
       "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz",
@@ -1872,6 +1906,15 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/highlight.js": {
+      "version": "11.11.1",
+      "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
+      "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
     "node_modules/immutable": {
       "version": "5.1.3",
       "resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.3.tgz",
@@ -1928,6 +1971,15 @@
       "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
       "license": "MIT"
     },
+    "node_modules/linkify-it": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
+      "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
+      "license": "MIT",
+      "dependencies": {
+        "uc.micro": "^2.0.0"
+      }
+    },
     "node_modules/lodash": {
       "version": "4.17.21",
       "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
@@ -1960,6 +2012,23 @@
         "@jridgewell/sourcemap-codec": "^1.5.0"
       }
     },
+    "node_modules/markdown-it": {
+      "version": "14.1.0",
+      "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
+      "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
+      "license": "MIT",
+      "dependencies": {
+        "argparse": "^2.0.1",
+        "entities": "^4.4.0",
+        "linkify-it": "^5.0.0",
+        "mdurl": "^2.0.0",
+        "punycode.js": "^2.3.1",
+        "uc.micro": "^2.1.0"
+      },
+      "bin": {
+        "markdown-it": "bin/markdown-it.mjs"
+      }
+    },
     "node_modules/math-intrinsics": {
       "version": "1.1.0",
       "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -1969,6 +2038,12 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/mdurl": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
+      "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
+      "license": "MIT"
+    },
     "node_modules/memoize-one": {
       "version": "6.0.0",
       "resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz",
@@ -2155,6 +2230,15 @@
       "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
       "license": "MIT"
     },
+    "node_modules/punycode.js": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
+      "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/readdirp": {
       "version": "4.1.2",
       "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz",
@@ -2286,6 +2370,12 @@
         "node": ">=8.0"
       }
     },
+    "node_modules/uc.micro": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
+      "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
+      "license": "MIT"
+    },
     "node_modules/undici-types": {
       "version": "7.8.0",
       "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.8.0.tgz",

+ 3 - 0
package.json

@@ -12,11 +12,14 @@
     "@microsoft/fetch-event-source": "^2.0.1",
     "axios": "^1.10.0",
     "element-plus": "^2.10.2",
+    "highlight.js": "^11.11.1",
+    "markdown-it": "^14.1.0",
     "router": "^2.2.0",
     "vue": "^3.5.17",
     "vue-router": "^4.5.1"
   },
   "devDependencies": {
+    "@types/markdown-it": "^14.1.2",
     "@types/node": "^24.0.10",
     "@vitejs/plugin-vue": "^6.0.0",
     "path": "^0.12.7",

+ 2 - 1
src/api/questions.js

@@ -2,7 +2,6 @@ import axios from "@/utils/request";
 
 import { fetchEventSource } from '@microsoft/fetch-event-source'
 
-
 // 数字人对话框
 export function CreateDialogue (data){
   return axios({
@@ -24,7 +23,9 @@ export async function sendChatMessageStream (
     onError,
     onClose
 ) {
+
   return fetchEventSource(`http://192.168.110.8:8080/admin-api/bjdxWeb/ai/dialogue-send-stream`, {
+
     method: 'post',
     headers: {
       'Content-Type': 'application/json',

+ 192 - 0
src/components/MarkdownView/index.vue

@@ -0,0 +1,192 @@
+<template>
+  <div ref="contentRef" class="markdown-view" v-html="renderedMarkdown"></div>
+</template>
+
+<script setup lang="ts">
+import MarkdownIt from 'markdown-it'
+import 'highlight.js/styles/vs2015.min.css'
+import hljs from 'highlight.js'
+import { ref, computed} from 'vue'
+
+// 定义组件属性
+const props = defineProps({
+  content: {
+    type: String,
+    required: true
+  }
+})
+
+const contentRef = ref()
+
+const md = new MarkdownIt({
+  highlight: function (str, lang) {
+    if (lang && hljs.getLanguage(lang)) {
+      try {
+        const copyHtml = `<div id="copy" data-copy='${str}' style="position: absolute; right: 10px; top: 5px; color: #fff;cursor: pointer;">复制</div>`
+        return `<pre style="position: relative;">${copyHtml}<code class="hljs">${hljs.highlight(lang, str, true).value}</code></pre>`
+      } catch (__) {}
+    }
+    return ``
+  }
+})
+
+/** 渲染 markdown */
+const renderedMarkdown = computed(() => {
+  return md.render(props.content)
+})
+
+</script>
+
+<style lang="scss">
+.markdown-view {
+  font-family: PingFang SC;
+  font-size: 0.95rem;
+  font-weight: 400;
+  line-height: 1.6rem;
+  letter-spacing: 0em;
+  text-align: left;
+  color: #3b3e55;
+  max-width: 100%;
+
+  pre {
+    position: relative;
+  }
+
+  pre code.hljs {
+    width: auto;
+  }
+
+  code.hljs {
+    border-radius: 6px;
+    padding-top: 20px;
+    width: auto;
+    @media screen and (min-width: 1536px) {
+      width: 960px;
+    }
+
+    @media screen and (max-width: 1536px) and (min-width: 1024px) {
+      width: calc(100vw - 400px - 64px - 32px * 2);
+    }
+
+    @media screen and (max-width: 1024px) and (min-width: 768px) {
+      width: calc(100vw - 32px * 2);
+    }
+
+    @media screen and (max-width: 768px) {
+      width: calc(100vw - 16px * 2);
+    }
+  }
+
+  p,
+  code.hljs {
+    margin-bottom: 16px;
+  }
+
+  p {
+    //margin-bottom: 1rem !important;
+    margin: 0;
+    margin-bottom: 3px;
+  }
+
+  /* 标题通用格式 */
+  h1,
+  h2,
+  h3,
+  h4,
+  h5,
+  h6 {
+    color: var(--color-G900);
+    margin: 24px 0 8px;
+    font-weight: 600;
+  }
+
+  h1 {
+    font-size: 22px;
+    line-height: 32px;
+  }
+
+  h2 {
+    font-size: 20px;
+    line-height: 30px;
+  }
+
+  h3 {
+    font-size: 18px;
+    line-height: 28px;
+  }
+
+  h4 {
+    font-size: 16px;
+    line-height: 26px;
+  }
+
+  h5 {
+    font-size: 16px;
+    line-height: 24px;
+  }
+
+  h6 {
+    font-size: 16px;
+    line-height: 24px;
+  }
+
+  /* 列表(有序,无序) */
+  ul,
+  ol {
+    margin: 0 0 8px 0;
+    padding: 0;
+    font-size: 16px;
+    line-height: 24px;
+    color: #3b3e55; // var(--color-CG600);
+  }
+
+  li {
+    margin: 4px 0 0 20px;
+    margin-bottom: 1rem;
+  }
+
+  ol > li {
+    list-style-type: decimal;
+    margin-bottom: 1rem;
+    // 表达式,修复有序列表序号展示不全的问题
+    // &:nth-child(n + 10) {
+    //     margin-left: 30px;
+    // }
+
+    // &:nth-child(n + 100) {
+    //     margin-left: 30px;
+    // }
+  }
+
+  ul > li {
+    list-style-type: disc;
+    font-size: 16px;
+    line-height: 24px;
+    margin-right: 11px;
+    margin-bottom: 1rem;
+    color: #3b3e55; // var(--color-G900);
+  }
+
+  ol ul,
+  ol ul > li,
+  ul ul,
+  ul ul li {
+    // list-style: circle;
+    font-size: 16px;
+    list-style: none;
+    margin-left: 6px;
+    margin-bottom: 1rem;
+  }
+
+  ul ul ul,
+  ul ul ul li,
+  ol ol,
+  ol ol > li,
+  ol ul ul,
+  ol ul ul > li,
+  ul ol,
+  ul ol > li {
+    list-style: square;
+  }
+}
+</style>

+ 62 - 58
src/views/AIQuestions.vue

@@ -23,13 +23,15 @@
             <div v-for="(item, index) in messageList" :key="index">
               <!-- AI消息 -->
               <div class="ai-message" v-if="item.type !== 'user'">
-                {{ item.content }}
+                 <MarkdownView class="left-text" :content="item.content" />
+<!--                {{item.content}}-->
               </div>
 
               <!-- 用户消息 -->
               <div class="user-message" v-if="item.type === 'user'">
                 {{ item.content }}
               </div>
+
             </div>
           </div>
           <!-- 输入框和发送按钮 -->
@@ -52,6 +54,7 @@
 import { ref, onMounted, computed } from 'vue'
 import { CreateDialogue, sendChatMessageStream } from '@/api/questions.js'
 import { useRouter, useRoute } from 'vue-router'
+import MarkdownView from '@/components/MarkdownView/index.vue'
 import {
   Document,
   Menu as IconMenu,
@@ -83,7 +86,7 @@ onMounted(() => {
 })
 
 // 聊天对话
-const activeConversationModelPath = ref(null) // 选中的对话编号
+const activeConversationModelPath= ref(null) // 选中的对话编号
 const activeConversationId = ref(null) // 选中的对话编号
 const activeConversation = ref(null) // 选中的 Conversation
 const conversationInProgress = ref(false) // 对话是否正在进行中。目前只有【发送】消息时,会更新为 true,避免切换对话、删除对话等操作,导致 stream 中断
@@ -107,10 +110,11 @@ const enableContext = ref(true) // 是否开启上下文
 const receiveMessageFullText = ref('')
 const receiveMessageDisplayedText = ref('')
 
+
 // =========== 【聊天对话】相关 ===========
 
 /** 获取对话信息 */
-const getConversation = async id => {
+const getConversation = async (id) => {
   if (!id) {
     return
   }
@@ -124,10 +128,11 @@ const getConversation = async id => {
   activeConversationModelPath.value = personImage.value
 }
 
+
 // =========== 【发送消息】相关 ===========
 
 /** 处理来自 keydown 的发送消息 */
-const handleSendByKeydown = async event => {
+const handleSendByKeydown = async (event) => {
   // 判断用户是否在输入
   if (isComposing.value) {
     return
@@ -156,7 +161,7 @@ const handleSendByButton = () => {
 }
 
 /** 处理 prompt 输入变化 */
-const handlePromptInput = event => {
+const handlePromptInput = (event) => {
   // 非输入法 输入设置为 true
   if (!isComposing.value) {
     // 回车 event data 是 null
@@ -185,7 +190,7 @@ const onCompositionend = () => {
 }
 
 /** 真正执行【发送】消息操作 */
-const doSendMessage = async content => {
+const doSendMessage = async (content) => {
   // 校验
   if (content.length < 1) {
     console.error('发送失败,原因:内容为空!')
@@ -206,7 +211,8 @@ const doSendMessage = async content => {
 }
 
 /** 真正执行【发送】消息操作 */
-const doSendMessageStream = async userMessage => {
+const doSendMessageStream = async (userMessage) => {
+
   // 创建 AbortController 实例,以便中止请求
   conversationInAbortController.value = new AbortController()
   // 标记对话进行中
@@ -238,42 +244,41 @@ const doSendMessageStream = async userMessage => {
     let isFirstChunk = true // 是否是第一个 chunk 消息段
 
     await sendChatMessageStream(
-      userMessage.conversationId,
-      userMessage.content,
-      conversationInAbortController.value,
-      enableContext.value,
-      async res => {
-        const { code, data, msg } = JSON.parse(res.data)
-        if (code !== 0) {
-          console.log(`对话异常! ${msg}`)
-          return
-        }
-        // 如果内容为空,就不处理。
-        // if (data.receive.content === '') {
-        //   return
-        // }
-        receiveMessageFullText.value =
-          receiveMessageFullText.value + data.receive.content
-        // 首次返回需要添加一个 message 到页面,后面的都是更新
-        if (isFirstChunk) {
-          isFirstChunk = false
-          // 弹出两个假数据
-          activeMessageList.value.pop()
-          activeMessageList.value.pop()
-          // 更新返回的数据
-          activeMessageList.value.push(data.send)
-          activeMessageList.value.push(data.receive)
+        userMessage.conversationId,
+        userMessage.content,
+        conversationInAbortController.value,
+        enableContext.value,
+        async (res) => {
+          const { code, data, msg } = JSON.parse(res.data)
+          if (code !== 0) {
+            console.log(`对话异常! ${msg}`)
+            return
+          }
+          // 如果内容为空,就不处理。
+          // if (data.receive.content === '') {
+          //   return
+          // }
+          receiveMessageFullText.value = receiveMessageFullText.value + data.receive.content
+          // 首次返回需要添加一个 message 到页面,后面的都是更新
+          if (isFirstChunk) {
+            isFirstChunk = false
+            // 弹出两个假数据
+            activeMessageList.value.pop()
+            activeMessageList.value.pop()
+            // 更新返回的数据
+            activeMessageList.value.push(data.send)
+            activeMessageList.value.push(data.receive)
+          }
+        },
+        (error) => {
+          console.log(`对话异常! ${error}`)
+          stopStream()
+          // 需要抛出异常,禁止重试
+          throw error
+        },
+        () => {
+          stopStream()
         }
-      },
-      error => {
-        console.log(`对话异常! ${error}`)
-        stopStream()
-        // 需要抛出异常,禁止重试
-        throw error
-      },
-      () => {
-        stopStream()
-      }
     )
   } catch {}
 }
@@ -314,9 +319,9 @@ const messageList = computed(() => {
 // ============== 【消息滚动】相关 =============
 
 /** 滚动到 message 底部 */
-const scrollToBottom = async isIgnore => {
+const scrollToBottom = async (isIgnore) => {
   // if (messageRef.value) {
-  // messageRef.value.scrollToBottom(isIgnore)
+    // messageRef.value.scrollToBottom(isIgnore)
   // }
 }
 
@@ -334,9 +339,7 @@ const textRoll = async () => {
     const task = async () => {
       // 调整速度
       const diff =
-        (receiveMessageFullText.value.length -
-          receiveMessageDisplayedText.value.length) /
-        10
+          (receiveMessageFullText.value.length - receiveMessageDisplayedText.value.length) / 10
       if (diff > 5) {
         textSpeed.value = 10
       } else if (diff > 2) {
@@ -356,8 +359,7 @@ const textRoll = async () => {
         index++
 
         // 更新 message
-        const lastMessage =
-          activeMessageList.value[activeMessageList.value.length - 1]
+        const lastMessage = activeMessageList.value[activeMessageList.value.length - 1]
         lastMessage.content = receiveMessageDisplayedText.value
         // 滚动到住下面
         await scrollToBottom()
@@ -378,22 +380,24 @@ const textRoll = async () => {
   } catch {}
 }
 
+
 /** 初始化 **/
 onMounted(async () => {
-  if (personId.value) {
+  if(personId.value) {
     // 智能问答
-    CreateDialogue({ roleId: personId.value })
-      .then(res => {
-        console.log('创建会话:', res)
-        activeConversationId.value = res.data
-      })
-      .catch(error => {
-        console.error('请求出错:', error)
-      })
+    CreateDialogue({roleId: personId.value}).then(res => {
+      console.log("创建会话:",res);
+      activeConversationId.value = res.data;
+    }).catch(error => {
+      console.error('请求出错:', error);
+    });
+
     await getConversation(personId.value)
   }
+
   // 获取列表数据
   // activeMessageListLoading.value = true
+
 })
 </script>