chat文件上传
parent
58c4710b56
commit
655ff9bb90
|
|
@ -16,10 +16,12 @@
|
|||
"echarts": "^5.4.3",
|
||||
"highlight.js": "^11.8.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mammoth": "^1.6.0",
|
||||
"markdown-it": "^13.0.1",
|
||||
"markdown-it-link-attributes": "^4.0.1",
|
||||
"md5": "^2.3.0",
|
||||
"mitt": "^3.0.1",
|
||||
"pdfjs-dist": "2.6.347",
|
||||
"pinia": "^2.1.3",
|
||||
"qrcode.vue": "^3.4.1",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1692435749942" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3272" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M797.696 442.368q-60.416 0-113.152 22.528t-91.648 61.952-61.952 92.16-23.04 113.152q0 50.176 17.408 97.28l-392.192 0q-21.504 0-44.544-9.216t-42.496-26.112-31.744-40.96-12.288-53.76l0-41.984 0-81.92q0-47.104-0.512-100.864t-0.512-99.84l0-78.848 0-35.84q0-61.44 34.304-97.28t94.72-35.84l503.808 0q23.552 0 47.616 8.704t43.008 24.064 31.232 35.84 12.288 44.032l0 13.312-517.12 0q-51.2 0-67.584 57.344-8.192 29.696-18.432 62.976t-18.432 61.952q-10.24 33.792-20.48 65.536-2.048 8.192-2.048 14.336 0 16.384 11.264 28.16t28.672 11.776 26.624-12.288 14.336-27.648l56.32-196.608 541.696 0q18.432 3.072 35.84 11.776t30.208 23.552 18.432 36.864 0.512 51.712q0 2.048-0.512 5.12t-1.536 8.192q-39.936-13.312-88.064-13.312zM802.816 510.976q46.08 0 86.016 17.408t70.144 47.616 47.616 70.144 17.408 86.016-17.408 86.016-47.616 70.144-70.144 47.616-86.016 17.408-86.016-17.408-70.144-47.616-47.616-70.144-17.408-86.016 17.408-86.016 47.616-70.144 70.144-47.616 86.016-17.408zM845.824 729.088l67.584-68.608q9.216-9.216 9.216-22.528t-9.216-22.528-22.528-9.216-22.528 9.216l-68.608 68.608-67.584-68.608q-9.216-9.216-22.528-9.216t-22.528 9.216-9.216 22.528 9.216 22.528l67.584 68.608-67.584 67.584q-9.216 9.216-9.216 22.528t9.216 22.528q9.216 10.24 22.528 10.24t22.528-10.24l67.584-67.584 68.608 67.584q9.216 10.24 22.528 10.24t22.528-10.24q9.216-9.216 9.216-22.528t-9.216-22.528z" p-id="3273"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg t="1692433413174" class="icon" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="1991" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="200" height="200">
|
||||
<path
|
||||
d="M855.04 385.024q19.456 2.048 38.912 10.24t33.792 23.04 21.504 37.376 2.048 54.272q-2.048 8.192-8.192 40.448t-14.336 74.24-18.432 86.528-19.456 76.288q-5.12 18.432-14.848 37.888t-25.088 35.328-36.864 26.112-51.2 10.24H195.584q-21.504 0-44.544-9.216t-42.496-26.112-31.744-40.96-12.288-53.76V327.68q0-62.464 33.792-97.792t95.232-35.328h503.808q22.528 0 46.592 8.704t43.52 24.064 31.744 35.84 12.288 44.032v11.264H778.24q-40.96 0-95.744-0.512t-116.736-0.512-115.712-0.512-92.672-0.512h-47.104q-26.624 0-41.472 16.896t-23.04 44.544q-8.192 29.696-18.432 62.976t-18.432 61.952q-10.24 33.792-20.48 65.536-2.048 8.192-2.048 13.312 0 17.408 11.776 29.184t29.184 11.776q31.744 0 43.008-39.936L324.608 384q133.12 1.024 243.712 1.024h286.72z"
|
||||
p-id="1992"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
|
@ -19,7 +19,7 @@ function platCheck() {
|
|||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
const mobileKeywords = ['iphone', 'ipod', 'android', 'silk', 'blackberry', 'bb10', 'phone', 'mobile', 'kindle', 'opera mini', 'mobile safari', 'windows phone'];
|
||||
const isMobileDevice = mobileKeywords.some(keyword => userAgent.includes(keyword));
|
||||
if (!isMobileDevice) {
|
||||
if (!isMobileDevice && import.meta.env.PROD) {
|
||||
let _href = window.location.href;
|
||||
let _origin = window.location.origin;
|
||||
window.location.href = _href.replace(_origin, import.meta.env.VITE_PC_HOST);
|
||||
|
|
|
|||
|
|
@ -100,12 +100,21 @@ export const useChat = defineStore("chat-store", {
|
|||
requestBaseUrl: 'chat',
|
||||
}).then((res) => {
|
||||
const { messages } = res;
|
||||
const chat = this.chat.find(item => item.id === uuid)
|
||||
const localMessages = chat?.messages ?? []
|
||||
const temp = messages.map((item) => {
|
||||
var key = Object.keys(item)[0];
|
||||
|
||||
|
||||
const localItem = (localMessages.find(item => Object.keys(item)[0] == key) ?? {})[key] ?? {}
|
||||
|
||||
|
||||
const { role } = item[key]
|
||||
const obj = Object.assign({}, { ...item[key] }, { loading: false, inversion: role == 'user' })
|
||||
const obj = Object.assign({}, localItem, { ...item[key] }, {
|
||||
loading: false, inversion: role == 'user'
|
||||
})
|
||||
return {
|
||||
[key]: obj
|
||||
[key]: obj,
|
||||
};
|
||||
});
|
||||
this.setChat(uuid, { ...res, messages: temp })
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
import mammoth from 'mammoth';
|
||||
import { readFileAsArrayBuffer } from './fileUtils'
|
||||
|
||||
export default async function extractDocxText(file) {
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file)
|
||||
const options = {}
|
||||
const result = await mammoth.extractRawText({ arrayBuffer }, options)
|
||||
return result.value
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
export const readFileAsArrayBuffer = (file) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
resolve(reader.result)
|
||||
}
|
||||
reader.onerror = () => {
|
||||
reject(new Error('Error reading the file.'))
|
||||
}
|
||||
reader.readAsArrayBuffer(file)
|
||||
})
|
||||
}
|
||||
|
||||
export function getFileInfo(file) {
|
||||
const fileName = file.name;
|
||||
const fileExtension = getFileExtension(fileName);
|
||||
const fileSize = file.size;
|
||||
|
||||
return {
|
||||
name: fileName,
|
||||
extension: fileExtension,
|
||||
size: fileSize
|
||||
};
|
||||
}
|
||||
export function getFileExtension(fileName) {
|
||||
const parts = fileName.split('.');
|
||||
return parts[parts.length - 1].toLowerCase();
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import * as pdfjsLib from 'pdfjs-dist'
|
||||
import * as pdfWorkerMin from 'pdfjs-dist/build/pdf.worker.min?url'
|
||||
|
||||
import { readFileAsArrayBuffer } from './fileUtils'
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorkerMin.default
|
||||
|
||||
|
||||
export default async function extractTextFromPDF(file) {
|
||||
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file)
|
||||
|
||||
const loadingTask = pdfjsLib.getDocument(arrayBuffer);
|
||||
const pdfDocument = await loadingTask.promise;
|
||||
const numPages = pdfDocument.numPages;
|
||||
|
||||
let text = '';
|
||||
|
||||
for (let pageNum = 1; pageNum <= numPages; pageNum++) {
|
||||
const page = await pdfDocument.getPage(pageNum);
|
||||
const pageText = await page.getTextContent();
|
||||
|
||||
pageText.items.forEach(item => {
|
||||
text += item.str + ' ';
|
||||
});
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="text-50px">
|
||||
<div class="text-46px">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 32 32"
|
||||
|
|
|
|||
|
|
@ -1,344 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<div :class="wrapClass">
|
||||
<template v-if="file">
|
||||
<div class="p-12px flex items-center text-white">
|
||||
<SvgIcon name="文件" class="text-[#FEE77B] w-5 h-5" />
|
||||
<div class="ml-12px text-22px">{{ file.name }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="bg-[#27292B] flex items-center px-12px py-12px justify-between text-22px"
|
||||
>
|
||||
<div>.{{ file.extension }}</div>
|
||||
<div>{{ kbToMbWithDecimal(file.size) }} M</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="p-12px flex items-center text-white">
|
||||
<SvgIcon name="删除文件夹" class=" text-gray-400 w-5 h-5" />
|
||||
<div class="ml-12px text-22px">文件已失效</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
const props = defineProps({
|
||||
file: {
|
||||
type: Object,
|
||||
},
|
||||
})
|
||||
|
||||
function kbToMbWithDecimal(bytes) {
|
||||
const mb = bytes / (1024 * 1024)
|
||||
return mb.toFixed(1)
|
||||
}
|
||||
|
||||
const wrapClass = computed(() => {
|
||||
return [
|
||||
'text-wrap',
|
||||
'min-w-[80px]',
|
||||
'rounded-6px',
|
||||
'overflow-hidden',
|
||||
'bg-[#d2f9d1]',
|
||||
'dark:bg-[#414548]',
|
||||
'message-request',
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
<template>
|
||||
<div class="mb-37px">
|
||||
<Message
|
||||
:type="currentMsg.type"
|
||||
:file="currentMsg.fileInfo"
|
||||
:text="currentMsg.text"
|
||||
:loading="currentMsg.loading"
|
||||
:inversion="currentMsg.inversion"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
:class="[{ 'flex-row-reverse': inversion }]"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-center flex-shrink-0 h-50px overflow-hidden rounded-full basis-50px mt-4px"
|
||||
class="flex items-center justify-center flex-shrink-0 h-50px overflow-hidden rounded-full basis-50px mt-0px"
|
||||
:class="[inversion ? 'ml-19px' : 'mr-19px']"
|
||||
>
|
||||
<AvatarComponent />
|
||||
|
|
@ -20,11 +20,13 @@
|
|||
>
|
||||
<TextComponent
|
||||
ref="textRef"
|
||||
v-if="type==='text' || !inversion"
|
||||
:inversion="inversion"
|
||||
:text="text"
|
||||
:loading="loading"
|
||||
:as-raw-text="asRawText"
|
||||
/>
|
||||
<FileComponent :file="file" v-else/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -32,8 +34,17 @@
|
|||
<script setup>
|
||||
import TextComponent from './text.vue'
|
||||
import AvatarComponent from './avatar.vue'
|
||||
import FileComponent from './file.vue'
|
||||
import { ref } from 'vue'
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text',
|
||||
},
|
||||
file: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: '',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
<template>
|
||||
<div class="inline-block leading-1">
|
||||
<input
|
||||
type="file"
|
||||
ref="fileInput"
|
||||
class="hidden"
|
||||
@change="handleFileChange"
|
||||
/>
|
||||
<div class="inline-block !leading-0" @click="openFileInput">
|
||||
<slot>Upload File</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const fileInput = ref(null)
|
||||
const emits = defineEmits(['change'])
|
||||
|
||||
function openFileInput() {
|
||||
fileInput.value.value = null;
|
||||
fileInput.value.click()
|
||||
}
|
||||
|
||||
function handleFileChange(event) {
|
||||
const file = event.target.files[0]
|
||||
if (file) {
|
||||
emits('change', file)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -53,15 +53,17 @@
|
|||
</template>
|
||||
清除</van-button
|
||||
>
|
||||
<van-button
|
||||
type="primary"
|
||||
class="!rounded-2px !h-53px !mr-14px border-[#414548] text-white !border-none text-22px"
|
||||
>
|
||||
<template #icon>
|
||||
<SvgIcon name="cloud-upload" 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"
|
||||
|
|
@ -94,6 +96,20 @@ 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 authModal = useAuthModal()
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
|
||||
|
|
@ -113,8 +129,67 @@ 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]
|
||||
|
|
@ -172,8 +247,8 @@ function handleSubmit() {
|
|||
onConversation()
|
||||
}
|
||||
|
||||
async function onConversation(action = 'next') {
|
||||
let message = inputValue.value
|
||||
async function onConversation(action = 'next', file, fileText) {
|
||||
let message = inputValue.value || fileText
|
||||
if (loading.value) return
|
||||
if ((!message || message.trim() === '') && action == 'next') return
|
||||
|
||||
|
|
@ -181,8 +256,10 @@ async function onConversation(action = 'next') {
|
|||
action: action,
|
||||
conversation_id: chatStore.active,
|
||||
message: {
|
||||
text: message,
|
||||
text: message?.substring(0, 4000),
|
||||
id: uuidv4(),
|
||||
type: (file?.extension === 'doc' ? 'docx' : file?.extension) || 'text',
|
||||
fileInfo: file,
|
||||
},
|
||||
}
|
||||
if (action == 'next') {
|
||||
|
|
@ -198,6 +275,8 @@ async function onConversation(action = 'next') {
|
|||
inversion: true,
|
||||
parent_id: params.parent_message_id || '',
|
||||
id: params.message.id,
|
||||
type: params.message.type,
|
||||
fileInfo: file,
|
||||
})
|
||||
scrollToBottom()
|
||||
chatStore.addChatByUuid(chatStore.active, {
|
||||
|
|
@ -212,7 +291,7 @@ async function onConversation(action = 'next') {
|
|||
conversationUserList.value[conversationUserList.value.length - 1]
|
||||
if (lastUserContext) {
|
||||
const obj = lastUserContext[Object.keys(lastUserContext)[0]]
|
||||
params.message.text = obj.text
|
||||
params.message.text = obj.text?.substring(0, 4000)
|
||||
params.message.id = obj.id
|
||||
params.parent_message_id = obj.id
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue