389 lines
10 KiB
Vue
389 lines
10 KiB
Vue
<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>
|