聊天机器人

This commit is contained in:
lyc 2024-09-10 17:07:00 +08:00
parent 3a1cf6224a
commit 0be3d131e2
4 changed files with 327 additions and 23 deletions

View File

@ -0,0 +1,20 @@
import request from '@/utils/request'
// 创建对话
export const createChart = ({ headers, data }) => {
return request({
url: '/qf/createChart',
method: 'post',
headers,
data,
})
}
// 大模型对话
export const sendChart = ({ headers, data }) => {
return request({
url: '/qf/sendTalk',
method: 'post',
headers,
data,
})
}

View File

@ -0,0 +1,47 @@
<template>
<div>
{{ displayedText }}
</div>
</template>
<script setup>
import { ref, onMounted, nextTick } from 'vue'
const props = defineProps({
text: {
type: String,
required: true,
},
speed: {
type: Number,
default: 50, // 100
},
})
const emit = defineEmits(['loaded'])
const displayedText = ref('')
const startTyping = async () =>{
if(!props.text || props.text.length == 0) return
for (let i = 0; i < props.text.length; i++) {
displayedText.value += props.text.charAt(i);
if(displayedText.value.length == props.text.length -1){
emit('onSuccess')
}
emit('loaded')
await wait(props.speed);
nextTick();
}
}
const wait = (ms) =>{
return new Promise(resolve => setTimeout(resolve, ms));
}
onMounted(() =>{
startTyping()
})
</script>
<style lang="scss" scoped>
</style>

View File

@ -3,7 +3,7 @@
<svg class="icon ai-icon" aria-hidden="true" @click="isOpen = true; isMax = false" v-if="!isOpen"> <svg class="icon ai-icon" aria-hidden="true" @click="isOpen = true; isMax = false" v-if="!isOpen">
<use xlink:href="#icon-aijiqiren"></use> <use xlink:href="#icon-aijiqiren"></use>
</svg> </svg>
<el-card v-drag v-else shadow="always" class="chart-card" :class="[isMax ? 'card-max' : '']"> <el-card v-else shadow="always" class="chart-card" :class="[isMax ? 'card-max' : '']">
<template #header> <template #header>
<div class="flex chart-header"> <div class="flex chart-header">
<div class="header-name flex"> <div class="header-name flex">
@ -14,46 +14,101 @@
</div> </div>
<div class="header-tool"> <div class="header-tool">
<i class="iconfont icon-window-max_line icon-tool" @click="isMax = !isMax"></i> <i class="iconfont icon-window-max_line icon-tool" @click="isMax = !isMax"></i>
<i class="iconfont icon-close icon-tool" @click="isOpen = !isOpen"></i> <i class="iconfont icon-close icon-tool" @click="closeChart"></i>
</div> </div>
</div> </div>
</template> </template>
<el-scrollbar height="400px"> <div class="chart-body">
<div class="default-chart flex"> <el-scrollbar ref="chatref">
<div>你好{{ userStore.nickName }}</div> <div class="default-chart flex">
<div>我是AIx教学助手我可以帮助你</div> <div>你好{{ userStore.nickName }}</div>
<div class="outer-ai flex"> <div>我是AIx教学助手我可以帮助你</div>
<ul class="ai-ul"> <div class="outer-ai flex">
<li class="flex ai-li" v-for="item in outerAi" :key="item.id" @click="onClick(item)" <ul class="ai-ul">
:class="item.disabled ? 'li-disabled' : ''"> <li class="flex ai-li" v-for="item in outerAi" :key="item.id" @click="onClick(item)"
<el-image class="ai-img" :src="item.img" /> :class="item.disabled ? 'li-disabled' : ''">
<div class="ai-name flex"> <el-image class="ai-img" :src="item.img" />
<span class="title">{{ item.title }}</span> <div class="ai-name flex">
<span>{{ item.secondTit }}</span> <span class="title">{{ item.title }}</span>
</div> <span>{{ item.secondTit }}</span>
</li> </div>
</ul> </li>
</ul>
</div>
</div> </div>
</div> <!--对话-->
<!--对话--> <div class="chart-content" ref="innerRef">
<div class=""></div> <div v-for="item in msgList" :key="item.timestamp">
</el-scrollbar> <div class="author-con" v-if="item.name == 'user'">
<div class="author-msg">
{{ item.content }}
</div>
</div>
<div v-else class="robot-msg">
<Answer :text="item.content ? item.content : ''" @loaded="chartLoaded" @onSuccess="isFinally = true"/>
</div>
</div>
<div v-if="loaded" class="chart-loading">
<div></div>
<div></div>
<div></div>
</div>
</div>
</el-scrollbar>
</div>
<div class="chart-input">
<el-input v-model="msgVal" size="large" class="chart-ipt" @keyup.enter="sendMsg" @focus="isFocus = true" @blur="isFocus = false" />
<i class="iconfont icon-tujing" :class="[isFocus ? 'icon-focus' : '']" @click="sendMsg"></i>
</div>
</el-card> </el-card>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref } from 'vue' import { ref, onMounted } from 'vue'
import useUserStore from '@/store/modules/user' import useUserStore from '@/store/modules/user'
import outLink from '@/utils/linkConfig' import outLink from '@/utils/linkConfig'
import Answer from './container/text.vue'
import { createChart, sendChart } from '@/api/ai/index'
import vDrag from '@/views/tool/directive/drag' import vDrag from '@/views/tool/directive/drag'
const { ipcRenderer } = window.electron || {} const { ipcRenderer } = window.electron || {}
const userStore = useUserStore().user const userStore = useUserStore().user
/**
* 大模型相关
* conversation_id 会话ID 用于对话
* headers 请求头
* app_id 大模型 应用id
* loaded : 回答状态
* msgList : 消息列表
* curAnswer : 当前回答的文字答案
*/
let conversation_id = null
const app_id = '712ff0df-ed6b-470f-bf87-8cfbaf757be5'
const headers = {
isToken: true,
Authorization: 'Bearer bce-v3/ALTAK-VpMGiUjWehcHSPZGjUKwB/c97384c2c71a0d3b1d1060d7f9e2a4eac3343732',
'Content-Type': 'application/json;charset=utf-8',
}
const loaded = ref(false)
const msgList = ref([])
const chatref = ref(null)
const innerRef = ref(null)
const isFinally = ref(true)
//
const isMax = ref(false) const isMax = ref(false)
const isOpen = ref(true) const isOpen = ref(false)
//
const msgVal = ref('')
//
const isFocus = ref(false)
const outerAi = [ const outerAi = [
{ {
@ -86,6 +141,7 @@ const outerAi = [
} }
] ]
// web
const onClick = ({ path, disabled }) => { const onClick = ({ path, disabled }) => {
if (disabled) return if (disabled) return
let configObj = outLink().getBaseData() let configObj = outLink().getBaseData()
@ -98,6 +154,61 @@ const onClick = ({ path, disabled }) => {
cookieData: { ...configObj.data } cookieData: { ...configObj.data }
}) })
} }
//
const getChartId = () =>{
const data = { app_id }
createChart({ data, headers }).then( res =>{
if( res.code == 200){
localStorage.setItem("conversation_id", res.data.conversation_id);
conversation_id = res.data.conversation_id;
}
})
}
//
const sendMsg = () =>{
//
if(msgVal.value == '') return
if(!isFinally.value) return
isFinally.value = false
let msg = msgVal.value
msgVal.value = ''
msgList.value.push({ name: 'user', timestamp: + new Date(), content: msg})
chatref.value.setScrollTop(innerRef.value.clientHeight)
loaded.value = true
const data = {
appId: app_id,
content: msg,
stream: false,
conversationId: conversation_id,
}
sendChart({ data, headers }).then(res =>{
loaded.value = false
msgList.value.push({name: 'robot', timestamp: + new Date(), content: res.data.answer})
})
}
const chartLoaded = () =>{
chatref.value.setScrollTop(innerRef.value.clientHeight)
}
const closeChart = () =>{
loaded.value = false
msgList.value = []
isOpen.value = false
}
onMounted(() => {
conversation_id = localStorage.getItem("conversation_id");
if (!conversation_id) {
getChartId();
}
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -126,6 +237,15 @@ const onClick = ({ path, disabled }) => {
:deep(.el-card__header) { :deep(.el-card__header) {
padding: 10px; padding: 10px;
} }
:deep(.el-card__body) {
height: calc(100% - 50px);
padding: 10px 15px;
}
.chart-body {
height: calc(100% - 60px);
}
} }
.card-max { .card-max {
@ -209,5 +329,121 @@ const onClick = ({ path, disabled }) => {
} }
} }
} }
.chart-content {
font-size: 14px;
margin-top: 20px;
.author-con {
@include flex-direction-c;
align-items: flex-end;
}
.author-msg {
display: inline-block;
background-color: #E0DFFF;
padding: 5px 8px;
border-radius: 7px;
}
.robot-msg {
background-color: #fff;
text-align: left;
}
}
.chart-input {
position: relative;
display: flex;
align-items: center;
padding-top: 15px;
.icon-tujing {
font-size: 26px;
position: absolute;
right: 8px;
color: #c0c0c2;
cursor: pointer;
}
.icon-focus {
color: #409EFF;
}
}
.chart-ipt {
:deep(.el-input__wrapper) {
padding-right: 40px;
}
}
}
.chart-loading,
.chart-loading>div {
position: relative;
box-sizing: border-box;
}
.chart-loading {
display: block;
font-size: 0;
color: #66b1ff;
}
.chart-loading.la-dark {
color: #66b1ff;
}
.chart-loading>div {
display: inline-block;
float: none;
background-color: currentColor;
border: 0 solid currentColor;
}
.chart-loading {
width: 54px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
}
.chart-loading>div:nth-child(1) {
animation-delay: -200ms;
}
.chart-loading>div:nth-child(2) {
animation-delay: -100ms;
}
.chart-loading>div:nth-child(3) {
animation-delay: 0ms;
}
.chart-loading>div {
width: 8px;
height: 8px;
border-radius: 100%;
margin-right: 4px;
animation: ball-pulse 1s ease infinite;
}
@keyframes ball-pulse {
0%,
60%,
100% {
opacity: 1;
transform: scale(1);
}
30% {
opacity: 0.1;
transform: scale(0.01);
}
} }
</style> </style>

View File

@ -254,6 +254,7 @@ onMounted(async ()=>{
.iconfont{ .iconfont{
font-size: 28px; font-size: 28px;
color: #707070; color: #707070;
font-weight: bold;
} }
&:hover{ &:hover{
color: #409EFF; color: #409EFF;