|
@@ -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>
|