aigc-h5/src/views/chat/index.vue

461 lines
12 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="flex flex-col w-full h-full">
<ScrollContainer id="scrollRef" ref="scrollRef">
<div class="p-26px">
<MessageGroup
v-for="(item, i) in dataSources"
:key="i"
:arr="item"
></MessageGroup>
</div>
</ScrollContainer>
<div class="p-18px">
<div class="border border-[#414548]">
<div class="border-b border-[#414548] h-74px flex items-center px-25px">
<SvgIcon
@click="handleRefresh"
name="refresh"
class="text-white text-26px cursor-pointer mr-28px"
></SvgIcon>
<SvgIcon
@click="handleStop"
name="stop"
class="text-white text-26px cursor-pointer mr-28px"
></SvgIcon>
<div class="flex-1 text-right text-22px">
剩余字数:{{ surplus }}
</div>
</div>
<van-field
ref="inputRef"
class="cu-field-msg !text-white"
type="textarea"
placeholder="请输入内容"
:rows="3"
v-model="inputValue"
:border="false"
@pressEnter="handleEnter"
/>
<div class="border-t border-[#414548] pb-10px px-14px">
<div class="flex items-end my-14px">
<div class="flex">
<div class="text-[#EC4B4B] text-18px">*</div>
<div class="opacity-40 text-21px">
可支持的格式:.doc、.docx、.pdf小于20M
</div>
</div>
</div>
<div class="flex items-center">
<van-button
@click="handleClear"
type="primary"
class="!rounded-2px !h-53px !bg-[#414548] !mr-14px border-[#414548] text-white !border-none text-22px"
>
<template #icon>
<SvgIcon name="delete" class="text-25px mr-13px" />
</template>
清除</van-button
>
<UploadComp @change="handleFileChange">
<van-button
type="primary"
class="!rounded-2px !h-53px !mr-14px border-[#414548] text-white !border-none !text-22px !leading-53px"
>
<template #icon>
<SvgIcon name="cloud-upload" class="text-25px mr-13px" />
</template>
上传</van-button
>
</UploadComp>
<div class="flex-1"></div>
<van-button
@click="handleSubmit"
type="primary"
class="!rounded-2px !h-53px border-[#414548] text-white !border-none !text-22px"
>
<template #icon>
<SvgIcon name="generated" class="text-25px mr-13px" />
</template>
生成</van-button
>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import MessageGroup from './components/message-group.vue'
import {
ref,
computed,
onMounted,
nextTick,
onBeforeMount,
getCurrentInstance,
} from 'vue'
import ScrollContainer from '@/components/ScrollContainer/index.vue'
import http from '@/io/request'
import { v4 as uuidv4 } from 'uuid'
import { useRoute } from 'vue-router'
import { useChat } from '@/stores'
import UploadComp from './components/upload-comp.vue'
import extractTextFromPDF from '@/utils/pdfUtils.js'
import extractDocxText from '@/utils/docxUtils.js'
import { getFileInfo } from '@/utils/fileUtils.js'
import { showToast } from 'vant'
import { useAuthModal } from '@/stores/authModal'
import { useUserInfo } from '@/stores/userInfo'
const userInfo = useUserInfo()
const isLogin = computed(() => {
return !!userInfo.userData?.id ?? false
})
const surplus = computed(() => {
const { words_count=0 ,words_used = 0} = userInfo.userData?.equity ?? {}
return words_count - words_used
})
const authModal = useAuthModal()
const { proxy } = getCurrentInstance()
let controller = new AbortController()
const inputRef = ref(null)
const route = useRoute()
const { uuid } = route.params
const chatStore = useChat()
const scrollRef = ref(null)
const loading = ref(false)
const inputValue = ref('')
const fileLoading = ref(false)
const currentChart = computed(() => chatStore.getCurrentChat)
async function handleFileChange(file) {
if (!isLogin.value) {
authModal.setAuthModalType('login')
authModal.showAuthModal()
return
}
if (!userInfo.userData?.equity?.pdf) return showToast('上传文件权限未开通')
if (fileLoading.value) return
fileLoading.value = true
try {
const valid = validateFile(file)
if (!valid) return
const info = getFileInfo(file)
let result = ''
if (info.extension === 'docx' || info.extension === 'doc') {
result = await extractDocxText(file)
} else if (info.extension === 'pdf') {
result = await extractTextFromPDF(file)
}
if (!result || result.trim() == '') return showToast('文件内容为空')
onConversation('next', info, result)
} catch (error) {
showToast(error.message)
} finally {
fileLoading.value = false
}
}
function validateFile(file) {
const allowedExtensions = ['doc', 'docx', 'pdf']
const maxFileSize = 20 * 1024 * 1024 // 20MB
const fileExtension = getFileExtension(file.name)
const fileSize = file.size
if (fileSize === 0) {
throw new Error('文件内容为空')
}
if (!allowedExtensions.includes(fileExtension)) {
throw new Error('文件格式无效')
}
if (fileSize > maxFileSize) {
throw new Error('文件大小超出限制')
}
return true
}
function getFileExtension(fileName) {
const parts = fileName.split('.')
return parts[parts.length - 1].toLowerCase()
}
const conversationList = computed(() => {
return chatStore.getCurrentChat.filter((item) => {
const key = Object.keys(item)[0]
const value = item[key]
return !value.inversion
})
})
const conversationUserList = computed(() => {
return chatStore.getCurrentChat.filter((item) => {
const key = Object.keys(item)[0]
const value = item[key]
return value.inversion
})
})
const dataSources = computed(() => {
const arr = chatStore.getCurrentChat
return groupDataByParentId(arr)
})
function groupDataByParentId(data) {
return data.reduce((groupedData, item) => {
const itemId = Object.keys(item)[0]
const itemData = item[itemId]
if (itemData.parent_id === '') {
groupedData[itemId] = []
} else {
const parentItemId = itemData.parent_id
groupedData[parentItemId] = groupedData[parentItemId] || []
groupedData[parentItemId].push(itemData)
}
return groupedData
}, {})
}
function handleRefresh() {
if (currentChart.value.length && !loading.value) onConversation('variant')
}
function handleStop() {
if (loading.value) {
controller.abort()
loading.value = false
}
}
function handleClear() {
inputValue.value = ''
}
function handleSubmit() {
onConversation()
}
async function onConversation(action = 'next', file, fileText) {
let message = inputValue.value || fileText
if (loading.value) return
if (!isLogin) {
authModal.setAuthModalType('login')
authModal.showAuthModal()
return
}
if ((!message || message.trim() === '') && action == 'next') return
const params = {
action: action,
conversation_id: chatStore.active,
message: {
text: message?.substring(0, 4000),
id: uuidv4(),
type: (file?.extension === 'doc' ? 'docx' : file?.extension) || 'text',
fileInfo: file,
},
}
if (action == 'next') {
const lastContext =
conversationList.value[conversationList.value.length - 1]
params.parent_message_id = uuidv4()
if (lastContext) {
params.parent_message_id = lastContext[Object.keys(lastContext)[0]]?.id
}
chatStore.addChatByUuid(chatStore.active, {
text: message,
inversion: true,
parent_id: params.parent_message_id || '',
id: params.message.id,
type: params.message.type,
fileInfo: file,
})
scrollToBottom()
chatStore.addChatByUuid(chatStore.active, {
text: '⋯',
id: uuidv4(),
parent_id: params.message.id,
inversion: false,
})
}
if (action == 'variant') {
const lastUserContext =
conversationUserList.value[conversationUserList.value.length - 1]
if (lastUserContext) {
const obj = lastUserContext[Object.keys(lastUserContext)[0]]
params.message.text = obj.text?.substring(0, 4000)
params.message.id = obj.id
params.parent_message_id = obj.id
}
chatStore.addChatByUuid(chatStore.active, {
text: '⋯',
id: uuidv4(),
parent_id: params.parent_message_id,
inversion: false,
})
}
loading.value = true
inputValue.value = ''
scrollToBottom()
let tempMessage_id = null
try {
const fetchChatAPIOnce = async () => {
await http.post('/api/v1/conversation', params, {
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]
tempMessage_id = message_id
const msg = arr.reduce((acc, item) => {
return acc + item.text
}, '')
chatStore.updateChatByUuid(
conversation_id,
chatStore.getCurrentChat.length - 1,
{
[message_id]: {
conversation_id: conversation_id,
id: message_id,
text: msg,
inversion: false,
loading: true,
parent_id: params.message.id || '',
},
}
)
scrollToBottomIfAtBottom()
}
},
})
chatStore.updateChatSome(chatStore.getCurrentChat.length - 1, {
loading: false,
})
await chatStore.getChat(chatStore.active)
}
await fetchChatAPIOnce()
} catch (error) {
if (error.message === 'canceled') {
chatStore.updateChatSome(chatStore.getCurrentChat.length - 1, {
loading: false,
})
return
}
const errorMessage = error?.errmsg ?? '好像出错了,请稍后再试。'
chatStore.updateChatSome(chatStore.getCurrentChat.length - 1, {
loading: false,
text: errorMessage,
})
} finally {
loading.value = false
userInfo.getUserInfo()
}
}
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 handleEnter(event) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault()
handleSubmit()
}
}
function handleDataChange(data) {
inputValue.value = data
}
onMounted(async () => {
proxy.$mitt.on('temp-copy', handleDataChange)
if (inputRef.value) {
inputRef.value.focus()
}
await chatStore.getChat(uuid)
scrollToBottom()
})
function scrollToBottom() {
nextTick()
scrollRef.value.scrollBottom()
}
function scrollToBottomIfAtBottom() {
nextTick()
scrollRef.value.scrollToBottomIfAtBottom()
}
</script>
<style lang="scss" scoped>
.scroll-smooth {
scroll-behavior: smooth;
}
</style>
<style lang="scss">
.cu-field-msg {
&.van-cell {
background-color: transparent;
padding: 14px 18px;
font-size: 22px;
placeholder-color: #999999;
}
.van-field__control {
color: white;
}
}
</style>