aigc-h5/src/views/chat/components/ai-assistant.vue

389 lines
10 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="fixed w-86 h-full z-999" :style="styleObj">
<!-- <vue-draggable-resizable
:x="0"
:y="0"
:z="999"
:resizable="true"
w="auto"
h="auto"
> -->
<div class="w-86 bg-[#161718] p-3" ref="floatWindow">
<div class="flex text-white items-center justify-between">
<div class="flex items-center">
<Avatar />
<span class="font-bold text-lg ml-4">海兔AI智慧助理</span>
</div>
<SvgIcon
class="text-white text-xl cursor-pointer"
name="close"
></SvgIcon>
</div>
<div class="border-2px border-[#A6A8AF] p-4 mt-4">
<div class="grid grid-cols-3 gap-x-2.5 options">
<div
class="border-2px border-[#414548] h-6.5 text-center text-sm leading-6.5 text-white cursor-pointer"
:class="{ active: optionIndex === index }"
@click="changeOption(index)"
v-for="(item, index) in options"
:key="item.name"
>
{{ item.name }}
</div>
</div>
<div class="w-full h-2px bg-[#3662FE] my-2.5"></div>
<div class="flex justify-end">
<SvgIcon
v-if="loading"
@click="handleStop"
class="text-white text-xl ml-2 cursor-pointer"
name="pause"
></SvgIcon>
<SvgIcon class="text-white text-xl ml-2" name="right-arrow"></SvgIcon>
</div>
<div class="mt-5 h-97 -mx-4">
<ScrollContainer ref="scrollRefAi">
<div class="px-4">
<div class="text-base text-white opacity-40 leading-6">
<div class="text-center" v-if="contentLoading">
<a-spin size="small" />
</div>
<div v-else>
{{ contenText }}
</div>
<!-- 大自然是人类赖以生存发展的基本条件,尊重自然、顺应自然、保护自然是全面建设社会主义现代化国家的内在要求大自然是人类赖以生存发展的基本条件,尊重自然、顺应自然、保护自然是全面建设社会主义现代化国家是全面建设社会主义现代化国家的内在要求 -->
</div>
<Message
class="my-4"
v-for="(item, index) in dataSources"
:key="index"
:text="item.text"
:loading="item.loading"
:inversion="item.inversion"
></Message>
</div>
</ScrollContainer>
</div>
<div class="flex">
<a-input
@pressEnter="sendMessage"
v-model:value="prompt"
size="small"
class="flex-1 text-sm rounded-r-none rounded-4px bg-[#414548] bg-opacity-40 text-white placeholder-[#FFFFFF40] border-[#414548]"
placeholder="向我提问有关文本的任何问题"
></a-input>
<a-button
@click="sendMessage"
type="primary"
class="rounded-r-4px rounded-l-none px-6 h-full !w-19"
>
<template #icon>
<SvgIcon class="text-white text-xl" name="send"></SvgIcon>
</template>
</a-button>
</div>
<div class="opacity-40 text-sm text-center mt-4">
使
</div>
</div>
</div>
<!-- </vue-draggable-resizable> -->
</div>
</template>
<script setup>
import Message from './message.vue'
import Avatar from './avatar.vue'
import ScrollContainer from '@/components/ScrollContainer/index.vue'
import { ref, computed, nextTick } from 'vue'
import http from '@/io/request'
import { v4 as uuidv4 } from 'uuid'
import { useAiChat } from '@/stores/aichat'
import VueDraggableResizable from 'vue-draggable-resizable/src/components/vue-draggable-resizable.vue'
import 'vue-draggable-resizable/dist/VueDraggableResizable.css'
const props = defineProps({
top: {
type: String,
default: '140px',
},
right: {
type: String,
default: '0px',
},
content: {
type: String,
default: '',
},
})
const styleObj = computed(() => {
return {
top: props.top,
right: props.right,
}
})
const contentLoading = ref(false)
const contenText = ref('')
let controller = new AbortController()
const scrollRefAi = ref(null)
const aiChatStore = useAiChat()
const loading = ref(false)
const uuid = uuidv4()
const options = [
{
name: 'AI总结',
value: `Instructions: Summarize the highlights of the content and output a useful summary in a few sentences,usage and download address not included.
You must write the summary in Chinese (China) language.
"""
{0}
"""`,
},
{
name: 'Q&A',
value: `
Instructions: List the highlights of the content in the form of Q&As, no less than 3 Q&As. Here is an example of the template output you should use:
##### Who are you?
I am AI.
Be sure not to write out the template examples.
Please answer using Chinese (China) language.
"""
{0}
"""
`,
},
{
name: '重点内容',
value: `
Instructions: Summarize this content into a bulleted list of the most important information.
Please answer using Chinese (China) language.
"""
{0}
"""
`,
},
]
const optionIndex = ref(null)
const changeOption = (index) => {
optionIndex.value = index
contenText.value = ''
autoMessage()
}
const currentOption = computed(() => {
if(optionIndex.value === null) return null
return options[optionIndex.value] ?? null
})
const dataSources = computed(() => {
return aiChatStore.getHistoryByUuid(uuid)
})
const prompt = ref('')
const autoMessage = async ()=>{
const message = replacePlaceholder(currentOption.value.value, truncateRichText(props.content, 4000))
if(loading.value) return
contentLoading.value = true
loading.value = true
try {
const fetchChatAPIOnce = async () => {
await http.post(
'/api/v1/answer',
{
prompt: message,
},
{
signal: controller.signal,
requestBaseUrl: 'chat',
onDownloadProgress: async ({ event }) => {
contentLoading.value = false
const xhr = event.target
const { responseText } = xhr
if (xhr.status == 200) {
const arr = parseEventMessages(responseText)
const msg = arr.reduce((acc, item) => {
return acc + item.text
}, '')
contenText.value = msg
scrollToBottomIfAtBottom()
}
},
}
)
}
await fetchChatAPIOnce()
} catch (error) {
if (error.message === 'canceled') {
return
}
const errorMessage = error?.errmsg ?? '好像出错了,请稍后再试。'
contenText.value = errorMessage
}finally{
contentLoading.value = false
loading.value = false
}
}
function truncateRichText(richText, maxLength) {
// 去除标签
const plainText = richText.replace(/<[^>]+>/g, '');
// 截取最多 maxLength 个字符
const truncatedText = plainText.substring(0, maxLength);
return truncatedText;
}
function replacePlaceholder(originalText, replacement) {
return originalText.replace('{0}', replacement);
}
const sendMessage = async () => {
const message = prompt.value
if (loading.value) return
if (!message || message.trim() === '') {
return
}
aiChatStore.addHistory(uuid, {
text: message,
inversion: true,
loading: false,
})
scrollToBottom()
aiChatStore.addHistory(uuid, {
text: '⋯',
inversion: false,
loading: true,
})
prompt.value = ''
loading.value = true
scrollToBottom()
try {
const fetchChatAPIOnce = async () => {
await http.post(
'/api/v1/answer',
{
prompt: message,
},
{
signal: controller.signal,
requestBaseUrl: 'chat',
onDownloadProgress: async ({ event }) => {
const xhr = event.target
const { responseText } = xhr
if (xhr.status == 200) {
const arr = parseEventMessages(responseText)
const { conversation_id, message_id } = arr[0]
const msg = arr.reduce((acc, item) => {
return acc + item.text
}, '')
aiChatStore.updateChatByUuid(uuid, dataSources.value.length - 1, {
text: msg,
inversion: false,
loading: true,
})
scrollToBottomIfAtBottom()
}
},
}
)
aiChatStore.updateChatSomeByUuid(uuid, dataSources.value.length - 1, {
loading: false,
})
}
await fetchChatAPIOnce()
} catch (error) {
if (error.message === 'canceled') {
aiChatStore.updateChatSomeByUuid(uuid, dataSources.value.length - 1, {
loading: false,
})
return
}
const errorMessage = error?.errmsg ?? '好像出错了,请稍后再试。'
aiChatStore.updateChatSomeByUuid(uuid, dataSources.value.length - 1, {
loading: false,
text: errorMessage,
})
} finally {
loading.value = false
}
}
function handleStop() {
if (loading.value) {
controller.abort()
loading.value = false
}
}
function parseEventMessages(dataString) {
const lines = dataString.trim().split('\n')
const eventMessages = []
let currentEvent = {}
for (const line of lines) {
if (line.startsWith('event:message')) {
if (Object.keys(currentEvent).length > 0) {
eventMessages.push(currentEvent)
currentEvent = {}
}
} else if (line.startsWith('data:')) {
const jsonData = line.substring('data:'.length)
currentEvent = JSON.parse(jsonData)
}
}
if (Object.keys(currentEvent).length > 0) {
eventMessages.push(currentEvent)
}
return eventMessages
}
function scrollToBottom() {
nextTick()
scrollRefAi.value.scrollBottom()
}
function scrollToBottomIfAtBottom() {
nextTick()
scrollRefAi.value.scrollToBottomIfAtBottom()
}
</script>
<style lang="scss" scoped>
:deep(.ant-input-group) {
.ant-input-group-addon {
height: 100%;
// display: inline-block;
&:last-child {
padding: 0;
}
}
}
.options {
.active {
@apply bg-[#3662FE] text-white border-[#3662FE];
}
}
</style>