瀏覽代碼

对话区功能

SanHQin 23 小時之前
父節點
當前提交
736b94dbc1

+ 3 - 0
package.json

@@ -16,6 +16,7 @@
   "homepage": "https://github.com/pipipi-pikachu/PPTist",
   "dependencies": {
     "@icon-park/vue-next": "^1.4.2",
+    "@microsoft/fetch-event-source": "^2.0.1",
     "animate.css": "^4.1.1",
     "axios": "^1.7.9",
     "clipboard": "^2.0.11",
@@ -28,6 +29,7 @@
     "html2canvas": "^1.4.1",
     "katex": "^0.16.22",
     "lodash": "^4.17.21",
+    "markdown-it": "^14.1.0",
     "mitt": "^3.0.1",
     "nanoid": "^5.0.7",
     "number-precision": "^1.6.0",
@@ -64,6 +66,7 @@
     "@types/file-saver": "^2.0.7",
     "@types/html2canvas": "^1.0.0",
     "@types/lodash": "^4.14.202",
+    "@types/markdown-it": "^14.1.2",
     "@types/node": "^18.19.3",
     "@types/qs": "^6.14.0",
     "@types/svg-arc-to-cubic-bezier": "^3.2.2",

二進制
src/assets/img/ai_agent_header.png


二進制
src/assets/img/avatar.png


二進制
src/assets/img/user_header.png


+ 45 - 0
src/services/course.ts

@@ -113,6 +113,47 @@ export const selectCourseSLook = (cid: string): Promise<any> => {
   })
 }
 
+
+
+/**
+ * 
+ * 获取用户数据
+ * @param uid 用户id
+ * @returns Promise<any>
+ */
+
+export const getUser = (uid: string): Promise<any> => {
+  return axios.get(`${API_URL}getUser`, {
+    params: { uid },
+  })
+}
+
+/**
+ * 
+ * 存储对话内容
+ * @param any 用户id
+ * @returns Promise<any>
+ */
+
+export const insertChat = (params: any): Promise<any> => {
+  return axios.post(`https://gpt4.cocorobo.cn/insert_chat`, params)
+}
+
+/**
+ * 
+ * 获取对话内容
+ * @param any 用户id
+ * @returns Promise<any>
+ */
+
+export const getChatList = (params: any): Promise<any> => {
+  return axios.post(`https://gpt4.cocorobo.cn/get_agent_park_chat`, params)
+}
+
+
+
+
+
 export default {
   getCourseDetail,
   submitWork,
@@ -123,4 +164,8 @@ export default {
   updateCourseFollowC,
   selectCourseSLook,
   yweb_socket,
+  getUser,
+  insertChat,
+  getChatList
 }
+

+ 439 - 23
src/views/Student/components/DialoguePanel.vue

@@ -1,51 +1,467 @@
 <template>
-  <div class="dialogue-panel">
-    对话功能待开发
-  </div>
+	<div class="dialogue-panel">
+		<div class="dp_messageList" ref="messageListRef">
+			<div class="dp_ml_item" v-for="item in messageList" :key="item.id">
+				<div class="dp_ml_i_message right" v-if="item.userContent"><!--v-if="item.userContent"-->
+          <div class="dp_ml_i_m_msgArea">
+            <div class="dp_ml_i_m_ma_name" v-if="item.userName" v-text="item.userName"></div>
+            <div class="dp_ml_i_m_ma_textBlok">
+              <span v-html="item.userContent"></span>
+            </div>
+          </div>
+          <div class="dp_ml_i_m_avatarArea">
+            <div>
+              <img src="../../../assets/img/avatar.png" alt="">
+            </div>
+          </div>
+        </div>
+        <div class="dp_ml_i_message left" v-if="item.aiContent || item.loading"><!--v-if="item.aiContent || item.loading"-->
+          <div class="dp_ml_i_m_avatarArea">
+            <div>
+              <img src="../../../assets/img/ai_agent_header.png" alt="">
+            </div>
+          </div>
+          <div class="dp_ml_i_m_msgArea">
+            <div class="dp_ml_i_m_ma_name" v-if="item.aiName" v-text="item.aiName"></div>
+            <div class="dp_ml_i_m_ma_textBlok">
+              <span v-if="item.aiContent" v-html="item.aiContent"></span>
+              <svg v-else xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from SVG Spinners by Utkarsh Verma - https://github.com/n3r4zzurr0/svg-spinners/blob/main/LICENSE --><circle cx="4" cy="12" r="3" fill="currentColor"><animate id="svgSpinners3DotsBounce0" attributeName="cy" begin="0;svgSpinners3DotsBounce1.end+0.25s" calcMode="spline" dur="0.6s" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12"/></circle><circle cx="12" cy="12" r="3" fill="currentColor"><animate attributeName="cy" begin="svgSpinners3DotsBounce0.begin+0.1s" calcMode="spline" dur="0.6s" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12"/></circle><circle cx="20" cy="12" r="3" fill="currentColor"><animate id="svgSpinners3DotsBounce1" attributeName="cy" begin="svgSpinners3DotsBounce0.begin+0.2s" calcMode="spline" dur="0.6s" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12"/></circle></svg>
+            </div>
+          </div>
+        </div>
+			</div>
+		</div>
+		<div class="dp_inputArea">
+			<div class="dp_ia_bottom">
+				<div class="dp_ia_b_left">
+					<div>
+						<textarea
+							v-model="inputText"
+							type="text"
+							placeholder="请输入"
+							@keydown.enter.exact.prevent="sendMessage"
+						></textarea>
+					</div>
+				</div>
+				<div class="dp_ia_b_right">
+					<div
+						class="dp_ia_b_r_btn"
+						v-if="!sendMessageLoading"
+						@click.stop="sendMessage"
+					>
+						<svg
+							t="1756432184712"
+							class="icon"
+							viewBox="0 0 1024 1024"
+							version="1.1"
+							xmlns="http://www.w3.org/2000/svg"
+							p-id="7156"
+							width="200"
+							height="200"
+						>
+							<path
+								d="M211.649242 813.217191a425.984 425.984 0 1 0 602.421836-602.442865 425.984 425.984 0 1 0-602.421836 602.442865Z"
+								fill="#00A0E9"
+								p-id="7157"
+							></path>
+							<path
+								d="M266.3936 427.7248l422.5024-103.5776c20.4288-5.0176 37.5296 15.9744 28.4672 34.9696l-188.2112 395.1616c-9.728 20.3776-39.3728 18.432-46.2848-3.072l-48.0256-149.4016a25.06752 25.06752 0 0 1 5.2224-24.3712L522.1888 486.4c5.0176-5.5808-1.6896-13.9264-8.192-10.0864l-108.9024 63.5392a24.9856 24.9856 0 0 1-24.6272 0.3072L260.2496 473.8048c-19.8656-10.9568-15.9232-40.6528 6.144-46.08z"
+								fill="#FFFFFF"
+								p-id="7158"
+							></path>
+						</svg>
+					</div>
+					<div
+						class="dp_ia_b_r_btn"
+						v-if="sendMessageLoading"
+						@click.stop="stopSendMessage"
+					>
+						<svg
+							t="1756432604950"
+							class="icon"
+							viewBox="0 0 1024 1024"
+							version="1.1"
+							xmlns="http://www.w3.org/2000/svg"
+							p-id="11335"
+							width="200"
+							height="200"
+						>
+							<path
+								d="M512 960C264.57 960 64 759.43 64 512S264.57 64 512 64s448 200.57 448 448-200.57 448-448 448z m0.43-83.66c201.01 0 364.01-163.01 364.01-364.01s-163-364.02-364.01-364.02-364.01 163.01-364.01 364.01 162.9 364.02 364.01 364.02zM371.99 343.92h280.03c15.44 0 27.96 12.52 27.96 27.96v280.13c0 15.44-12.52 27.96-27.96 27.96H371.99c-15.44 0-27.96-12.52-27.96-27.96V371.99c0-15.55 12.52-28.07 27.96-28.07z"
+								p-id="11336"
+								fill="#707070"
+							></path>
+						</svg>
+					</div>
+				</div>
+			</div>
+		</div>
+	</div>
 </template>
 
 <script lang="ts" setup>
-import { ref, computed } from 'vue'
+import { ref, reactive, onMounted } from 'vue'
+import { fetchEventSource } from '@microsoft/fetch-event-source'
+import MarkdownIt from 'markdown-it'
+import api from '../../../services/course'
+
+
+
+interface Props {
+  courseid?: string | null
+  userid?: string | null
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  courseid: null,
+  userid: null,
+})
 
 interface Message {
-  id: string
-  type: 'user' | 'assistant'
-  sender: string
-  text: string
-  time: Date
+	id: string;
+	userContent: string;
+	aiContent: string;
+	createTime: string;
+	userName: string;
+	aiName: string;
+	loading: boolean;
 }
 
 // 对话消息列表
-const messages = ref<Message[]>([
+const messageList = reactive<Message[]>([
   {
     id: '1',
-    type: 'assistant',
-    sender: 'AI助手',
-    text: '你好!我是你的学习助手,有什么可以帮助你的吗?',
-    time: new Date()
-  }
+    userContent: '',
+    aiContent: '你好!我是你的学习助手,有什么可以帮助你的吗?', // 你好!我是你的学习助手,有什么可以帮助你的吗?
+    createTime: new Date().toLocaleString().replace(/\//g, '-'),
+    userName: '老师',
+    aiName: 'AI助手',
+    loading: false,
+  },
 ])
 
+const sendMessageLoading = ref<boolean>(false)
+
 // 输入框文本
 const inputText = ref('')
 
+// 消息区域Ref
+const messageListRef = ref<HTMLDivElement>()
+
+// 发送消息请求实例
+const curRequestController = ref<AbortController | null>(null)
+
+// 用户名称
+const userName = ref<string>('')
+
 // 发送消息
 const sendMessage = () => {
+  if (sendMessageLoading.value) return
+  if (!inputText.value.trim()) return
+  const userInput = inputText.value.trim()
+  inputText.value = ''
+  sendMessageLoading.value = true
+  const _uid = new Date().getTime().toString()
+  const newMessage = {
+    id: _uid,
+    userContent: userInput,
+    aiContent: '',
+    createTime: new Date().toLocaleString().replace(/\//g, '-'),
+    userName: userName.value || '老师',
+    aiName: 'AI助手',
+    loading: true,
+  }
+  messageList.push(newMessage)
+  messageListScrollBottom()
+
+  const md = new MarkdownIt()
+
+  const params = {
+    model: 'gpt-4o-2024-11-20',
+    temperature: 0,
+    max_tokens: 4096,
+    top_p: 1,
+    frequency_penalty: 0,
+    presence_penalty: 0,
+    messages: [{ role: 'user', content: userInput}],
+    uid: new Date().getTime().toString(),
+    mind_map_question: '',
+    stream: true,
+  }
+
+  let _addText = ''
+  curRequestController.value = new AbortController()
 
+  fetchEventSource(
+    'https://gpt4.cocorobo.cn/chat_post_stream',
+    {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify(params),
+      signal: curRequestController.value.signal,
+      onmessage(ev) {
+        const _data = ev.data
+        if (_data === '[DONE]') {
+          insertChat(_uid)
+          return (sendMessageLoading.value = false)
+        }
+        _addText += _data
+        const msgItem = messageList.find((item) => item.id === _uid)
+        
+        if (msgItem) {
+          msgItem.aiContent = md.render(_addText)
+          msgItem.loading = false
+        }
+        messageListScrollBottom()
+      },
+      onclose() {
+        sendMessageLoading.value = false
+        curRequestController.value = null
+        console.log('连接关闭')
+      },
+      onerror(err) {
+        console.error(err)
+        sendMessageLoading.value = false
+        const errorMsgItem = messageList.find((item) => item.id === _uid)
+        if (errorMsgItem) {
+          errorMsgItem.aiContent = '网络错误'
+        }
+      },
+    }
+  )
+
+  // 发送消息
+}
+
+const stopSendMessage = () => {
+  sendMessageLoading.value = false
+  curRequestController.value?.abort()
+  if (messageList.length > 0) {
+    insertChat(messageList[messageList.length - 1].id)
+  }
+  console.log('主动关闭连接')
 }
 
-// 格式化时间
-const formatTime = (time: Date) => {
+// 消息区域触底
+const messageListScrollBottom = () => {
+  if (messageListRef.value) messageListRef.value.scrollTop = messageListRef.value.scrollHeight
+}
+
+// 保存消息
+const insertChat = async (uid: string) => {
+  const _msg = messageList.find(i => i.id === uid)
+  if ( props.userid && props.courseid && _msg) {
+    const params = {
+      userId: props.userid,
+      userName: userName.value
+        ? userName.value
+        : await getUserName(props.userid),
+      groupId: `602def61-005d-11ee-91d8-005056b8q12w`,
+      answer: _msg.aiContent,
+      problem: _msg.userContent,
+      file_id: '',
+      alltext: _msg.aiContent,
+      type: 'chat',
+      filename: '',
+      session_name: `${props.userid}_${props.courseid}_pptCourse`,
+    }
 
+    api.insertChat(params)
+  }
 }
+
+// 获取用户名称
+const getUserName = (uid:string | null) => {
+  if (!uid) return '-'
+  return new Promise(resolve => {
+    return api.getUser(uid).then((res: any) => {
+      const data = res[0][0]
+      userName.value = data.username
+      resolve(data.username)
+    })
+  })
+}
+
+// 获取对话内容
+const getMessageList = () => {
+  if (props.courseid && props.userid) {
+    const params = {
+      userid: props.userid,
+      groupid: `602def61-005d-11ee-91d8-005056b8q12w`,
+      session_name: `${props.userid}_${props.courseid}_pptCourse`,
+    }
+    api.getChatList(params).then((res: any) => {
+      const data = JSON.parse(res.FunctionResponse).response
+      if (data && data.length > 0) {
+        data.forEach((item:any) => {
+          const oldMessage = {
+            id: item.id,
+            userContent: item.problem,
+            aiContent: item.answer,
+            createTime: item.createtime,
+            userName: userName.value || '老师',
+            aiName: 'AI助手',
+            loading: false,
+          }
+          messageList.push(oldMessage)
+        })
+      }
+    })
+  }
+}
+
+onMounted(() => {
+  getUserName(props.userid)
+  getMessageList()
+})
+
 </script>
 
 <style lang="scss" scoped>
 .dialogue-panel {
-  height: 100%;
-  display: flex;
-  flex-direction: column;
-  background: #fff;
+	width: 100%;
+	height: 100%;
+	display: flex;
+	flex-direction: column;
+	.dp_messageList {
+		width: 100%;
+		height: calc(100% - 60px - 10px);
+    overflow: auto;
+		.dp_ml_item{
+      width: 100%;
+      height: auto;
+      .dp_ml_i_message{
+        width: 100%;
+        height: auto;
+        display: flex;
+        margin-bottom: 20px;
+        .dp_ml_i_m_avatarArea{
+          width: 50px;
+          height: auto;
+          display: flex;
+          justify-content: center;
+          div{
+            width: 40px;
+            height: 40px;
+            margin-top: 10px;
+            border-radius: 50%;
+            overflow: hidden;
+            img{
+              width: 100%;
+              height: 100%;
+              object-fit: cover;
+            }
+          }
+        }
+        .dp_ml_i_m_msgArea{
+          width: calc(100% - 50px - 10px);
+          height: auto;
+          margin: 0 5px;
+          margin-top: 5px;
+          display: flex;
+          flex-direction: column;
+          .dp_ml_i_m_ma_name{
+            max-width: 100%;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+            font-size: 14px;
+            color: #333;
+            font-weight: 500;
+            margin-bottom: 5px;
+          }
+          .dp_ml_i_m_ma_textBlok{
+            width: fit-content;
+            display: block;
+            background-color: #F8F9FA;
+            padding: 10px;
+            border: solid 1px #E9ECEF;
+            border-radius: 4px;
+            &>svg{
+              width: 17px;
+              height: 17px;
+            }
+          }
+        }
+      }
+    }
+    .right{
+      justify-content: flex-end;
+      .dp_ml_i_m_msgArea{
+        align-items: flex-end;
+        .dp_ml_i_m_ma_name{
+          text-align: right;
+        }
+        .dp_ml_i_m_ma_textBlok{
+          background-color: #3681FC !important;
+          color: #fff;
+          border-color: #69A1FD !important;
+        }
+      }
+      
+    }
+	}
+	.dp_inputArea {
+		width: 100%;
+		height: 60px;
+		.dp_ia_bottom {
+			width: 100%;
+			height: 100%;
+			display: flex;
+			align-items: center;
+			.dp_ia_b_left {
+				width: calc(100% - 50px);
+				height: 100%;
+				display: flex;
+				align-items: center;
+				justify-content: center;
+				& > div {
+					width: 100%;
+					height: auto;
+					display: flex;
+					align-items: flex-end;
+					border: solid 1px #ececec;
+					border-radius: 6px;
+					box-sizing: border-box;
+					box-shadow: 0 0 10px #ececec;
+					& > textarea {
+						border: none;
+						outline: none;
+						width: 100%;
+						height: 40px;
+						background: none;
+						text-indent: 0.5em;
+						font-size: 14px;
+						resize: none;
+						box-sizing: border-box;
+						padding: 10px 0;
+						&::placeholder {
+							color: #949494;
+						}
+					}
+				}
+			}
+			.dp_ia_b_right {
+				width: 45px;
+				height: 100%;
+				display: flex;
+				align-items: center;
+				justify-content: center;
+				margin-left: 5px;
+				.dp_ia_b_r_btn {
+					width: 40px;
+					height: 40px;
+					border-radius: 50%;
+					cursor: pointer;
+					& > svg {
+						width: 40px;
+						height: 40px;
+					}
+				}
+			}
+		}
+	}
 }
-
-</style> 
+</style>

+ 1 - 1
src/views/Student/index.vue

@@ -198,7 +198,7 @@
         
         <!-- 对话区内容 -->
         <div v-show="!workPanelCollapsed && rightPanelMode === 'dialogue'" class="panel-content">
-          <DialoguePanel />
+          <DialoguePanel :userid="props.userid" :courseid="props.courseid"/>
         </div>
         
         <!-- 选择题统计内容 -->