UnisKB/ui/src/components/ai-dialog/index.vue

308 lines
8.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div class="ai-dialog p-24">
<el-scrollbar ref="scrollDiv">
<div ref="dialogScrollbar" class="ai-dialog__content">
<div class="item-content mb-16">
<div class="avatar">
<AppAvatar class="avatar-gradient">
<img src="@/assets/icon_robot.svg" style="width: 54%" alt="" />
</AppAvatar>
</div>
<div class="content">
<el-card shadow="always" class="dialog-card">
<h4>您好我是 MaxKB 智能小助手</h4>
<div class="mt-4" v-if="data?.prologue">
<el-text type="info">{{ data?.prologue }}</el-text>
</div>
</el-card>
<el-card shadow="always" class="dialog-card mt-12" v-if="data?.example?.length > 0">
<h4 class="mb-8">您可以尝试输入以下问题:</h4>
<el-space wrap>
<template v-for="(item, index) in data?.example" :key="index">
<div
@click="quickProblemHandel(item)"
class="problem-button cursor ellipsis-2"
v-if="item"
>
<el-icon><EditPen /></el-icon>
{{ item }}
</div>
</template>
</el-space>
</el-card>
</div>
</div>
<template v-for="(item, index) in chatList" :key="index">
<!-- -->
<div class="item-content mb-16 lighter">
<div class="avatar">
<AppAvatar>
<img src="@/assets/user-icon.svg" style="width: 54%" alt="" />
</AppAvatar>
</div>
<div class="content">
<div class="text">
{{ item.problem_text }}
</div>
</div>
</div>
<!-- 回答 -->
<div class="item-content mb-16 lighter">
<div class="avatar">
<AppAvatar class="avatar-gradient">
<img src="@/assets/icon_robot.svg" style="width: 54%" alt="" />
</AppAvatar>
</div>
<div class="content">
<div class="flex" v-if="!item.answer_text">
<el-card shadow="always" class="dialog-card"> 回答中... </el-card>
</div>
<el-card v-else shadow="always" class="dialog-card">
<MarkdownRenderer :source="item.answer_text"></MarkdownRenderer>
</el-card>
<el-button type="primary" link class="mt-8">停止回答</el-button>
</div>
</div>
</template>
</div>
</el-scrollbar>
<div class="ai-dialog__operate p-24">
<div class="operate-textarea flex">
<el-input
v-model="inputValue"
type="textarea"
placeholder="请输入"
:autosize="{ minRows: 1, maxRows: 8 }"
@keydown.enter="sendChatHandle($event)"
:disabled="loading"
/>
<div class="operate" v-loading="loading">
<el-button text class="sent-button" :disabled="isDisabledChart" @click="sendChatHandle">
<img v-show="isDisabledChart" src="@/assets/icon_send.svg" alt="" />
<img v-show="!isDisabledChart" src="@/assets/icon_send_colorful.svg" alt="" />
</el-button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, nextTick, onUpdated, computed } from 'vue'
import applicationApi from '@/api/application'
import { ChatManage, type chatType } from '@/api/type/application'
import { randomId } from '@/utils/utils'
import MarkdownRenderer from '@/components/markdown-renderer/index.vue'
const props = defineProps({
data: {
type: Object,
default: () => {}
}
})
const scrollDiv = ref()
const dialogScrollbar = ref()
const loading = ref(false)
const inputValue = ref('')
const chartOpenId = ref('')
const chatList = ref<chatType[]>([])
const isDisabledChart = computed(
() => !(inputValue.value && props.data?.name && props.data?.model_id)
)
function quickProblemHandel(val: string) {
inputValue.value = val
}
function sendChatHandle(event: any) {
if (!event.ctrlKey) {
// 如果没有按下组合键ctrl则会阻止默认事件
event.preventDefault()
if (!isDisabledChart.value) {
chatMessage()
}
} else {
// 如果同时按下ctrl+回车键,则会换行
inputValue.value += '\n'
}
}
/**
* 对话
*/
function getChartOpenId() {
loading.value = true
const obj = {
model_id: props.data.model_id,
dataset_id_list: props.data.dataset_id_list,
multiple_rounds_dialogue: props.data.multiple_rounds_dialogue
}
applicationApi
.postChatOpen(obj)
.then((res) => {
chartOpenId.value = res.data
chatMessage()
})
.catch(() => {
loading.value = false
})
}
function chatMessage() {
loading.value = true
if (!chartOpenId.value) {
getChartOpenId()
} else {
const problem_text = inputValue.value
applicationApi.postChatMessage(chartOpenId.value, problem_text).then(async (response) => {
const id = randomId()
chatList.value.push({
id: id,
problem_text: problem_text,
answer_text: '',
buffer: []
})
inputValue.value = ''
const row = chatList.value.find((item) => item.id === id)
if (row) {
const chatMange = new ChatManage(row, 50, loading)
chatMange.write()
const reader = response.body.getReader()
while (true) {
const { done, value } = await reader.read()
if (done) {
chatMange.close()
break
}
try {
const decoder = new TextDecoder('utf-8')
const str = decoder.decode(value, { stream: true })
if (str && str.startsWith('data:')) {
// console.log(JSON?.parse(str.replace('data:', '')))
const content = JSON?.parse(str.replace('data:', ''))?.content
if (content) {
chatMange.append(content)
}
}
} catch (e) {}
}
}
})
}
}
// 滚动到底部
function handleScrollBottom() {
nextTick(() => {
scrollDiv.value.setScrollTop(dialogScrollbar.value.scrollHeight)
})
}
onUpdated(() => {
handleScrollBottom()
})
</script>
<style lang="scss" scoped>
.ai-dialog {
--padding-left: 40px;
height: 100%;
display: flex;
flex-direction: column;
box-sizing: border-box;
position: relative;
padding-right: 20px;
padding-top: 0;
color: var(--app-text-color);
&__content {
width: 99%;
padding-bottom: 96px;
.avatar {
float: left;
}
.content {
padding-left: var(--padding-left);
}
.text {
word-break: break-all;
padding: 6px 0;
}
.problem-button {
width: 100%;
border: none;
border-radius: 8px;
background: var(--app-layout-bg-color);
height: 46px;
padding: 0 12px;
line-height: 46px;
box-sizing: border-box;
color: var(--el-text-color-regular);
-webkit-line-clamp: 1;
word-break: break-all;
&:hover {
background: var(--el-color-primary-light-9);
}
:deep(.el-icon) {
color: var(--el-color-primary);
}
}
}
&__operate {
background: #f3f7f9;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
box-sizing: border-box;
z-index: 10;
padding-top: 16px;
&:before {
background: linear-gradient(0deg, #f3f7f9 0%, rgba(243, 247, 249, 0) 100%);
content: '';
position: absolute;
width: 100%;
top: -16px;
left: 0;
height: 16px;
}
.operate-textarea {
box-shadow: 0px 6px 24px 0px rgba(31, 35, 41, 0.08);
background-color: #ffffff;
border-radius: 8px;
border: 1px solid #ffffff;
box-sizing: border-box;
&:has(.el-textarea__inner:focus) {
border: 1px solid var(--el-color-primary);
}
:deep(.el-textarea__inner) {
border-radius: 8px !important;
box-shadow: none;
resize: none;
padding: 12px 16px;
}
.operate {
padding: 6px 10px;
.sent-button {
max-height: none;
.el-icon {
font-size: 24px;
}
}
:deep(.el-loading-spinner) {
margin-top: -15px;
.circular {
width: 31px;
height: 31px;
}
}
}
}
}
.dialog-card {
border: none;
}
}
</style>