345 lines
9.0 KiB
Vue
345 lines
9.0 KiB
Vue
<template>
|
||
<LayoutContent>
|
||
<div class="flex flex-col w-full h-full">
|
||
<ScrollContainer id="scrollRef" ref="scrollRef">
|
||
<div class="p-4">
|
||
<MessageGroup
|
||
v-for="(item, i) in dataSources"
|
||
:key="i"
|
||
:arr="item"
|
||
></MessageGroup>
|
||
</div>
|
||
</ScrollContainer>
|
||
<div class="p-2">
|
||
<div class="border border-[#414548]">
|
||
<div class="border-b border-[#414548] h-9 flex items-center px-5">
|
||
<SvgIcon
|
||
@click="handleRefresh"
|
||
name="refresh"
|
||
class="text-white text-base cursor-pointer mr-6"
|
||
></SvgIcon>
|
||
<SvgIcon
|
||
@click="handleStop"
|
||
name="stop"
|
||
class="text-white text-base cursor-pointer mr-6"
|
||
></SvgIcon>
|
||
</div>
|
||
<a-textarea
|
||
ref="inputRef"
|
||
class="bg-transparent border-none rounded-none text-white minp focus:shadow-none placeholder-[#FFFFFF40]"
|
||
placeholder="请输入内容"
|
||
:rows="4"
|
||
v-model:value="inputValue"
|
||
@pressEnter="handleEnter"
|
||
/>
|
||
<div class="flex pb-5 px-5 pt-4 justify-between">
|
||
<div class="flex items-end">
|
||
<a-button
|
||
@click="handleClear"
|
||
type="primary"
|
||
class="rounded-4px h-9.5 px-6 !bg-[#414548] mr-2.5 border-[#414548] text-white"
|
||
>
|
||
<template #icon>
|
||
<SvgIcon name="delete" class="text-lg mr-2" />
|
||
</template>
|
||
清除</a-button
|
||
>
|
||
<a-button type="primary" class="rounded-4px h-9.5 px-6">
|
||
<template #icon>
|
||
<SvgIcon name="cloud-upload" class="text-lg mr-2" />
|
||
</template>
|
||
上传</a-button
|
||
>
|
||
<div class="ml-2">
|
||
<span class="text-[#EC4B4B] text-base">*</span>
|
||
<span class="opacity-40 text-xs">
|
||
可支持的格式:.doc、.docx、.pdf(小于20M)
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<a-button
|
||
@click="handleSubmit"
|
||
type="primary"
|
||
class="rounded-4px h-9.5 px-6"
|
||
>
|
||
<template #icon>
|
||
<SvgIcon name="generated" class="text-sm mr-2" />
|
||
</template>
|
||
生成</a-button
|
||
>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</LayoutContent>
|
||
</template>
|
||
<script setup>
|
||
import { Layout } from 'ant-design-vue'
|
||
import MessageGroup from './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'
|
||
|
||
|
||
|
||
const { proxy } = getCurrentInstance();
|
||
|
||
let controller = new AbortController()
|
||
|
||
const inputRef = ref(null)
|
||
|
||
const LayoutContent = Layout.Content
|
||
|
||
const route = useRoute()
|
||
|
||
const { uuid } = route.params
|
||
|
||
const chatStore = useChat()
|
||
|
||
const scrollRef = ref(null)
|
||
|
||
const loading = ref(false)
|
||
|
||
const inputValue = ref('')
|
||
|
||
const currentChart = computed(() => chatStore.getCurrentChat)
|
||
|
||
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') {
|
||
let message = inputValue.value
|
||
if (loading.value) return
|
||
if ((!message || message.trim() === '') && action == 'next') return
|
||
controller = new AbortController()
|
||
const params = {
|
||
action: action,
|
||
conversation_id: chatStore.active,
|
||
message: {
|
||
text: message,
|
||
id: uuidv4(),
|
||
},
|
||
}
|
||
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,
|
||
})
|
||
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
|
||
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
|
||
}
|
||
}
|
||
|
||
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>
|