|
@@ -6,6 +6,12 @@
|
|
|
<div v-for="(message, index) in messages" :key="index" class="chat-message">
|
|
<div v-for="(message, index) in messages" :key="index" class="chat-message">
|
|
|
<div class="message-content user-message chat" v-if="message.content">
|
|
<div class="message-content user-message chat" v-if="message.content">
|
|
|
<div v-html="message.content"></div>
|
|
<div v-html="message.content"></div>
|
|
|
|
|
+ <!-- 显示上传的文件 -->
|
|
|
|
|
+ <div class="message-files" v-if="message.sourceFiles && message.sourceFiles.length > 0">
|
|
|
|
|
+ <div v-for="(file, index) in message.sourceFiles" :key="index" class="message-file-item">
|
|
|
|
|
+ <span>{{ file.title }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="message-content ai-message chat" v-if="message.aiContent || message.loading">
|
|
<div class="message-content ai-message chat" v-if="message.aiContent || message.loading">
|
|
|
<div v-if="message.aiContent" v-html="message.aiContent"></div>
|
|
<div v-if="message.aiContent" v-html="message.aiContent"></div>
|
|
@@ -49,7 +55,7 @@
|
|
|
:placeholder="messages.length === 0 ? lang.ssAiChatExample : lang.ssAiChatShortcut"
|
|
:placeholder="messages.length === 0 ? lang.ssAiChatExample : lang.ssAiChatShortcut"
|
|
|
v-model="inputText" @keyup.enter.exact="sendMessage" rows="5" />
|
|
v-model="inputText" @keyup.enter.exact="sendMessage" rows="5" />
|
|
|
<div class="input-actions">
|
|
<div class="input-actions">
|
|
|
- <FileInput accept="*" @change="handleFileUpload" v-show="false">
|
|
|
|
|
|
|
+ <FileInput accept="*" @change="handleFileUpload" >
|
|
|
<button class="attach-btn">
|
|
<button class="attach-btn">
|
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
<path
|
|
<path
|
|
@@ -98,7 +104,9 @@ import { useSlidesStore } from '@/store'
|
|
|
import { lang } from '@/main'
|
|
import { lang } from '@/main'
|
|
|
import MarkdownIt from 'markdown-it'
|
|
import MarkdownIt from 'markdown-it'
|
|
|
import { getWorkPageId } from '@/services/course'
|
|
import { getWorkPageId } from '@/services/course'
|
|
|
-import FileInput from '@/components/FileInput.vue'
|
|
|
|
|
|
|
+import FileInput from '@/components/FileInput2.vue'
|
|
|
|
|
+import axios from '@/services/config'
|
|
|
|
|
+import message from '@/utils/message'
|
|
|
|
|
|
|
|
interface ChatMessage {
|
|
interface ChatMessage {
|
|
|
uid?: string
|
|
uid?: string
|
|
@@ -155,7 +163,7 @@ const chatLoading = ref(false)
|
|
|
const showQuickActions = ref(false)
|
|
const showQuickActions = ref(false)
|
|
|
const streamController = ref<{ abort: () => void } | null>(null)
|
|
const streamController = ref<{ abort: () => void } | null>(null)
|
|
|
const noStreamController = ref<{ promise: Promise<string>; abort: () => void } | null>(null)
|
|
const noStreamController = ref<{ promise: Promise<string>; abort: () => void } | null>(null)
|
|
|
-const files = ref<Array<{ title: string; id?: string; url?: string }>>([])
|
|
|
|
|
|
|
+const files = ref<Array<{ title: string; id?: string | null; url?: string; isProcessing?: boolean; cancel?: () => void }>>([])
|
|
|
|
|
|
|
|
// 快捷操作短语数组
|
|
// 快捷操作短语数组
|
|
|
const quickActions = [
|
|
const quickActions = [
|
|
@@ -177,6 +185,12 @@ const sendMessage = () => {
|
|
|
if (chatLoading.value) {
|
|
if (chatLoading.value) {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
+ // 检查是否有文件正在处理中
|
|
|
|
|
+ const hasProcessingFile = files.value.some(file => file.isProcessing)
|
|
|
|
|
+ if (hasProcessingFile) {
|
|
|
|
|
+ message.error('请等待文件上传完成后再发送消息')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
if (inputText.value.trim() || files.value.length > 0) {
|
|
if (inputText.value.trim() || files.value.length > 0) {
|
|
|
// 添加用户消息
|
|
// 添加用户消息
|
|
|
messages.value.push({
|
|
messages.value.push({
|
|
@@ -196,6 +210,10 @@ const sendMessage = () => {
|
|
|
prevChatResult()
|
|
prevChatResult()
|
|
|
messages.value.at(-1).loading = true
|
|
messages.value.at(-1).loading = true
|
|
|
messages.value.at(-1).chatloading = true
|
|
messages.value.at(-1).chatloading = true
|
|
|
|
|
+ messages.value.at(-1).sourceFiles = files.value.filter(file => file.id !== null).map(file => ({
|
|
|
|
|
+ title: file.title,
|
|
|
|
|
+ id: file.id
|
|
|
|
|
+ }))
|
|
|
chatLoading.value = true
|
|
chatLoading.value = true
|
|
|
sendAction(inputText.value)
|
|
sendAction(inputText.value)
|
|
|
inputText.value = ''
|
|
inputText.value = ''
|
|
@@ -220,22 +238,62 @@ const stopMessage = () => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 处理文件上传
|
|
// 处理文件上传
|
|
|
-const handleFileUpload = (files2) => {
|
|
|
|
|
|
|
+const handleFileUpload = async (files2: File[]) => {
|
|
|
const maxSize = 10 * 1024 * 1024 // 10MB
|
|
const maxSize = 10 * 1024 * 1024 // 10MB
|
|
|
|
|
+ const uploadPromises = []
|
|
|
|
|
+
|
|
|
for (let i = 0; i < files2.length; i++) {
|
|
for (let i = 0; i < files2.length; i++) {
|
|
|
const file = files2[i]
|
|
const file = files2[i]
|
|
|
if (file.size > maxSize) {
|
|
if (file.size > maxSize) {
|
|
|
- alert('文件大小不能超过10MB')
|
|
|
|
|
|
|
+ message.error('文件大小不能超过10MB')
|
|
|
continue
|
|
continue
|
|
|
}
|
|
}
|
|
|
|
|
+ // 先添加文件到列表,显示解析中状态
|
|
|
|
|
+ const fileIndex = files.value.length
|
|
|
files.value.push({
|
|
files.value.push({
|
|
|
- title: file.name
|
|
|
|
|
|
|
+ title: file.name + ' (解析中...)',
|
|
|
|
|
+ id: null,
|
|
|
|
|
+ isProcessing: true,
|
|
|
|
|
+ cancel: null
|
|
|
|
|
+ })
|
|
|
|
|
+ // 创建取消控制器
|
|
|
|
|
+ const controller = new AbortController()
|
|
|
|
|
+ files.value[fileIndex].cancel = () => {
|
|
|
|
|
+ controller.abort()
|
|
|
|
|
+ files.value.splice(fileIndex, 1)
|
|
|
|
|
+ }
|
|
|
|
|
+ // 创建上传Promise并添加到数组
|
|
|
|
|
+ const uploadPromise = uploadFile2(file, controller.signal).then(res => {
|
|
|
|
|
+ if (!res) {
|
|
|
|
|
+ files.value.splice(fileIndex, 1)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ // 上传成功,更新文件状态
|
|
|
|
|
+ files.value[fileIndex] = {
|
|
|
|
|
+ title: file.name,
|
|
|
|
|
+ id: res.results.document_id,
|
|
|
|
|
+ isProcessing: false
|
|
|
|
|
+ }
|
|
|
|
|
+ }).catch(error => {
|
|
|
|
|
+ if (error.name !== 'AbortError') {
|
|
|
|
|
+ console.error('文件上传失败:', error)
|
|
|
|
|
+ files.value.splice(fileIndex, 1)
|
|
|
|
|
+ }
|
|
|
})
|
|
})
|
|
|
|
|
+
|
|
|
|
|
+ uploadPromises.push(uploadPromise)
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ // 等待所有文件上传完成
|
|
|
|
|
+ await Promise.allSettled(uploadPromises)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 移除文件
|
|
// 移除文件
|
|
|
const removeFile = (index: number) => {
|
|
const removeFile = (index: number) => {
|
|
|
|
|
+ const file = files.value[index]
|
|
|
|
|
+ if (file && file.isProcessing && file.cancel) {
|
|
|
|
|
+ file.cancel()
|
|
|
|
|
+ }
|
|
|
files.value.splice(index, 1)
|
|
files.value.splice(index, 1)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -309,7 +367,7 @@ const sendAction = async (action: string) => {
|
|
|
chatLoading.value = false
|
|
chatLoading.value = false
|
|
|
prevChatResult()
|
|
prevChatResult()
|
|
|
}
|
|
}
|
|
|
- }, session_name.value).then(controller => {
|
|
|
|
|
|
|
+ }, session_name.value, messages.value.at(-1).sourceFiles?.map(file => file.id).filter(Boolean)).then(controller => {
|
|
|
streamController.value = controller
|
|
streamController.value = controller
|
|
|
}).catch(err => {
|
|
}).catch(err => {
|
|
|
chatLoading.value = false
|
|
chatLoading.value = false
|
|
@@ -376,6 +434,49 @@ const setPageId = async (tool: any, json: any) => {
|
|
|
return res[0][0].id
|
|
return res[0][0].id
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// 上传文件
|
|
|
|
|
+const uploadFile2 = async (file: File, signal?: AbortSignal): Promise<any> => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const uuid = uuidv4()
|
|
|
|
|
+ const formData = new FormData()
|
|
|
|
|
+ const timestamp = Date.now()
|
|
|
|
|
+ const finalExtension = file.name.split('.').pop()?.toLowerCase() || ''
|
|
|
|
|
+ const baseName = file.name.slice(0, -(finalExtension.length + 1))
|
|
|
|
|
+
|
|
|
|
|
+ formData.append(
|
|
|
|
|
+ 'file',
|
|
|
|
|
+ new File([file], `${baseName}${timestamp}.${finalExtension}`)
|
|
|
|
|
+ )
|
|
|
|
|
+ formData.append('collection_ids', JSON.stringify([]))
|
|
|
|
|
+ formData.append('id', uuid)
|
|
|
|
|
+ formData.append('metadata', JSON.stringify({ title: file.name }))
|
|
|
|
|
+ formData.append('ingestion_mode', 'fast')
|
|
|
|
|
+ formData.append('run_with_orchestration', 'true')
|
|
|
|
|
+
|
|
|
|
|
+ // 同步知识库
|
|
|
|
|
+ const res = await axios.post(
|
|
|
|
|
+ 'https://r2rserver.cocorobo.cn/v3/documents',
|
|
|
|
|
+ formData,
|
|
|
|
|
+ {
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Content-Type': 'multipart/form-data',
|
|
|
|
|
+ },
|
|
|
|
|
+ signal: signal
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ console.log(res)
|
|
|
|
|
+ return res
|
|
|
|
|
+ }
|
|
|
|
|
+ catch (error) {
|
|
|
|
|
+ console.log('err', error)
|
|
|
|
|
+ if (error.name === 'AbortError') {
|
|
|
|
|
+ throw error
|
|
|
|
|
+ }
|
|
|
|
|
+ return ''
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
|
|
|
|
|
|
|
|
const agentid1 = ref('cbb29b41-2a4a-4453-bf8d-357929ced4bd')// 判断意图
|
|
const agentid1 = ref('cbb29b41-2a4a-4453-bf8d-357929ced4bd')// 判断意图
|
|
@@ -733,4 +834,31 @@ ul {
|
|
|
border-right: 1px solid #000;
|
|
border-right: 1px solid #000;
|
|
|
padding: 10px;
|
|
padding: 10px;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+.message-files {
|
|
|
|
|
+ margin-top: 8px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.message-file-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ background: #f3f4f6;
|
|
|
|
|
+ padding: 4px 8px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ max-width: 200px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.message-file-item span {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+}
|
|
|
</style>
|
|
</style>
|