mirror of
https://github.com/1Panel-dev/MaxKB.git
synced 2025-12-26 01:33:05 +00:00
feat: add local and URL upload options with validation messages
This commit is contained in:
parent
8c0836627a
commit
56a8795b33
|
|
@ -1,5 +1,5 @@
|
|||
import { Result } from '@/request/Result'
|
||||
import { get, post, del, put } from '@/request/index'
|
||||
import {Result} from '@/request/Result'
|
||||
import {get, post, del, put} from '@/request/index'
|
||||
|
||||
const prefix = '/oss/file'
|
||||
/**
|
||||
|
|
@ -10,6 +10,10 @@ const postImage: (data: any) => Promise<Result<any>> = (data) => {
|
|||
return post(`${prefix}`, data)
|
||||
}
|
||||
|
||||
const getFile: (params: any) => Promise<Result<any>> = (params) => {
|
||||
return get(`/oss/get_url` , params)
|
||||
}
|
||||
export default {
|
||||
postImage,
|
||||
getFile
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@
|
|||
@mouseleave.stop="mouseleave()"
|
||||
>
|
||||
<div class="flex align-center">
|
||||
<img :src="getImgUrl(item && item?.name)" alt="" width="24" />
|
||||
<img :src="getImgUrl(item && item?.name)" alt="" width="24"/>
|
||||
<div class="ml-4 ellipsis-1" :title="item && item?.name">
|
||||
{{ item && item?.name }}
|
||||
</div>
|
||||
|
|
@ -53,7 +53,7 @@
|
|||
v-if="showDelete === item.url"
|
||||
>
|
||||
<el-icon style="font-size: 16px; top: 2px">
|
||||
<CircleCloseFilled />
|
||||
<CircleCloseFilled/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -80,7 +80,7 @@
|
|||
@mouseleave.stop="mouseleave()"
|
||||
>
|
||||
<div class="flex align-center">
|
||||
<img :src="getImgUrl(item && item?.name)" alt="" width="24" />
|
||||
<img :src="getImgUrl(item && item?.name)" alt="" width="24"/>
|
||||
<div class="ml-4 ellipsis-1" :title="item && item?.name">
|
||||
{{ item && item?.name }}
|
||||
</div>
|
||||
|
|
@ -91,7 +91,7 @@
|
|||
v-if="showDelete === item.url"
|
||||
>
|
||||
<el-icon style="font-size: 16px; top: 2px">
|
||||
<CircleCloseFilled />
|
||||
<CircleCloseFilled/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -115,7 +115,7 @@
|
|||
@mouseleave.stop="mouseleave()"
|
||||
>
|
||||
<div class="flex align-center">
|
||||
<img :src="getImgUrl(item && item?.name)" alt="" width="24" />
|
||||
<img :src="getImgUrl(item && item?.name)" alt="" width="24"/>
|
||||
<div class="ml-4 ellipsis-1" :title="item && item?.name">
|
||||
{{ item && item?.name }}
|
||||
</div>
|
||||
|
|
@ -126,7 +126,7 @@
|
|||
v-if="showDelete === item.url"
|
||||
>
|
||||
<el-icon style="font-size: 16px; top: 2px">
|
||||
<CircleCloseFilled />
|
||||
<CircleCloseFilled/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -146,7 +146,7 @@
|
|||
v-if="showDelete === item.url"
|
||||
>
|
||||
<el-icon style="font-size: 16px; top: 2px">
|
||||
<CircleCloseFilled />
|
||||
<CircleCloseFilled/>
|
||||
</el-icon>
|
||||
</div>
|
||||
<el-image
|
||||
|
|
@ -173,7 +173,7 @@
|
|||
v-if="showDelete === item.url"
|
||||
>
|
||||
<el-icon style="font-size: 16px; top: 2px">
|
||||
<CircleCloseFilled />
|
||||
<CircleCloseFilled/>
|
||||
</el-icon>
|
||||
</div>
|
||||
<video
|
||||
|
|
@ -213,7 +213,7 @@
|
|||
|
||||
<div class="operate flex-between">
|
||||
<div>
|
||||
<slot name="userInput" />
|
||||
<slot name="userInput"/>
|
||||
</div>
|
||||
<div class="flex align-center">
|
||||
<template v-if="props.applicationDetails.stt_model_enable">
|
||||
|
|
@ -223,7 +223,7 @@
|
|||
<AppIcon v-if="isMicrophone" iconName="app-keyboard"></AppIcon>
|
||||
<el-icon v-else>
|
||||
<!-- 录音 -->
|
||||
<Microphone />
|
||||
<Microphone/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</span>
|
||||
|
|
@ -235,13 +235,13 @@
|
|||
v-if="recorderStatus === 'STOP'"
|
||||
>
|
||||
<el-icon>
|
||||
<Microphone />
|
||||
<Microphone/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
|
||||
<div v-else class="operate flex align-center">
|
||||
<el-text type="info"
|
||||
>00:{{ recorderTime < 10 ? `0${recorderTime}` : recorderTime }}</el-text
|
||||
>00:{{ recorderTime < 10 ? `0${recorderTime}` : recorderTime }}</el-text
|
||||
>
|
||||
<el-button
|
||||
text
|
||||
|
|
@ -256,48 +256,60 @@
|
|||
</template>
|
||||
|
||||
<template v-if="recorderStatus === 'STOP' || mode === 'mobile'">
|
||||
<span v-if="props.applicationDetails.file_upload_enable" class="flex align-center ml-4">
|
||||
<span
|
||||
v-if="props.applicationDetails.file_upload_enable"
|
||||
class="flex align-center ml-4">
|
||||
<!-- 如果URL地址 -->
|
||||
<!-- <el-button
|
||||
text
|
||||
:disabled="checkMaxFilesLimit() || loading"
|
||||
class="mt-4"
|
||||
@click="showURLSetting = true"
|
||||
>
|
||||
<el-icon><Paperclip /></el-icon>
|
||||
</el-button> -->
|
||||
<el-button
|
||||
v-if="props.applicationDetails.file_upload_setting.url_upload"
|
||||
text
|
||||
:disabled="checkMaxFilesLimit() || loading"
|
||||
class="mt-4"
|
||||
@click="openUrlSetting"
|
||||
>
|
||||
<el-icon><Paperclip/></el-icon>
|
||||
</el-button>
|
||||
<!-- 没有URL地址 -->
|
||||
<el-upload
|
||||
action="#"
|
||||
multiple
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:accept="getAcceptList()"
|
||||
:on-change="(file: any, fileList: any) => uploadFile(file, fileList)"
|
||||
v-model:file-list="fileAllList"
|
||||
ref="upload"
|
||||
>
|
||||
<el-tooltip
|
||||
:disabled="mode === 'mobile'"
|
||||
effect="dark"
|
||||
placement="top"
|
||||
popper-class="upload-tooltip-width"
|
||||
>
|
||||
<template #content>
|
||||
<div class="break-all pre-wrap">
|
||||
{{ $t('chat.uploadFile.label') }}:{{ $t('chat.uploadFile.most')
|
||||
}}{{ props.applicationDetails.file_upload_setting.maxFiles
|
||||
}}{{ $t('chat.uploadFile.limit') }}
|
||||
{{ props.applicationDetails.file_upload_setting.fileLimit }}MB<br />{{
|
||||
$t('chat.uploadFile.fileType')
|
||||
}}:{{ getAcceptList().replace(/\./g, '').replace(/,/g, '、').toUpperCase() }}
|
||||
</div>
|
||||
</template>
|
||||
<el-button text :disabled="checkMaxFilesLimit() || loading" class="mt-4">
|
||||
<el-icon><Paperclip /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</el-upload>
|
||||
<el-upload
|
||||
v-else
|
||||
action="#"
|
||||
multiple
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:accept="getAcceptList()"
|
||||
:on-change="(file: any, fileList: any) => uploadFile(file, fileList)"
|
||||
v-model:file-list="fileAllList"
|
||||
ref="upload"
|
||||
>
|
||||
<el-tooltip
|
||||
:disabled="mode === 'mobile'"
|
||||
effect="dark"
|
||||
placement="top"
|
||||
popper-class="upload-tooltip-width"
|
||||
>
|
||||
<template #content>
|
||||
<div class="break-all pre-wrap">
|
||||
{{ $t('chat.uploadFile.label') }}:{{
|
||||
$t('chat.uploadFile.most')
|
||||
}}{{
|
||||
props.applicationDetails.file_upload_setting.maxFiles
|
||||
}}{{ $t('chat.uploadFile.limit') }}
|
||||
{{
|
||||
props.applicationDetails.file_upload_setting.fileLimit
|
||||
}}MB<br/>{{
|
||||
$t('chat.uploadFile.fileType')
|
||||
}}:{{
|
||||
getAcceptList().replace(/\./g, '').replace(/,/g, '、').toUpperCase()
|
||||
}}
|
||||
</div>
|
||||
</template>
|
||||
<el-button text
|
||||
:disabled="checkMaxFilesLimit() || loading"
|
||||
class="mt-4">
|
||||
<el-icon><Paperclip/></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</el-upload>
|
||||
</span>
|
||||
<el-divider
|
||||
direction="vertical"
|
||||
|
|
@ -317,7 +329,7 @@
|
|||
src="@/assets/icon_send.svg"
|
||||
alt=""
|
||||
/>
|
||||
<SendIcon v-show="!isDisabledChat && !loading && !uploadLoading" />
|
||||
<SendIcon v-show="!isDisabledChat && !loading && !uploadLoading"/>
|
||||
</el-button>
|
||||
</template>
|
||||
</div>
|
||||
|
|
@ -334,26 +346,32 @@
|
|||
|
||||
<!-- 弹出URL设置框 -->
|
||||
<div class="popperURLSetting" v-if="showURLSetting">
|
||||
<el-card shadow="always" class="border-r-8" style="--el-card-padding: 16px">
|
||||
<el-card shadow="always" class="border-r-8" style="--el-card-padding: 16px"
|
||||
v-if="props.applicationDetails.file_upload_setting.url_upload">
|
||||
<el-form label-position="top" ref="urlFormRef" :model="urlForm">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="flex-between">
|
||||
<span>URL 地址</span>
|
||||
<span>{{ $t('chat.uploadFile.urlTitle') }}</span>
|
||||
<el-select
|
||||
:teleported="false"
|
||||
v-model="urlForm.type"
|
||||
size="small"
|
||||
style="width: 85px"
|
||||
>
|
||||
<el-option :label="$t('common.fileUpload.image')" value="image" />
|
||||
<el-option :label="$t('common.fileUpload.audio')" value="audio" />
|
||||
<el-option
|
||||
v-for="option in fileUploadOptions"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
v-show="option.visible"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
<el-input
|
||||
v-model="urlForm.source_url"
|
||||
placeholder="请输入URL地址,每行一个地址"
|
||||
:placeholder="$t('chat.uploadFile.urlPlaceholder')"
|
||||
:rows="5"
|
||||
type="textarea"
|
||||
/>
|
||||
|
|
@ -361,12 +379,14 @@
|
|||
</el-form>
|
||||
<div class="text-right">
|
||||
<el-button @click="showURLSetting = false">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="showURLSetting = false">{{
|
||||
$t('common.confirm')
|
||||
}}</el-button>
|
||||
<el-button type="primary" @click="saveUrl">{{
|
||||
$t('common.confirm')
|
||||
}}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-divider style="margin: 16px 0" />
|
||||
<el-divider style="margin: 16px 0"/>
|
||||
<el-upload
|
||||
v-if="props.applicationDetails.file_upload_setting.local_upload"
|
||||
action="#"
|
||||
multiple
|
||||
:auto-upload="false"
|
||||
|
|
@ -377,32 +397,35 @@
|
|||
ref="upload"
|
||||
class="import-button"
|
||||
>
|
||||
<el-button class="w-full url-upload-button">本地上传</el-button>
|
||||
<el-button class="w-full url-upload-button">{{
|
||||
$t('chat.uploadFile.localUpload')
|
||||
}}
|
||||
</el-button>
|
||||
</el-upload>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick, reactive, type Ref } from 'vue'
|
||||
import { t } from '@/locales'
|
||||
import {computed, nextTick, onMounted, reactive, ref, type Ref} from 'vue'
|
||||
import {t} from '@/locales'
|
||||
import Recorder from 'recorder-core'
|
||||
import TouchChat from './TouchChat.vue'
|
||||
import applicationApi from '@/api/application/application'
|
||||
import { MsgAlert } from '@/utils/message'
|
||||
import { type chatType } from '@/api/type/application'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { getImgUrl } from '@/utils/common'
|
||||
import {MsgAlert, MsgWarning} from '@/utils/message'
|
||||
import {type chatType} from '@/api/type/application'
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
import {getImgUrl} from '@/utils/common'
|
||||
import bus from '@/bus'
|
||||
import 'recorder-core/src/engine/mp3'
|
||||
import 'recorder-core/src/engine/mp3-engine'
|
||||
import { MsgWarning } from '@/utils/message'
|
||||
import chatAPI from '@/api/chat/chat'
|
||||
import imageApi from '@/api/image'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const {
|
||||
query: { mode, question },
|
||||
query: {mode, question},
|
||||
} = route as any
|
||||
const quickInputRef = ref()
|
||||
const props = withDefaults(
|
||||
|
|
@ -451,6 +474,7 @@ const urlForm = reactive({
|
|||
type: '',
|
||||
})
|
||||
|
||||
|
||||
const uploadLoading = computed(() => {
|
||||
return Object.values(filePromisionDict.value).length > 0
|
||||
})
|
||||
|
|
@ -467,12 +491,12 @@ const upload = ref()
|
|||
|
||||
const imageExtensions = ['JPG', 'JPEG', 'PNG', 'GIF', 'BMP']
|
||||
const documentExtensions = ['PDF', 'DOCX', 'TXT', 'XLS', 'XLSX', 'MD', 'HTML', 'CSV']
|
||||
const videoExtensions: any = ['MP4', 'AVI', 'MKV', 'MOV', 'FLV', 'WMV']
|
||||
const videoExtensions = ['MP4', 'AVI', 'MKV', 'MOV', 'FLV', 'WMV']
|
||||
const audioExtensions = ['MP3', 'WAV', 'OGG', 'AAC', 'M4A']
|
||||
const otherExtensions = ref(['PPT', 'DOC'])
|
||||
|
||||
const getAcceptList = () => {
|
||||
const { image, document, audio, video, other } = props.applicationDetails.file_upload_setting
|
||||
const {image, document, audio, video, other} = props.applicationDetails.file_upload_setting
|
||||
let accepts: any = []
|
||||
if (image) {
|
||||
accepts = [...imageExtensions]
|
||||
|
|
@ -502,15 +526,15 @@ const checkMaxFilesLimit = () => {
|
|||
return (
|
||||
props.applicationDetails.file_upload_setting.maxFiles <=
|
||||
uploadImageList.value.length +
|
||||
uploadDocumentList.value.length +
|
||||
uploadAudioList.value.length +
|
||||
uploadVideoList.value.length +
|
||||
uploadOtherList.value.length
|
||||
uploadDocumentList.value.length +
|
||||
uploadAudioList.value.length +
|
||||
uploadVideoList.value.length +
|
||||
uploadOtherList.value.length
|
||||
)
|
||||
}
|
||||
const filePromisionDict: any = ref<any>({})
|
||||
const uploadFile = async (file: any, fileList: any) => {
|
||||
const { maxFiles, fileLimit } = props.applicationDetails.file_upload_setting
|
||||
const {maxFiles, fileLimit} = props.applicationDetails.file_upload_setting
|
||||
// 单次上传文件数量限制
|
||||
const file_limit_once =
|
||||
uploadImageList.value.length +
|
||||
|
|
@ -518,7 +542,6 @@ const uploadFile = async (file: any, fileList: any) => {
|
|||
uploadAudioList.value.length +
|
||||
uploadVideoList.value.length +
|
||||
uploadOtherList.value.length
|
||||
|
||||
if (file_limit_once >= maxFiles) {
|
||||
MsgWarning(t('chat.uploadFile.limitMessage1') + maxFiles + t('chat.uploadFile.limitMessage2'))
|
||||
fileList.splice(0, fileList.length, ...fileList.slice(0, maxFiles))
|
||||
|
|
@ -546,8 +569,7 @@ const uploadFile = async (file: any, fileList: any) => {
|
|||
const inner = reactive(file)
|
||||
fileAllList.value.push(inner)
|
||||
if (!chatId_context.value) {
|
||||
const res = await props.openChatId()
|
||||
chatId_context.value = res
|
||||
chatId_context.value = await props.openChatId()
|
||||
}
|
||||
const api =
|
||||
props.type === 'debug-ai-chat'
|
||||
|
|
@ -676,7 +698,8 @@ const TouchEnd = (bool?: boolean) => {
|
|||
}
|
||||
}
|
||||
// 取消录音控制台日志
|
||||
Recorder.CLog = function () {}
|
||||
Recorder.CLog = function () {
|
||||
}
|
||||
|
||||
class RecorderManage {
|
||||
recorder?: any
|
||||
|
|
@ -982,16 +1005,16 @@ onMounted(() => {
|
|||
// 获取当前路由信息
|
||||
const route = router.currentRoute.value
|
||||
// 复制query对象
|
||||
const query = { ...route.query }
|
||||
const query = {...route.query}
|
||||
// 删除特定的参数
|
||||
delete query.question
|
||||
const newRoute =
|
||||
Object.entries(query)?.length > 0
|
||||
? route.path +
|
||||
'?' +
|
||||
Object.entries(query)
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join('&')
|
||||
'?' +
|
||||
Object.entries(query)
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join('&')
|
||||
: route.path
|
||||
|
||||
history.pushState(null, '', '/chat' + newRoute)
|
||||
|
|
@ -1003,6 +1026,300 @@ onMounted(() => {
|
|||
})
|
||||
}, 800)
|
||||
})
|
||||
|
||||
const mime_types = {
|
||||
"html": "text/html",
|
||||
"htm": "text/html",
|
||||
"shtml": "text/html",
|
||||
"css": "text/css",
|
||||
"xml": "text/xml",
|
||||
"gif": "image/gif",
|
||||
"jpeg": "image/jpeg",
|
||||
"jpg": "image/jpeg",
|
||||
"js": "application/javascript",
|
||||
"atom": "application/atom+xml",
|
||||
"rss": "application/rss+xml",
|
||||
"mml": "text/mathml",
|
||||
"txt": "text/plain",
|
||||
"jad": "text/vnd.sun.j2me.app-descriptor",
|
||||
"wml": "text/vnd.wap.wml",
|
||||
"htc": "text/x-component",
|
||||
"avif": "image/avif",
|
||||
"png": "image/png",
|
||||
"svg": "image/svg+xml",
|
||||
"svgz": "image/svg+xml",
|
||||
"tif": "image/tiff",
|
||||
"tiff": "image/tiff",
|
||||
"wbmp": "image/vnd.wap.wbmp",
|
||||
"webp": "image/webp",
|
||||
"ico": "image/x-icon",
|
||||
"jng": "image/x-jng",
|
||||
"bmp": "image/x-ms-bmp",
|
||||
"woff": "font/woff",
|
||||
"woff2": "font/woff2",
|
||||
"jar": "application/java-archive",
|
||||
"war": "application/java-archive",
|
||||
"ear": "application/java-archive",
|
||||
"json": "application/json",
|
||||
"hqx": "application/mac-binhex40",
|
||||
"doc": "application/msword",
|
||||
"pdf": "application/pdf",
|
||||
"ps": "application/postscript",
|
||||
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
"eps": "application/postscript",
|
||||
"ai": "application/postscript",
|
||||
"rtf": "application/rtf",
|
||||
"m3u8": "application/vnd.apple.mpegurl",
|
||||
"kml": "application/vnd.google-earth.kml+xml",
|
||||
"kmz": "application/vnd.google-earth.kmz",
|
||||
"xls": "application/vnd.ms-excel",
|
||||
"eot": "application/vnd.ms-fontobject",
|
||||
"ppt": "application/vnd.ms-powerpoint",
|
||||
"odg": "application/vnd.oasis.opendocument.graphics",
|
||||
"odp": "application/vnd.oasis.opendocument.presentation",
|
||||
"ods": "application/vnd.oasis.opendocument.spreadsheet",
|
||||
"odt": "application/vnd.oasis.opendocument.text",
|
||||
"wmlc": "application/vnd.wap.wmlc",
|
||||
"wasm": "application/wasm",
|
||||
"7z": "application/x-7z-compressed",
|
||||
"cco": "application/x-cocoa",
|
||||
"jardiff": "application/x-java-archive-diff",
|
||||
"jnlp": "application/x-java-jnlp-file",
|
||||
"run": "application/x-makeself",
|
||||
"pl": "application/x-perl",
|
||||
"pm": "application/x-perl",
|
||||
"prc": "application/x-pilot",
|
||||
"pdb": "application/x-pilot",
|
||||
"rar": "application/x-rar-compressed",
|
||||
"rpm": "application/x-redhat-package-manager",
|
||||
"sea": "application/x-sea",
|
||||
"swf": "application/x-shockwave-flash",
|
||||
"sit": "application/x-stuffit",
|
||||
"tcl": "application/x-tcl",
|
||||
"tk": "application/x-tcl",
|
||||
"der": "application/x-x509-ca-cert",
|
||||
"pem": "application/x-x509-ca-cert",
|
||||
"crt": "application/x-x509-ca-cert",
|
||||
"xpi": "application/x-xpinstall",
|
||||
"xhtml": "application/xhtml+xml",
|
||||
"xspf": "application/xspf+xml",
|
||||
"zip": "application/zip",
|
||||
"bin": "application/octet-stream",
|
||||
"exe": "application/octet-stream",
|
||||
"dll": "application/octet-stream",
|
||||
"deb": "application/octet-stream",
|
||||
"dmg": "application/octet-stream",
|
||||
"iso": "application/octet-stream",
|
||||
"img": "application/octet-stream",
|
||||
"msi": "application/octet-stream",
|
||||
"msp": "application/octet-stream",
|
||||
"msm": "application/octet-stream",
|
||||
"mid": "audio/midi",
|
||||
"midi": "audio/midi",
|
||||
"kar": "audio/midi",
|
||||
"mp3": "audio/mp3",
|
||||
"ogg": "audio/ogg",
|
||||
"m4a": "audio/x-m4a",
|
||||
"ra": "audio/x-realaudio",
|
||||
"3gpp": "video/3gpp",
|
||||
"3gp": "video/3gpp",
|
||||
"ts": "video/mp2t",
|
||||
"mp4": "video/mp4",
|
||||
"mpeg": "video/mpeg",
|
||||
"mpg": "video/mpeg",
|
||||
"mov": "video/quicktime",
|
||||
"webm": "video/webm",
|
||||
"flv": "video/x-flv",
|
||||
"m4v": "video/x-m4v",
|
||||
"mng": "video/x-mng",
|
||||
"asx": "video/x-ms-asf",
|
||||
"asf": "video/x-ms-asf",
|
||||
"wmv": "video/x-ms-wmv",
|
||||
"avi": "video/x-msvideo",
|
||||
"wav": "audio/wav",
|
||||
"flac": "audio/flac",
|
||||
"aac": "audio/aac",
|
||||
"opus": "audio/opus",
|
||||
"csv": "text/csv",
|
||||
"tsv": "text/tab-separated-values",
|
||||
"ics": "text/calendar",
|
||||
}
|
||||
|
||||
function getExtensionsByMime(mime: string): string[] {
|
||||
return Object.entries(mime_types)
|
||||
.filter(([key, value]) => value === mime)
|
||||
.map(([key]) => key);
|
||||
}
|
||||
|
||||
const fileUploadOptions = computed(() => [
|
||||
{
|
||||
label: t('common.fileUpload.image'),
|
||||
value: 'image',
|
||||
visible: props.applicationDetails.file_upload_setting.image
|
||||
},
|
||||
{
|
||||
label: t('common.fileUpload.document'),
|
||||
value: 'document',
|
||||
visible: props.applicationDetails.file_upload_setting.document
|
||||
},
|
||||
{
|
||||
label: t('common.fileUpload.video'),
|
||||
value: 'video',
|
||||
visible: props.applicationDetails.file_upload_setting.video
|
||||
},
|
||||
{
|
||||
label: t('common.fileUpload.audio'),
|
||||
value: 'audio',
|
||||
visible: props.applicationDetails.file_upload_setting.audio
|
||||
},
|
||||
{
|
||||
label: t('common.fileUpload.other'),
|
||||
value: 'other',
|
||||
visible: props.applicationDetails.file_upload_setting.other
|
||||
}
|
||||
])
|
||||
|
||||
function openUrlSetting() {
|
||||
showURLSetting.value = true
|
||||
const visibleOptions = fileUploadOptions.value.filter(option => option.visible)
|
||||
if (visibleOptions.length > 0) {
|
||||
urlForm.type = visibleOptions[0].value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function saveUrl() {
|
||||
const urls = urlForm.source_url.split('\n')
|
||||
if (urls.length === 0) {
|
||||
MsgWarning(t('chat.uploadFile.invalidUrl'))
|
||||
return
|
||||
}
|
||||
// 允许的 MIME 类型
|
||||
const allowedTypes: Record<string, string[]> = {
|
||||
image: imageExtensions
|
||||
.map(ext => mime_types[ext.toLowerCase() as keyof typeof mime_types])
|
||||
.filter(Boolean) as string[],
|
||||
document: documentExtensions
|
||||
.map(ext => mime_types[ext.toLowerCase() as keyof typeof mime_types])
|
||||
.filter(Boolean) as string[],
|
||||
audio: audioExtensions
|
||||
.map(ext => mime_types[ext.toLowerCase() as keyof typeof mime_types])
|
||||
.filter(Boolean) as string[],
|
||||
video: videoExtensions
|
||||
.map(ext => mime_types[ext.toLowerCase() as keyof typeof mime_types])
|
||||
.filter(Boolean) as string[],
|
||||
other: otherExtensions.value
|
||||
.map(ext => mime_types[ext.toLowerCase() as keyof typeof mime_types])
|
||||
.filter(Boolean) as string[]
|
||||
};
|
||||
|
||||
// 校验 URL 是否有效
|
||||
const validUrls = urls.map(u => u.trim()).filter(u => {
|
||||
try {
|
||||
new URL(u);
|
||||
return u !== '';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (validUrls.length === 0) {
|
||||
MsgWarning(t('chat.uploadFile.invalidUrl'));
|
||||
return;
|
||||
}
|
||||
|
||||
const type = urlForm.type
|
||||
const expectedTypes = allowedTypes[type] || [];
|
||||
const validFiles: any[] = [];
|
||||
|
||||
// 异步校验单个 URL
|
||||
async function processUrl(url: string) {
|
||||
try {
|
||||
const res = await imageApi.getFile({url});
|
||||
if (!res.data) {
|
||||
MsgWarning(url + ' ' + t('chat.uploadFile.invalidUrl'));
|
||||
return;
|
||||
}
|
||||
|
||||
const contentType = res.data['Content-Type'] || '';
|
||||
const contentLength = res.data['Content-Length'];
|
||||
const fileSize = contentLength ? parseInt(contentLength, 10) : 0;
|
||||
|
||||
// 类型校验
|
||||
if (expectedTypes.length > 0 && !expectedTypes.some(type => contentType.includes(type))) {
|
||||
MsgWarning(url + ' ' + t('chat.uploadFile.urlErrorMessage'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 大小校验
|
||||
const {fileLimit} = props.applicationDetails.file_upload_setting;
|
||||
if (fileSize > fileLimit * 1024 * 1024) {
|
||||
MsgWarning(url + ' ' + t('chat.uploadFile.sizeLimit') + fileLimit + 'MB')
|
||||
return;
|
||||
}
|
||||
|
||||
// 文件名处理
|
||||
let fileName = url.substring(url.lastIndexOf('/') + 1);
|
||||
if (!fileName) fileName = `file_${Date.now()}`;
|
||||
if (!fileName.includes('.') && getExtensionsByMime(contentType)) {
|
||||
fileName += '.' + getExtensionsByMime(contentType)[0];
|
||||
}
|
||||
|
||||
const fileItem = {
|
||||
uid: `${Date.now()}_${Math.random()}`,
|
||||
name: fileName,
|
||||
url: url,
|
||||
type: contentType,
|
||||
size: fileSize,
|
||||
status: 'success'
|
||||
};
|
||||
|
||||
// 文档/音频类型需要下载后上传
|
||||
if (type === 'document' || type === 'audio' || type === 'other') {
|
||||
const base64Data = res.data.content;
|
||||
const byteString = atob(base64Data.split(',')[1] || base64Data);
|
||||
const mimeString = base64Data.split(',')[0]?.split(':')[1]?.split(';')[0] || contentType;
|
||||
const ab = new ArrayBuffer(byteString.length);
|
||||
const ia = new Uint8Array(ab);
|
||||
for (let i = 0; i < byteString.length; i++) ia[i] = byteString.charCodeAt(i);
|
||||
|
||||
const fileBlob = new Blob([ab], {type: mimeString});
|
||||
const fileObj = new File([fileBlob], fileName, {type: mimeString});
|
||||
|
||||
const uploadFileItem = {
|
||||
uid: fileItem.uid,
|
||||
name: fileName,
|
||||
size: fileSize,
|
||||
raw: fileObj,
|
||||
status: 'ready',
|
||||
percentage: 0
|
||||
};
|
||||
|
||||
await uploadFile(uploadFileItem, [uploadFileItem]);
|
||||
} else {
|
||||
validFiles.push(reactive(fileItem));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
MsgWarning(`${url} 无法访问`);
|
||||
}
|
||||
}
|
||||
|
||||
// 并行处理所有 URL
|
||||
await Promise.all(validUrls.map(url => processUrl(url)));
|
||||
|
||||
if (validFiles.length > 0) {
|
||||
fileAllList.value.push(...validFiles);
|
||||
}
|
||||
|
||||
showURLSetting.value = false;
|
||||
urlForm.source_url = '';
|
||||
urlForm.type = ''
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.ai-chat {
|
||||
|
|
@ -1101,6 +1418,7 @@ onMounted(() => {
|
|||
bottom: 65px;
|
||||
width: calc(100% - 50px);
|
||||
max-width: 320px;
|
||||
|
||||
.url-upload-button {
|
||||
border-color: var(--el-color-primary);
|
||||
color: var(--el-color-primary);
|
||||
|
|
|
|||
|
|
@ -81,6 +81,11 @@ export default {
|
|||
errorMessage: 'Upload Failed',
|
||||
fileMessage: 'Please process the file content',
|
||||
fileRepeat: 'File already exists',
|
||||
invalidUrl: 'Invalid URL',
|
||||
localUpload: 'Local Upload',
|
||||
urlPlaceholder: 'Please enter URL addresses, one per line',
|
||||
urlTitle: 'URL Address',
|
||||
urlErrorMessage: 'File type does not meet requirements'
|
||||
},
|
||||
executionDetails: {
|
||||
title: 'Execution Details',
|
||||
|
|
|
|||
|
|
@ -61,6 +61,8 @@ export default {
|
|||
other: 'Other',
|
||||
addExtensions: 'Add Extensions',
|
||||
existingExtensionsTip: 'The following extensions already exist',
|
||||
localUpload: 'Local Files',
|
||||
urlUpload: 'URL',
|
||||
},
|
||||
status: {
|
||||
label: 'Status',
|
||||
|
|
|
|||
|
|
@ -79,6 +79,11 @@ export default {
|
|||
errorMessage: '上传失败',
|
||||
fileMessage: '请解析文件内容',
|
||||
fileRepeat: '文件已存在',
|
||||
invalidUrl: '无效的URL',
|
||||
localUpload: '本地上传',
|
||||
urlPlaceholder: '请输入 URL 地址,每行一个地址',
|
||||
urlTitle: 'URL 地址',
|
||||
urlErrorMessage: '文件类型不符合要求'
|
||||
},
|
||||
executionDetails: {
|
||||
title: '执行详情',
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@ export default {
|
|||
other: '其他文件',
|
||||
addExtensions: '添加后缀名',
|
||||
existingExtensionsTip: '文件后缀已存在',
|
||||
localUpload: '本地文件',
|
||||
urlUpload: 'URL 地址',
|
||||
},
|
||||
status: {
|
||||
label: '状态',
|
||||
|
|
|
|||
|
|
@ -79,6 +79,11 @@ export default {
|
|||
fileMessage: '請解析文件內容',
|
||||
errorMessage: '上傳失敗',
|
||||
fileRepeat: '文件已存在',
|
||||
invalidUrl: '无效的URL',
|
||||
localUpload: '本地上傳',
|
||||
urlPlaceholder: '請輸入 URL 地址,每行一個地址',
|
||||
urlTitle: 'URL 地址',
|
||||
urlErrorMessage: '文件类型不符合要求'
|
||||
},
|
||||
executionDetails: {
|
||||
title: '執行詳細',
|
||||
|
|
|
|||
|
|
@ -61,6 +61,8 @@ export default {
|
|||
other: '其他文件',
|
||||
addExtensions: '添加後綴名',
|
||||
existingExtensionsTip: '文件後綴已存在',
|
||||
localUpload: '本地文件',
|
||||
urlUpload: 'URL 地址',
|
||||
},
|
||||
status: {
|
||||
label: '狀態',
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@ const statisticsType = computed(() => [
|
|||
},
|
||||
])
|
||||
|
||||
const topOptions = [{ label: 'TOP 10', value: 10 }]
|
||||
const topOptions = [{ label: 'TOP 10', value: 10 }, { label: 'TOP 20', value: 20}, { label: 'TOP 50', value: 50}, { label: 'TOP 100', value: 100}]
|
||||
const tokenUsageCount = ref(10)
|
||||
const topQuestionsCount = ref(10)
|
||||
const tokenUsageOption = computed(() => {
|
||||
|
|
|
|||
|
|
@ -55,12 +55,12 @@
|
|||
>
|
||||
<div class="flex-between">
|
||||
<div class="flex align-center">
|
||||
<img class="mr-12" src="@/assets/workflow/icon_file-doc.svg" alt="" />
|
||||
<img class="mr-12" src="@/assets/workflow/icon_file-doc.svg" alt=""/>
|
||||
<div>
|
||||
<p class="line-height-22 mt-4">
|
||||
{{ $t('common.fileUpload.document') }}
|
||||
<el-text class="color-secondary"
|
||||
>{{
|
||||
>{{
|
||||
$t(
|
||||
'views.applicationWorkflow.nodes.baseNode.FileUploadSetting.fileUploadType.documentText',
|
||||
)
|
||||
|
|
@ -85,12 +85,12 @@
|
|||
>
|
||||
<div class="flex-between">
|
||||
<div class="flex align-center">
|
||||
<img class="mr-12" src="@/assets/workflow/icon_file-image.svg" alt="" />
|
||||
<img class="mr-12" src="@/assets/workflow/icon_file-image.svg" alt=""/>
|
||||
<div>
|
||||
<p class="line-height-22 mt-4">
|
||||
{{ $t('common.fileUpload.image') }}
|
||||
<el-text class="color-secondary"
|
||||
>{{
|
||||
>{{
|
||||
$t(
|
||||
'views.applicationWorkflow.nodes.baseNode.FileUploadSetting.fileUploadType.imageText',
|
||||
)
|
||||
|
|
@ -116,12 +116,12 @@
|
|||
>
|
||||
<div class="flex-between">
|
||||
<div class="flex align-center">
|
||||
<img class="mr-12" src="@/assets/workflow/icon_file-audio.svg" alt="" />
|
||||
<img class="mr-12" src="@/assets/workflow/icon_file-audio.svg" alt=""/>
|
||||
<div>
|
||||
<p class="line-height-22 mt-4">
|
||||
{{ $t('common.fileUpload.audio') }}
|
||||
<el-text class="color-secondary"
|
||||
>{{
|
||||
>{{
|
||||
$t(
|
||||
'views.applicationWorkflow.nodes.baseNode.FileUploadSetting.fileUploadType.audioText',
|
||||
)
|
||||
|
|
@ -156,7 +156,7 @@
|
|||
<p class="line-height-22 mt-4">
|
||||
{{ $t('common.fileUpload.video') }}
|
||||
<el-text class="color-secondary"
|
||||
>{{
|
||||
>{{
|
||||
$t(
|
||||
'views.applicationWorkflow.nodes.baseNode.FileUploadSetting.fileUploadType.videoText',
|
||||
)
|
||||
|
|
@ -181,12 +181,12 @@
|
|||
>
|
||||
<div class="flex-between">
|
||||
<div class="flex align-center">
|
||||
<img class="mr-12" :width="32" src="@/assets/fileType/unknown-icon.svg" alt="" />
|
||||
<img class="mr-12" :width="32" src="@/assets/fileType/unknown-icon.svg" alt=""/>
|
||||
<div>
|
||||
<p class="line-height-22 mt-4">
|
||||
{{ $t('common.fileUpload.other') }}
|
||||
<el-text class="color-secondary"
|
||||
>{{
|
||||
>{{
|
||||
$t(
|
||||
'views.applicationWorkflow.nodes.baseNode.FileUploadSetting.fileUploadType.otherText',
|
||||
)
|
||||
|
|
@ -230,6 +230,21 @@
|
|||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-form-item>
|
||||
<div class="flex align-center">
|
||||
<el-checkbox
|
||||
v-model="form_data.local_upload"
|
||||
class="mr-16"
|
||||
>
|
||||
{{ $t('common.fileUpload.localUpload') }}
|
||||
</el-checkbox>
|
||||
<el-checkbox
|
||||
v-model="form_data.url_upload"
|
||||
>
|
||||
{{ $t('common.fileUpload.urlUpload') }}
|
||||
</el-checkbox>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
|
@ -246,11 +261,11 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, ref } from 'vue'
|
||||
import type { InputInstance } from 'element-plus'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { MsgWarning } from '@/utils/message'
|
||||
import { t } from '@/locales'
|
||||
import {nextTick, ref} from 'vue'
|
||||
import type {InputInstance} from 'element-plus'
|
||||
import {cloneDeep} from 'lodash'
|
||||
import {MsgWarning} from '@/utils/message'
|
||||
import {t} from '@/locales'
|
||||
|
||||
const emit = defineEmits(['refresh'])
|
||||
const props = defineProps<{ nodeModel: any }>()
|
||||
|
|
@ -276,12 +291,14 @@ const form_data = ref({
|
|||
video: false,
|
||||
other: false,
|
||||
otherExtensions: ['PPT', 'DOC'],
|
||||
local_upload: true,
|
||||
url_upload: false,
|
||||
})
|
||||
|
||||
function open(data: any) {
|
||||
dialogVisible.value = true
|
||||
nextTick(() => {
|
||||
form_data.value = { ...form_data.value, ...data }
|
||||
form_data.value = {...form_data.value, ...data}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { fileURLToPath, URL } from 'node:url'
|
||||
import type { ProxyOptions } from 'vite'
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import {fileURLToPath, URL} from 'node:url'
|
||||
import type {ProxyOptions} from 'vite'
|
||||
import {defineConfig, loadEnv} from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||
import DefineOptions from 'unplugin-vue-define-options/vite'
|
||||
import path from 'path'
|
||||
import { createHtmlPlugin } from 'vite-plugin-html'
|
||||
import {createHtmlPlugin} from 'vite-plugin-html'
|
||||
import fs from 'fs'
|
||||
// import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
const envDir = './env'
|
||||
|
|
@ -69,6 +69,10 @@ export default defineConfig((conf: any) => {
|
|||
target: `http://127.0.0.1:8080`,
|
||||
changeOrigin: true,
|
||||
}
|
||||
proxyConf[`^${ENV.VITE_BASE_PATH}oss\/get_url\/.*$`] = {
|
||||
target: `http://127.0.0.1:8080`,
|
||||
changeOrigin: true,
|
||||
}
|
||||
// 前端静态资源转发到本身
|
||||
proxyConf[ENV.VITE_BASE_PATH] = {
|
||||
target: `http://127.0.0.1:${ENV.VITE_APP_PORT}`,
|
||||
|
|
@ -85,7 +89,7 @@ export default defineConfig((conf: any) => {
|
|||
vue(),
|
||||
vueJsx(),
|
||||
DefineOptions(),
|
||||
createHtmlPlugin({ template: ENV.VITE_ENTRY }),
|
||||
createHtmlPlugin({template: ENV.VITE_ENTRY}),
|
||||
renameHtmlPlugin(`dist${ENV.VITE_BASE_PATH}`, ENV.VITE_ENTRY),
|
||||
],
|
||||
server: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue