diff --git a/web/app/components/base/app-icon-picker/utils.ts b/web/app/components/base/app-icon-picker/utils.ts
index 14c9ae3f28..99154d56da 100644
--- a/web/app/components/base/app-icon-picker/utils.ts
+++ b/web/app/components/base/app-icon-picker/utils.ts
@@ -115,3 +115,52 @@ export default async function getCroppedImg(
}, mimeType)
})
}
+
+export function checkIsAnimatedImage(file) {
+ return new Promise((resolve, reject) => {
+ const fileReader = new FileReader()
+
+ fileReader.onload = function (e) {
+ const arr = new Uint8Array(e.target.result)
+
+ // Check file extension
+ const fileName = file.name.toLowerCase()
+ if (fileName.endsWith('.gif')) {
+ // If file is a GIF, assume it's animated
+ resolve(true)
+ }
+ // Check for WebP signature (RIFF and WEBP)
+ else if (isWebP(arr)) {
+ resolve(checkWebPAnimation(arr)) // Check if it's animated
+ }
+ else {
+ resolve(false) // Not a GIF or WebP
+ }
+ }
+
+ fileReader.onerror = function (err) {
+ reject(err) // Reject the promise on error
+ }
+
+ // Read the file as an array buffer
+ fileReader.readAsArrayBuffer(file)
+ })
+}
+
+// Function to check for WebP signature
+function isWebP(arr) {
+ return (
+ arr[0] === 0x52 && arr[1] === 0x49 && arr[2] === 0x46 && arr[3] === 0x46
+ && arr[8] === 0x57 && arr[9] === 0x45 && arr[10] === 0x42 && arr[11] === 0x50
+ ) // "WEBP"
+}
+
+// Function to check if the WebP is animated (contains ANIM chunk)
+function checkWebPAnimation(arr) {
+ // Search for the ANIM chunk in WebP to determine if it's animated
+ for (let i = 12; i < arr.length - 4; i++) {
+ if (arr[i] === 0x41 && arr[i + 1] === 0x4E && arr[i + 2] === 0x49 && arr[i + 3] === 0x4D)
+ return true // Found animation
+ }
+ return false // No animation chunk found
+}
diff --git a/web/app/components/base/chat/__tests__/__snapshots__/utils.spec.ts.snap b/web/app/components/base/chat/__tests__/__snapshots__/utils.spec.ts.snap
index 070975bfa7..7da09c4529 100644
--- a/web/app/components/base/chat/__tests__/__snapshots__/utils.spec.ts.snap
+++ b/web/app/components/base/chat/__tests__/__snapshots__/utils.spec.ts.snap
@@ -1804,6 +1804,280 @@ exports[`build chat item tree and get thread messages should get thread messages
]
`;
+exports[`build chat item tree and get thread messages should work with partial messages 1`] = `
+[
+ {
+ "children": [
+ {
+ "agent_thoughts": [
+ {
+ "chain_id": null,
+ "created_at": 1726105809,
+ "files": [],
+ "id": "1019cd79-d141-4f9f-880a-fc1441cfd802",
+ "message_id": "cd5affb0-7bc2-4a6f-be7e-25e74595c9dd",
+ "observation": "",
+ "position": 1,
+ "thought": "Sure! My number is 54. Your turn!",
+ "tool": "",
+ "tool_input": "",
+ "tool_labels": {},
+ },
+ ],
+ "children": [
+ {
+ "children": [
+ {
+ "agent_thoughts": [
+ {
+ "chain_id": null,
+ "created_at": 1726105822,
+ "files": [],
+ "id": "0773bec7-b992-4a53-92b2-20ebaeae8798",
+ "message_id": "324bce32-c98c-435d-a66b-bac974ebb5ed",
+ "observation": "",
+ "position": 1,
+ "thought": "My number is 4729. Your turn!",
+ "tool": "",
+ "tool_input": "",
+ "tool_labels": {},
+ },
+ ],
+ "children": [],
+ "content": "My number is 4729. Your turn!",
+ "conversationId": "dd6c9cfd-2656-48ec-bd51-2139c1790d80",
+ "feedbackDisabled": false,
+ "id": "324bce32-c98c-435d-a66b-bac974ebb5ed",
+ "input": {
+ "inputs": {},
+ "query": "3306",
+ },
+ "isAnswer": true,
+ "log": [
+ {
+ "files": [],
+ "role": "user",
+ "text": "Let's play a game, I say a number , and you response me with another bigger, yet randomly number. I'll start first, 38",
+ },
+ {
+ "files": [],
+ "role": "assistant",
+ "text": "Sure! My number is 54. Your turn!",
+ },
+ {
+ "files": [],
+ "role": "user",
+ "text": "3306",
+ },
+ {
+ "files": [],
+ "role": "assistant",
+ "text": "My number is 4729. Your turn!",
+ },
+ ],
+ "message_files": [],
+ "more": {
+ "latency": "1.30",
+ "time": "09/11/2024 09:50 PM",
+ "tokens": 66,
+ },
+ "parentMessageId": "question-324bce32-c98c-435d-a66b-bac974ebb5ed",
+ "siblingIndex": 0,
+ "workflow_run_id": null,
+ },
+ ],
+ "content": "3306",
+ "id": "question-324bce32-c98c-435d-a66b-bac974ebb5ed",
+ "isAnswer": false,
+ "message_files": [],
+ "parentMessageId": "cd5affb0-7bc2-4a6f-be7e-25e74595c9dd",
+ },
+ {
+ "children": [
+ {
+ "agent_thoughts": [
+ {
+ "chain_id": null,
+ "created_at": 1726107812,
+ "files": [],
+ "id": "5ca650f3-982c-4399-8b95-9ea241c76707",
+ "message_id": "684b5396-4e91-4043-88e9-aabe48b21acc",
+ "observation": "",
+ "position": 1,
+ "thought": "My number is 4821. Your turn!",
+ "tool": "",
+ "tool_input": "",
+ "tool_labels": {},
+ },
+ ],
+ "children": [
+ {
+ "children": [
+ {
+ "agent_thoughts": [
+ {
+ "chain_id": null,
+ "created_at": 1726111024,
+ "files": [],
+ "id": "095cacab-afad-4387-a41d-1662578b8b13",
+ "message_id": "19904a7b-7494-4ed8-b72c-1d18668cea8c",
+ "observation": "",
+ "position": 1,
+ "thought": "My number is 1456. Your turn!",
+ "tool": "",
+ "tool_input": "",
+ "tool_labels": {},
+ },
+ ],
+ "children": [],
+ "content": "My number is 1456. Your turn!",
+ "conversationId": "dd6c9cfd-2656-48ec-bd51-2139c1790d80",
+ "feedbackDisabled": false,
+ "id": "19904a7b-7494-4ed8-b72c-1d18668cea8c",
+ "input": {
+ "inputs": {},
+ "query": "1003",
+ },
+ "isAnswer": true,
+ "log": [
+ {
+ "files": [],
+ "role": "user",
+ "text": "Let's play a game, I say a number , and you response me with another bigger, yet randomly number. I'll start first, 38",
+ },
+ {
+ "files": [],
+ "role": "assistant",
+ "text": "Sure! My number is 54. Your turn!",
+ },
+ {
+ "files": [],
+ "role": "user",
+ "text": "3306",
+ },
+ {
+ "files": [],
+ "role": "assistant",
+ "text": "My number is 4821. Your turn!",
+ },
+ {
+ "files": [],
+ "role": "user",
+ "text": "1003",
+ },
+ {
+ "files": [],
+ "role": "assistant",
+ "text": "My number is 1456. Your turn!",
+ },
+ ],
+ "message_files": [],
+ "more": {
+ "latency": "1.38",
+ "time": "09/11/2024 11:17 PM",
+ "tokens": 86,
+ },
+ "parentMessageId": "question-19904a7b-7494-4ed8-b72c-1d18668cea8c",
+ "siblingIndex": 0,
+ "workflow_run_id": null,
+ },
+ ],
+ "content": "1003",
+ "id": "question-19904a7b-7494-4ed8-b72c-1d18668cea8c",
+ "isAnswer": false,
+ "message_files": [],
+ "parentMessageId": "684b5396-4e91-4043-88e9-aabe48b21acc",
+ },
+ ],
+ "content": "My number is 4821. Your turn!",
+ "conversationId": "dd6c9cfd-2656-48ec-bd51-2139c1790d80",
+ "feedbackDisabled": false,
+ "id": "684b5396-4e91-4043-88e9-aabe48b21acc",
+ "input": {
+ "inputs": {},
+ "query": "3306",
+ },
+ "isAnswer": true,
+ "log": [
+ {
+ "files": [],
+ "role": "user",
+ "text": "Let's play a game, I say a number , and you response me with another bigger, yet randomly number. I'll start first, 38",
+ },
+ {
+ "files": [],
+ "role": "assistant",
+ "text": "Sure! My number is 54. Your turn!",
+ },
+ {
+ "files": [],
+ "role": "user",
+ "text": "3306",
+ },
+ {
+ "files": [],
+ "role": "assistant",
+ "text": "My number is 4821. Your turn!",
+ },
+ ],
+ "message_files": [],
+ "more": {
+ "latency": "1.48",
+ "time": "09/11/2024 10:23 PM",
+ "tokens": 66,
+ },
+ "parentMessageId": "question-684b5396-4e91-4043-88e9-aabe48b21acc",
+ "siblingIndex": 1,
+ "workflow_run_id": null,
+ },
+ ],
+ "content": "3306",
+ "id": "question-684b5396-4e91-4043-88e9-aabe48b21acc",
+ "isAnswer": false,
+ "message_files": [],
+ "parentMessageId": "cd5affb0-7bc2-4a6f-be7e-25e74595c9dd",
+ },
+ ],
+ "content": "Sure! My number is 54. Your turn!",
+ "conversationId": "dd6c9cfd-2656-48ec-bd51-2139c1790d80",
+ "feedbackDisabled": false,
+ "id": "cd5affb0-7bc2-4a6f-be7e-25e74595c9dd",
+ "input": {
+ "inputs": {},
+ "query": "Let's play a game, I say a number , and you response me with another bigger, yet randomly number. I'll start first, 38",
+ },
+ "isAnswer": true,
+ "log": [
+ {
+ "files": [],
+ "role": "user",
+ "text": "Let's play a game, I say a number , and you response me with another bigger, yet randomly number. I'll start first, 38",
+ },
+ {
+ "files": [],
+ "role": "assistant",
+ "text": "Sure! My number is 54. Your turn!",
+ },
+ ],
+ "message_files": [],
+ "more": {
+ "latency": "1.52",
+ "time": "09/11/2024 09:50 PM",
+ "tokens": 46,
+ },
+ "parentMessageId": "question-cd5affb0-7bc2-4a6f-be7e-25e74595c9dd",
+ "siblingIndex": 0,
+ "workflow_run_id": null,
+ },
+ ],
+ "content": "Let's play a game, I say a number , and you response me with another bigger, yet randomly number. I'll start first, 38",
+ "id": "question-cd5affb0-7bc2-4a6f-be7e-25e74595c9dd",
+ "isAnswer": false,
+ "message_files": [],
+ },
+]
+`;
+
exports[`build chat item tree and get thread messages should work with real world messages 1`] = `
[
{
diff --git a/web/app/components/base/chat/__tests__/utils.spec.ts b/web/app/components/base/chat/__tests__/utils.spec.ts
index c602ac8a99..1dead1c949 100644
--- a/web/app/components/base/chat/__tests__/utils.spec.ts
+++ b/web/app/components/base/chat/__tests__/utils.spec.ts
@@ -255,4 +255,10 @@ describe('build chat item tree and get thread messages', () => {
const threadMessages6_2 = getThreadMessages(tree6, 'ff4c2b43-48a5-47ad-9dc5-08b34ddba61b')
expect(threadMessages6_2).toMatchSnapshot()
})
+
+ const partialMessages = (realWorldMessages as ChatItemInTree[]).slice(-10)
+ const tree7 = buildChatItemTree(partialMessages)
+ it('should work with partial messages', () => {
+ expect(tree7).toMatchSnapshot()
+ })
})
diff --git a/web/app/components/base/chat/utils.ts b/web/app/components/base/chat/utils.ts
index 16357361cf..61dfaecffc 100644
--- a/web/app/components/base/chat/utils.ts
+++ b/web/app/components/base/chat/utils.ts
@@ -134,6 +134,12 @@ function buildChatItemTree(allMessages: IChatItem[]): ChatItemInTree[] {
}
}
+ // If no messages have parentMessageId=null (indicating a root node),
+ // then we likely have a partial chat history. In this case,
+ // use the first available message as the root node.
+ if (rootNodes.length === 0 && allMessages.length > 0)
+ rootNodes.push(map[allMessages[0]!.id]!)
+
return rootNodes
}
diff --git a/web/app/components/base/file-uploader/constants.ts b/web/app/components/base/file-uploader/constants.ts
index 629fe2566b..a749d73c74 100644
--- a/web/app/components/base/file-uploader/constants.ts
+++ b/web/app/components/base/file-uploader/constants.ts
@@ -3,5 +3,6 @@ export const IMG_SIZE_LIMIT = 10 * 1024 * 1024
export const FILE_SIZE_LIMIT = 15 * 1024 * 1024
export const AUDIO_SIZE_LIMIT = 50 * 1024 * 1024
export const VIDEO_SIZE_LIMIT = 100 * 1024 * 1024
+export const MAX_FILE_UPLOAD_LIMIT = 10
export const FILE_URL_REGEX = /^(https?|ftp):\/\//
diff --git a/web/app/components/base/file-uploader/file-uploader-in-attachment/file-item.tsx b/web/app/components/base/file-uploader/file-uploader-in-attachment/file-item.tsx
index d22d6ff4ec..2a042bab40 100644
--- a/web/app/components/base/file-uploader/file-uploader-in-attachment/file-item.tsx
+++ b/web/app/components/base/file-uploader/file-uploader-in-attachment/file-item.tsx
@@ -1,6 +1,5 @@
import {
memo,
- useMemo,
} from 'react'
import {
RiDeleteBinLine,
@@ -35,17 +34,9 @@ const FileInAttachmentItem = ({
onRemove,
onReUpload,
}: FileInAttachmentItemProps) => {
- const { id, name, type, progress, supportFileType, base64Url, url } = file
- const ext = getFileExtension(name, type)
+ const { id, name, type, progress, supportFileType, base64Url, url, isRemote } = file
+ const ext = getFileExtension(name, type, isRemote)
const isImageFile = supportFileType === SupportUploadFileTypes.image
- const nameArr = useMemo(() => {
- const nameMatch = name.match(/(.+)\.([^.]+)$/)
-
- if (nameMatch)
- return [nameMatch[1], nameMatch[2]]
-
- return [name, '']
- }, [name])
return (
-
{nameArr[0]}
- {
- nameArr[1] && (
-
.{nameArr[1]}
- )
- }
+
{name}
{
@@ -93,7 +79,11 @@ const FileInAttachmentItem = ({
•
)
}
- {formatFileSize(file.size || 0)}
+ {
+ !!file.size && (
+ {formatFileSize(file.size)}
+ )
+ }
diff --git a/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-item.tsx b/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-item.tsx
index 6597373020..a051b89ec1 100644
--- a/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-item.tsx
+++ b/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-item.tsx
@@ -31,8 +31,8 @@ const FileItem = ({
onRemove,
onReUpload,
}: FileItemProps) => {
- const { id, name, type, progress, url } = file
- const ext = getFileExtension(name, type)
+ const { id, name, type, progress, url, isRemote } = file
+ const ext = getFileExtension(name, type, isRemote)
const uploadError = progress === -1
return (
@@ -75,7 +75,9 @@ const FileItem = ({
>
)
}
- {formatFileSize(file.size || 0)}
+ {
+ !!file.size && formatFileSize(file.size)
+ }
{
showDownloadAction && (
diff --git a/web/app/components/base/file-uploader/hooks.ts b/web/app/components/base/file-uploader/hooks.ts
index 942e5d612a..c735754ffe 100644
--- a/web/app/components/base/file-uploader/hooks.ts
+++ b/web/app/components/base/file-uploader/hooks.ts
@@ -18,6 +18,7 @@ import {
AUDIO_SIZE_LIMIT,
FILE_SIZE_LIMIT,
IMG_SIZE_LIMIT,
+ MAX_FILE_UPLOAD_LIMIT,
VIDEO_SIZE_LIMIT,
} from '@/app/components/base/file-uploader/constants'
import { useToastContext } from '@/app/components/base/toast'
@@ -25,7 +26,7 @@ import { TransferMethod } from '@/types/app'
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
import type { FileUpload } from '@/app/components/base/features/types'
import { formatFileSize } from '@/utils/format'
-import { fetchRemoteFileInfo } from '@/service/common'
+import { uploadRemoteFileInfo } from '@/service/common'
import type { FileUploadConfigResponse } from '@/models/common'
export const useFileSizeLimit = (fileUploadConfig?: FileUploadConfigResponse) => {
@@ -33,12 +34,14 @@ export const useFileSizeLimit = (fileUploadConfig?: FileUploadConfigResponse) =>
const docSizeLimit = Number(fileUploadConfig?.file_size_limit) * 1024 * 1024 || FILE_SIZE_LIMIT
const audioSizeLimit = Number(fileUploadConfig?.audio_file_size_limit) * 1024 * 1024 || AUDIO_SIZE_LIMIT
const videoSizeLimit = Number(fileUploadConfig?.video_file_size_limit) * 1024 * 1024 || VIDEO_SIZE_LIMIT
+ const maxFileUploadLimit = Number(fileUploadConfig?.workflow_file_upload_limit) || MAX_FILE_UPLOAD_LIMIT
return {
imgSizeLimit,
docSizeLimit,
audioSizeLimit,
videoSizeLimit,
+ maxFileUploadLimit,
}
}
@@ -49,7 +52,7 @@ export const useFile = (fileConfig: FileUpload) => {
const params = useParams()
const { imgSizeLimit, docSizeLimit, audioSizeLimit, videoSizeLimit } = useFileSizeLimit(fileConfig.fileUploadConfig)
- const checkSizeLimit = (fileType: string, fileSize: number) => {
+ const checkSizeLimit = useCallback((fileType: string, fileSize: number) => {
switch (fileType) {
case SupportUploadFileTypes.image: {
if (fileSize > imgSizeLimit) {
@@ -120,7 +123,7 @@ export const useFile = (fileConfig: FileUpload) => {
return true
}
}
- }
+ }, [audioSizeLimit, docSizeLimit, imgSizeLimit, notify, t, videoSizeLimit])
const handleAddFile = useCallback((newFile: FileEntity) => {
const {
@@ -188,6 +191,17 @@ export const useFile = (fileConfig: FileUpload) => {
}
}, [fileStore, notify, t, handleUpdateFile, params])
+ const startProgressTimer = useCallback((fileId: string) => {
+ const timer = setInterval(() => {
+ const files = fileStore.getState().files
+ const file = files.find(file => file.id === fileId)
+
+ if (file && file.progress < 80 && file.progress >= 0)
+ handleUpdateFile({ ...file, progress: file.progress + 20 })
+ else
+ clearTimeout(timer)
+ }, 200)
+ }, [fileStore, handleUpdateFile])
const handleLoadFileFromLink = useCallback((url: string) => {
const allowedFileTypes = fileConfig.allowed_file_types
@@ -197,19 +211,27 @@ export const useFile = (fileConfig: FileUpload) => {
type: '',
size: 0,
progress: 0,
- transferMethod: TransferMethod.remote_url,
+ transferMethod: TransferMethod.local_file,
supportFileType: '',
url,
+ isRemote: true,
}
handleAddFile(uploadingFile)
+ startProgressTimer(uploadingFile.id)
- fetchRemoteFileInfo(url).then((res) => {
+ uploadRemoteFileInfo(url, !!params.token).then((res) => {
const newFile = {
...uploadingFile,
- type: res.file_type,
- size: res.file_length,
+ type: res.mime_type,
+ size: res.size,
progress: 100,
- supportFileType: getSupportFileType(url, res.file_type, allowedFileTypes?.includes(SupportUploadFileTypes.custom)),
+ supportFileType: getSupportFileType(res.name, res.mime_type, allowedFileTypes?.includes(SupportUploadFileTypes.custom)),
+ uploadedId: res.id,
+ url: res.url,
+ }
+ if (!isAllowedFileExtension(res.name, res.mime_type, fileConfig.allowed_file_types || [], fileConfig.allowed_file_extensions || [])) {
+ notify({ type: 'error', message: t('common.fileUploader.fileExtensionNotSupport') })
+ handleRemoveFile(uploadingFile.id)
}
if (!checkSizeLimit(newFile.supportFileType, newFile.size))
handleRemoveFile(uploadingFile.id)
@@ -219,7 +241,7 @@ export const useFile = (fileConfig: FileUpload) => {
notify({ type: 'error', message: t('common.fileUploader.pasteFileLinkInvalid') })
handleRemoveFile(uploadingFile.id)
})
- }, [checkSizeLimit, handleAddFile, handleUpdateFile, notify, t, handleRemoveFile, fileConfig?.allowed_file_types])
+ }, [checkSizeLimit, handleAddFile, handleUpdateFile, notify, t, handleRemoveFile, fileConfig?.allowed_file_types, fileConfig.allowed_file_extensions, startProgressTimer])
const handleLoadFileFromLinkSuccess = useCallback(() => { }, [])
diff --git a/web/app/components/base/file-uploader/types.ts b/web/app/components/base/file-uploader/types.ts
index ac4584bb4c..285023f0af 100644
--- a/web/app/components/base/file-uploader/types.ts
+++ b/web/app/components/base/file-uploader/types.ts
@@ -29,4 +29,5 @@ export type FileEntity = {
uploadedId?: string
base64Url?: string
url?: string
+ isRemote?: boolean
}
diff --git a/web/app/components/base/file-uploader/utils.ts b/web/app/components/base/file-uploader/utils.ts
index 4c7ef0d89b..eb9199d74b 100644
--- a/web/app/components/base/file-uploader/utils.ts
+++ b/web/app/components/base/file-uploader/utils.ts
@@ -43,10 +43,13 @@ export const fileUpload: FileUpload = ({
})
}
-export const getFileExtension = (fileName: string, fileMimetype: string) => {
+export const getFileExtension = (fileName: string, fileMimetype: string, isRemote?: boolean) => {
if (fileMimetype)
return mime.getExtension(fileMimetype) || ''
+ if (isRemote)
+ return ''
+
if (fileName) {
const fileNamePair = fileName.split('.')
const fileNamePairLength = fileNamePair.length
diff --git a/web/app/components/base/image-uploader/image-list.tsx b/web/app/components/base/image-uploader/image-list.tsx
index 8d5d1a1af5..35f6149b13 100644
--- a/web/app/components/base/image-uploader/image-list.tsx
+++ b/web/app/components/base/image-uploader/image-list.tsx
@@ -133,6 +133,7 @@ const ImageList: FC