<template>
<div class="chatInfor">
<div class="chatInfor-content">
<el-scrollbar height="97%" id="chatBox" ref="scrollbarRef" v-loading="loading" width="928px"
element-loading-text="数据加载中。。。" :element-loading-spinner="svg"
element-loading-svg-view-box="-10, -10, 50, 50" element-loading-background="rgba(122, 122, 122, 0)">
<div class="chatInfor-content-item" v-for="(item, index) in chatList" :key="index">
<div class="chatInfor-content-item-time" v-if="item.role == 'user'">{{ item.created_at }}</div>
<div class="chatInfor-content-item-users" v-if="item.role == 'user'">
<p>
<div v-for="(items, index) in item.content">
<div v-if="items.type == 'text'"> {{ items.text }}</div>
<div style="display: flex;margin-left: -16px;" v-show="items.imgList">
<div v-for="(itemimage, index) in items.imgList"
style="margin-left:16px;margin-top:8px">
<el-image :src="itemimage" :zoom-rate="1.2" :max-scale="7"
class="chatInfor-input-button-imglist-item-img" :min-scale="0.2"
:preview-src-list="items.imgList" :initial-index="0" fit="cover"
style="height:72px;" />
</div>
</div>
<!-- <img :src="items.text" v-if="items.imgList.length > 0" /> -->
</div>
</p>
<img :src="icon ? proxy.$loginUrl + icon : hqdInforImg">
<!-- <img src="@/assets/function/chat/person-image.png" /> -->
</div>
<div class="chatInfor-content-item-chats" v-if="item.role == 'assistant' && item.progress == false">
<img src="@/assets/function/chat/hqd-image.png" />
<p v-if="!item.content[0].text">
<div class="loader">
<div class="loader__circle"></div>
<div class="loader__circle"></div>
<div class="loader__circle"></div>
<div class="loader__circle"></div>
<div class="loader__circle"></div>
</div>
</p>
<p v-else>
<span v-html="md.render(item.content[0].text)" :id="`id${index}`"></span>
<div class="chatInfor-content-item-chats-button" v-if="item.isRecepting == false">
<div class="ebutton" @click="reBuild(index)">
<SvgIcon name="chat-reset" class="chat-btn" />
<span>重新生成</span>
</div>
<div class="ebutton" @click="tranlate(item, index)">
<SvgIcon name="chat-trans" class="chat-btn" />
<span>翻译</span>
</div>
<div class="ebutton" @click="onCopy(item)">
<SvgIcon name="chat-copy" class="chat-btn" />
<span>复制</span>
</div>
</div>
</p>
<!-- <div>111</div> -->
</div>
<div class="chatInfor-content-item-chats" v-if="item.role == 'assistant' && item.progress == true">
<img src="@/assets/function/chat/hqd-image.png" />
<p v-if="!respContent">
<div class="loader">
<div class="loader__circle"></div>
<div class="loader__circle"></div>
<div class="loader__circle"></div>
<div class="loader__circle"></div>
<div class="loader__circle"></div>
</div>
</p>
<p v-html="md.render(respContent)" v-if="respContent"></p>
</div>
</div>
</el-scrollbar>
</div>
<div class="chatInfor-input">
<el-input type="textarea" class="el-text" v-model="infor" resize="none" ref="textInput"
@keyup.ctrl.enter.native="lineBreak" @keydown.enter.exact.native="sendMessage" autosize
placeholder="请输入你想要咨询的内容">
</el-input>
<div class="chatInfor-input-button">
<div class="chatInfor-input-button-imglist">
<div class="chatInfor-input-button-imglist-item" v-for="(item, index) in imgList" :key="index">
<el-image :src="item" :zoom-rate="1.2" :max-scale="7"
class="chatInfor-input-button-imglist-item-img" :min-scale="0.2" :preview-src-list="imgList"
:initial-index="0" fit="cover" style="height:32px;" />
<SvgIcon name="chat-close" class="chat-close" @click="deleteImage(item, index)" />
<!-- <img :src="proxy.$loginUrl + item" class="chatInfor-input-button-imglist-item-img"> -->
</div>
</div>
<div style="display: flex;align-items: center;justify-content: center;">
<el-upload v-model:file-list="fileList" class="upload-demo" :http-request="handleImageUpload"
:before-upload="beforeUpload" :on-remove="handleRemove" list-type="picture"
:show-file-list="false">
<SvgIcon name="chat-upload" class="chat-robot" v-if="!isRecepting" />
</el-upload>
<SvgIcon name="chat-send" class="chat-robots" @click="sendMessage" v-if="!isRecepting" />
</div>
<div class="loader-3" v-if="isRecepting">
<div class="circle"></div>
<div class="circle"></div>
<div class="circle"></div>
<div class="circle"></div>
<div class="circle"></div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import moment from "moment";
import { ref, getCurrentInstance, nextTick,onActivated } from 'vue'
const { proxy } = getCurrentInstance() as any;
import SvgIcon from '@/components/index.vue';
import { getChatDetails, uploadFile } from '@/api/chat'
import MarkdownIt from 'markdown-it'
import { ElScrollbar, ElMessage } from 'element-plus'
import type { UploadProps, UploadUserFile } from 'element-plus'
import useClipboard from 'vue-clipboard3';
import hqdInforImg from "@/assets/function/chat/person-image.png";
const md = new MarkdownIt()
const loading = ref(false);
const { toClipboard } = useClipboard()
const disabled = ref(false);
const fileList = ref<UploadUserFile[]>([]);
const chatId = ref('');
const imgList: any = ref([]);
const imgUrl: any = ref([]);
const respContent = ref('');
const sendfileType = ref(1);
const icon = localStorage.getItem('icon');
onActivated(()=>{
scrollToBottom()
})
const textInput = ref();
const isRecepting = ref(false); //判断是否已完成发送
const infor = ref('')
const svg = `
<path class="path" d="
M 30 15
L 28 17
M 25.61 25.61
A 15 15, 0, 0, 1, 15 30
A 15 15, 0, 1, 1, 27.99 7.5
L 15 15
" style="stroke-width: 4px; fill: rgba(0, 0, 0, 0)"/>
`
const chatList = ref<any>([])
const currentPost = ref('')
const scrollbarRef = ref<InstanceType<typeof ElScrollbar>>()
const getData = async (val: any) => {
loading.value = true;
chatId.value = val;
const res: any = await getChatDetails({ s_id: val, s_size: 99 });
if (res.status == 1) {
console.log(res.json_data.data_list)
res.json_data.data_list.map((item: any) => {
item.progress = false;
item.isRecepting = false;
if (item.role == 'user') {
item.content[0].imgList = [];
item.content.map((items: any) => {
if (items.type == 'image_url') {
item.content[0].imgList.push(items.text)
}
})
}
})
chatList.value = res.json_data.data_list;
console.log(chatList.value)
nextTick(() => {
scrollToBottom()
loading.value = false;
})
} else {
ElMessage({
message: '系统好像出了点问题呢',
type: 'error',
})
loading.value = false;
}
}
//处理换行逻辑,ctrl+enter换行,enter发送消息
const lineBreak = () => {
const textarea = textInput.value.textarea;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const value = textarea.value;
const newValue = value.substring(0, start) + "\n" + value.substring(end);
textarea.value = newValue;
textarea.selectionStart = textarea.selectionEnd = start + 1;
}
//滚动条置底
const scrollToBottom = () => {
const element: any = document.getElementById("chatBox");
scrollbarRef.value!.setScrollTop(element.clientHeight)
};
//发送消息
const sendMessage = (e: any) => {
e.preventDefault();
openWebSocketInfor();
}
//翻译
const tranlate = (val: any, vals: any) => {
isRecepting.value = true;
val.isRecepting = true;
const element: any = document.getElementById(`id${vals}`);
const text = element.innerText;
let ws = new WebSocket(`wss://chat.hqdoa.com/ws/knowledge/?s_id=${chatId.value
}&s_type=1&s_chat=${text.replace(/#/g, "")}&s_other=`);
val.content[0].text = "";
element.scrollIntoView();
ws.onmessage = e => {
const response = JSON.parse(e.data);
if (response.message === "success") {
const data = response.data;
if (data.is_stream) {
// 开始接收信息
val.content[0].text += data.text_stream;
isRecepting.value = true;
} else {
// val.content[0].text = data.text_stream;
// resetData()
val.isRecepting = false;
val.progress = false;
isRecepting.value = false;
disabled.value = false;
}
}
};
ws.onerror = handleError;
}
const updateChatList = (val1: any, val2: any) => {
// if(val1)
// const websocketurl = `wss://chat.hqdoa.com/ws/knowledge/?s_id=${chatId.value}&s_type=0&s_chat=${infor.value}&s_other=`;
let contentList: any = [];
contentList = val2;
console.log(val2);
// const contentList = [{ text: infor.value, type: 'text' }];
chatList.value.push({
role: 'user',
created_at: moment(new Date()).format("YYYY-MM-DD HH:mm:ss"),
role_info: { img: localStorage.getItem("icon") },
content: contentList,
})
}
const openWebSocketInfor = () => {
console.log(infor)
if (isRecepting.value == true) {
ElMessage({
message: '请等待回答结束后再发起新的提问哦',
type: 'warning',
})
} else {
let websocketurl = `wss://chat.hqdoa.com/ws/knowledge/?s_id=${chatId.value}&s_type=0`;
respContent.value = '';
console.log(imgList.value)
if (infor.value == '' && imgList.value.length == 0) {
ElMessage({
message: '警告,发送的信息不能为空',
type: 'warning',
})
} else {
let contentList = [];
if (sendfileType.value == 2) {
} else {
if (imgList.value.length) {
// contentList = imgList.value.map((item: any) => {
// return { text: item, type: "text", imgList: }
// })
contentList.push({ text: infor.value, type: 'text', imgList: imgList.value })
} else {
contentList.push({ text: infor.value, type: 'text' });
}
updateChatList("text", contentList);
websocketurl += `&s_chat=${infor.value}&s_other=${imgUrl.value.join(',')}`;
}
nextTick(() => {
scrollToBottom()
// loading.value = false;
})
sendMsgEvent(websocketurl)
}
}
}
const sendMsgEvent = (val: any) => {
isRecepting.value = true;
respContent.value = '';
let ws = new WebSocket(val);
const currentItem = {
isRecepting: true,
role: "assistant",
created_at: moment(new Date()).format("YYYY-MM-DD HH:mm:ss"),
content: [{ text: "", annotations: [], type: "text" }],
progress: true,
};
chatList.value.push(currentItem)
// const count = 1;
ws.onopen = () => {
console.log("连接成功");
};
ws.onmessage = e => {
const response = JSON.parse(e.data);
if (response.message == 'success') {
const data = response.data;
if (data.is_stream) {
respContent.value += data.text_stream;
isRecepting.value = true;
infor.value = '';
} else {
console.log(currentItem)
// respContent.value = '';
currentItem.content[0].text = respContent.value;
resetData()
currentItem.isRecepting = false;
currentItem.progress = false;
isRecepting.value = false;
disabled.value = false;
}
// chatList.value.push(currentItem)
scrollToBottom()
} else {
isRecepting.value = false;
ElMessage({
message: response.message,
type: 'error',
})
respContent.value = '';
}
}
ws.onerror = handleError
}
const handleError = (e: any) => {
console.log(e);
if (e.type == 'error') {
ElMessage({
message: '系统出了点问题哦,请重新发起问题提问',
type: 'error',
})
}
isRecepting.value = false;
};
//重置输入框
const resetData = () => {
infor.value = '';
imgList.value = [];
imgUrl.value = [];
}
const handleRemove = () => {
}
//处理文件上传前的逻辑
const beforeUpload = (file: any) => {
const isJPG = ["image/jpeg", "image/png"].includes(file.type);
if (!isJPG) {
ElMessage({
message: '上传文件封面只能是JPG/png格式!',
type: 'error',
})
}
return isJPG;
};
//上传文件
const handleImageUpload = async (file: any) => {
const formData = new FormData();
formData.append("s_file", file.file);
const data: any = await uploadFile(formData);
imgUrl.value.push(data.file_path)
imgList.value.push(proxy.$loginUrl + data.file_path);
console.log(imgList.value)
}
//删除图片
const deleteImage = (val1: any, val2: any) => {
console.log(val1, val2)
imgList.value.splice(val2, 1)
imgUrl.value.splice(val2, 1)
}
//重新发送
const reBuild = (val: any) => {
console.log(1)
console.log(val)
let websocketurl = `wss://chat.hqdoa.com/ws/knowledge/?s_id=${chatId.value}&s_type=0`;
if (isRecepting.value) {
ElMessage({
message: '请等待回答结束后再发起新的提问哦',
type: 'warning',
})
return false;
}
// let resetItem = '';
let resetItem = findIndexAsk(val);
chatList.value.push(resetItem);
console.log(resetItem)
// console.log(resetItem, '????')
let imgList: any = [];
const chaTtext = resetItem.content[0].text;
if (resetItem.content[0].imgList) {
const arr = resetItem.content[0].imgList.map(getUrlAfterStatic)
imgList.value = arr;
websocketurl += `&s_chat=${chaTtext}&s_other=${imgList.value.join(",")}`;
} else {
websocketurl += `&s_chat=${chaTtext}&s_other=`;
}
sendMsgEvent(websocketurl);
nextTick(() => {
scrollToBottom()
// loading.value = false;
})
}
//截取字符
const getUrlAfterStatic = (url: string) => {
const startIndex = url.indexOf('/static');
if (startIndex === -1) {
return null; // 或者抛出一个错误,表示没有找到 /static
}
return url.substring(startIndex);
}
//根据Index查找提问的问题
const findIndexAsk = (index: any) => {
let resetItem;
const currentItem = chatList.value[index];
scrollToBottom();
if (currentItem.role !== "user") {
for (let i = index; i > -1; i--) {
const item = chatList.value[i];
if (item.role === "user") {
resetItem = item;
break;
}
}
console.log(resetItem, 'gggg')
} else {
resetItem = currentItem;
}
return resetItem
}
//复制功能(注:复制内容只能是String类型)
const onCopy = async (msg: any) => {
console.log(msg.content[0])
try {
// 复制
await toClipboard(msg.content[0].text)
ElMessage({
message: '复制成功',
type: 'success',
})
console.log(1)
// 复制成功
} catch (e) {
// 复制失败
}
}
defineExpose({
getData
})
</script>
<style lang="scss">
.el-text {
width: 100%;
max-width: 928px;
// min-width: 800px;
// min-height: 24px;
// margin-left: 64px;
border: none !important;
--el-input-border-color: none !important;
--el-input-focus-border: none !important;
--el-input-border: none !important;
--el-input-focus-border-color: none !important;
--el-input-hover-border-color: none !important;
--el-input-clear-hover-color: none !important;
border-radius: 0px 0px 8px 8px;
.el-textarea__inner {
// height: auto !important;
// min-height: 24px !important;
font-size: 16px;
padding: 16px 16px 16px 16px;
min-height: 48px !important;
max-height: 80px !important;
line-height: 24px;
// line-height: 56px !important;
border: none !important;
border-radius: 8px 8px 0px 0px;
// border-radius: 8px;
}
}
</style>
<style lang="scss" scoped>
.upload-demo {
display: flex;
justify-content: center;
align-items: center
}
.chatInfor {
max-width: 1048px;
position: absolute;
left: 50%;
transform: translateX(-50%);
// min-width: 920px;
overflow: auto;
position: relative;
// padding: 0px 14%;
height: 100%;
text-align: left;
&-content {
height: 85%;
max-width: 1048px;
// overflow-y: scroll;
width: 100%;
&-item {
height: 100%;
width: 100%;
padding: 0px 60px;
font-size: 15px;
line-height: 24px;
letter-spacing: .5px;
&-time {
color: #A5A5A5;
font-size: 12px;
line-height: 20px;
;
text-align: center;
margin-top: 32px;
}
&-users {
margin-top: 16px;
display: flex;
justify-content: right;
width: 100%;
// align-items: right;
img {
width: 48px;
height: 48px;
border-radius: 100px;
}
//
p {
width: fit-content;
background-color: var(--vt-c-green);
padding: 12px 16px;
border-radius: 24px 0px 24px 24px;
color: var(--vt-c-selected);
margin-right: 16px;
max-width: calc(100% - 128px);
img {
height: 72px;
width: 100%;
}
}
}
&-chats {
margin-top: 16px;
display: flex;
justify-content: left;
position: relative;
img {
width: 48px;
height: 48px;
}
p {
margin-left: 16px;
width: 100%;
background-color: var(--vt-c-white);
padding: 12px 16px;
max-width: calc(100% - 128px);
position: relative;
border-radius: 0px 16px 16px 16px;
color: var(--vt-c-selected);
overflow-x: auto;
}
&-button {
float: right;
// position: absolute;
margin-top: 16px;
// right: 0px;
margin-right: 5px;
height: 16px;
// margin-bottom:02px;
display: flex;
align-items: center;
cursor: pointer;
.ebutton {
display: flex;
align-items: center;
color: #4E5969;
.chat-btn {
width: 20px;
height: 20px;
margin-left: 24px;
}
span {
margin-left: 4px;
font-size: 14px;
}
}
.ebutton:hover {
color: #85E822;
}
}
}
}
}
.active {
background-color: var(--el-fill-color-light);
&-input {
&-button {
background-color: var(--el-fill-color-light);
}
}
}
&-input {
// height: 85%;
// max-width: 928px;
// min-width: 928px;
// min-width: 800px;
position: relative;
width: calc(100% - 248px);
margin-left: 124px;
// border-radius: 8px;
&-button {
// position: absolute;
// right: 75px;
// bottom: 10px;
width: 100%;
max-width: 928px;
// min-width: 800px;
height: 36px;
z-index: 100;
// background-color: #fff;
display: flex;
align-items: center;
// position: absolute;
// flex-direction: row-reverse;
justify-content: space-between;
// text-align: right;
// margin-left: 64px;
background-color: #fff;
border-radius: 0px 0px 8px 8px;
&-imglist {
width: 80%;
height: 100%;
display: flex;
align-items: center;
&-item {
margin-left: 16px;
position: relative;
// width:12px;
img {
height: 32px;
width: auto;
}
}
}
// flex-direction: row-reverse;
.chat-robots {
width: 24px;
height: 24px;
margin-right: 16px;
cursor: pointer;
}
.chat-robot {
width: 20px;
height: 20px;
margin-right: 16px;
cursor: pointer;
}
.chat-close {
position: absolute;
width: 14px;
height: 14px;
right: -7px;
top: -5px;
cursor: pointer;
}
}
}
&-inputs {
width: 800px;
}
// &-input:hover {
// // margin-left:64PX;
// border: 1px solid var(--el-color-primary);
// max-width: 928px;
// border-radius: 8px;
// min-width: 800px;
// position: relative;
// width: calc(100% - 128px);
// margin-left: 64px;
// }
// overflow-x: auto;
// overflow-y: scroll;
}
/* From Uiverse.io by G4b413l */
/* From Uiverse.io by mobinkakei */
.loader-3 {
width: 9em;
display: flex;
justify-content: space-evenly;
}
.circle {
width: 8px;
height: 8px;
border-radius: 50%;
position: relative;
}
.circle:nth-child(1) {
background-color: #90be6d;
}
.circle:nth-child(2) {
background-color: #f9c74f;
}
.circle:nth-child(3) {
background-color: #f8961e;
}
.circle:nth-child(4) {
background-color: #f3722c;
}
.circle:nth-child(5) {
background-color: #f94346;
}
.circle::before {
content: "";
width: 100%;
height: 100%;
position: absolute;
border-radius: 50%;
opacity: 0.5;
animation: animateLoader38 2s ease-out infinite;
}
.circle:nth-child(1)::before {
background-color: #90be6d;
}
.circle:nth-child(2)::before {
background-color: #f9c74f;
animation-delay: 0.2s;
}
.circle:nth-child(3)::before {
background-color: #f8961e;
animation-delay: 0.4s;
}
.circle:nth-child(4)::before {
background-color: #f3722c;
animation-delay: 0.6s;
}
.circle:nth-child(5)::before {
background-color: #f94346;
animation-delay: 0.8s;
}
@keyframes animateLoader38 {
0% {
transform: scale(1);
}
50%,
75% {
transform: scale(2.5);
}
80%,
100% {
opacity: 0;
}
}
.text {
color: black;
font-weight: bolder;
}
.loader {
position: relative;
display: flex;
align-items: center;
gap: 0.3em;
margin-top: 3px;
// overflow-y: auto
}
.loader::before {
content: "";
position: absolute;
left: 0;
top: 10px;
width: 100%;
// height: 2em;
filter: blur(45px);
background-color: #e299ff;
background-image: radial-gradient(at 52% 57%, hsla(11, 83%, 72%, 1) 0px, transparent 50%),
radial-gradient(at 37% 57%, hsla(175, 78%, 66%, 1) 0px, transparent 50%);
}
.loader__circle {
--size__loader: 0.6em;
width: var(--size__loader);
height: var(--size__loader);
border-radius: 50%;
animation: loader__circle__jumping 2s infinite;
background-color: #b499ff;
}
.loader__circle:nth-child(2n) {
animation-delay: 300ms;
background-color: #e499ff;
}
.loader__circle:nth-child(3n) {
animation-delay: 600ms;
}
@keyframes loader__circle__jumping {
0%,
100% {
transform: translateY(0px);
}
25% {
transform: translateY(-15px) scale(0.5);
}
50% {
transform: translateY(0px);
}
75% {
transform: translateY(5px) scale(0.9);
}
}
</style>