diff --git a/.vscode/settings.json b/.vscode/settings.json index b30daf1..45d5fcf 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,7 +4,7 @@ "prettier.enable": false, "editor.formatOnSave": true, "editor.codeActionsOnSave": { - "source.fixAll.eslint": "explicit" + "source.fixAll.eslint": true }, "[python]": { "editor.formatOnType": true @@ -29,4 +29,4 @@ "i18n/lang", "app/api/messages" ] -} \ No newline at end of file +} diff --git a/app/components/base/action-button/index.css b/app/components/base/action-button/index.css deleted file mode 100644 index 2cabe7a..0000000 --- a/app/components/base/action-button/index.css +++ /dev/null @@ -1,45 +0,0 @@ -@tailwind components; - -@layer components { - .action-btn { - @apply inline-flex justify-center items-center cursor-pointer text-text-tertiary hover:text-text-secondary hover:bg-state-base-hover - } - - .action-btn-hover { - @apply bg-state-base-hover - } - - .action-btn-disabled { - @apply cursor-not-allowed - } - - .action-btn-xl { - @apply p-2 w-9 h-9 rounded-lg - } - - .action-btn-l { - @apply p-1.5 w-8 h-8 rounded-lg - } - - /* m is for the regular button */ - .action-btn-m { - @apply p-0.5 w-6 h-6 rounded-lg - } - - .action-btn-xs { - @apply p-0 w-4 h-4 rounded - } - - .action-btn.action-btn-active { - @apply text-text-accent bg-state-accent-active hover:bg-state-accent-active-alt - } - - .action-btn.action-btn-disabled { - @apply text-text-disabled - } - - .action-btn.action-btn-destructive { - @apply text-text-destructive bg-state-destructive-hover - } - -} \ No newline at end of file diff --git a/app/components/base/action-button/index.tsx b/app/components/base/action-button/index.tsx deleted file mode 100644 index c90d1a8..0000000 --- a/app/components/base/action-button/index.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import type { CSSProperties } from 'react' -import React from 'react' -import { type VariantProps, cva } from 'class-variance-authority' -import classNames from '@/utils/classnames' - -enum ActionButtonState { - Destructive = 'destructive', - Active = 'active', - Disabled = 'disabled', - Default = '', - Hover = 'hover', -} - -const actionButtonVariants = cva( - 'action-btn', - { - variants: { - size: { - xs: 'action-btn-xs', - m: 'action-btn-m', - l: 'action-btn-l', - xl: 'action-btn-xl', - }, - }, - defaultVariants: { - size: 'm', - }, - }, -) - -export type ActionButtonProps = { - size?: 'xs' | 's' | 'm' | 'l' | 'xl' - state?: ActionButtonState - styleCss?: CSSProperties -} & React.ButtonHTMLAttributes & VariantProps - -function getActionButtonState(state: ActionButtonState) { - switch (state) { - case ActionButtonState.Destructive: - return 'action-btn-destructive' - case ActionButtonState.Active: - return 'action-btn-active' - case ActionButtonState.Disabled: - return 'action-btn-disabled' - case ActionButtonState.Hover: - return 'action-btn-hover' - default: - return '' - } -} - -const ActionButton = React.forwardRef( - ({ className, size, state = ActionButtonState.Default, styleCss, children, ...props }, ref) => { - return ( - - ) - }, -) -ActionButton.displayName = 'ActionButton' - -export default ActionButton -export { ActionButton, ActionButtonState, actionButtonVariants } diff --git a/app/components/base/button/index.tsx b/app/components/base/button/index.tsx index 518a849..33aebb6 100644 --- a/app/components/base/button/index.tsx +++ b/app/components/base/button/index.tsx @@ -21,9 +21,6 @@ const Button: FC = ({ }) => { let style = 'cursor-pointer' switch (type) { - case 'link': - style = disabled ? 'border-solid border border-gray-200 bg-gray-200 cursor-not-allowed text-gray-800' : 'border-solid border border-gray-200 cursor-pointer text-blue-600 bg-white hover:shadow-sm hover:border-gray-300' - break case 'primary': style = (disabled || loading) ? 'bg-primary-600/75 cursor-not-allowed text-white' : 'bg-primary-600 hover:bg-primary-600/75 hover:shadow-md cursor-pointer text-white hover:shadow-sm' break diff --git a/app/components/base/file-uploader-in-attachment/constants.ts b/app/components/base/file-uploader-in-attachment/constants.ts deleted file mode 100644 index 47ea377..0000000 --- a/app/components/base/file-uploader-in-attachment/constants.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { SupportUploadFileTypes } from './types' -// fallback for file size limit of dify_config -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):\/\// - -export const FILE_EXTS: Record = { - [SupportUploadFileTypes.image]: ['JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG'], - [SupportUploadFileTypes.document]: ['TXT', 'MD', 'MDX', 'MARKDOWN', 'PDF', 'HTML', 'XLSX', 'XLS', 'DOC', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB'], - [SupportUploadFileTypes.audio]: ['MP3', 'M4A', 'WAV', 'AMR', 'MPGA'], - [SupportUploadFileTypes.video]: ['MP4', 'MOV', 'MPEG', 'WEBM'], -} diff --git a/app/components/base/file-uploader-in-attachment/file-from-link-or-local/index.tsx b/app/components/base/file-uploader-in-attachment/file-from-link-or-local/index.tsx deleted file mode 100644 index 5da52a6..0000000 --- a/app/components/base/file-uploader-in-attachment/file-from-link-or-local/index.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { - memo, - useState, -} from 'react' -import { useTranslation } from 'react-i18next' -import { RiUploadCloud2Line } from '@remixicon/react' -import FileInput from '../file-input' -import { useFile } from '../hooks' -import { useStore } from '../store' -import { FILE_URL_REGEX } from '../constants' -import type { FileUpload } from '../types' -import { - PortalToFollowElem, - PortalToFollowElemContent, - PortalToFollowElemTrigger, -} from '@/app/components/base/portal-to-follow-elem' -import Button from '@/app/components/base/button' -import cn from '@/utils/classnames' - -type FileFromLinkOrLocalProps = { - showFromLink?: boolean - showFromLocal?: boolean - trigger: (open: boolean) => React.ReactNode - fileConfig: FileUpload -} -const FileFromLinkOrLocal = ({ - showFromLink = true, - showFromLocal = true, - trigger, - fileConfig, -}: FileFromLinkOrLocalProps) => { - const { t } = useTranslation() - const files = useStore(s => s.files) - const [open, setOpen] = useState(false) - const [url, setUrl] = useState('') - const [showError, setShowError] = useState(false) - const { handleLoadFileFromLink } = useFile(fileConfig) - const disabled = !!fileConfig.number_limits && files.length >= fileConfig.number_limits - - const handleSaveUrl = () => { - if (!url) - return - - if (!FILE_URL_REGEX.test(url)) { - setShowError(true) - return - } - handleLoadFileFromLink(url) - setUrl('') - } - - return ( - - setOpen(v => !v)} asChild> - {trigger(open)} - - -
- { - showFromLink && ( - <> -
- { - setShowError(false) - setUrl(e.target.value.trim()) - }} - disabled={disabled} - /> - -
- { - showError && ( -
- {t('common.fileUploader.pasteFileLinkInvalid')} -
- ) - } - - ) - } - { - showFromLink && showFromLocal && ( -
-
- OR -
-
- ) - } - { - showFromLocal && ( - - ) - } -
- - - ) -} - -export default memo(FileFromLinkOrLocal) diff --git a/app/components/base/file-uploader-in-attachment/file-image-render.tsx b/app/components/base/file-uploader-in-attachment/file-image-render.tsx deleted file mode 100644 index d613505..0000000 --- a/app/components/base/file-uploader-in-attachment/file-image-render.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import cn from '@/utils/classnames' - -type FileImageRenderProps = { - imageUrl: string - className?: string - alt?: string - onLoad?: () => void - onError?: () => void - showDownloadAction?: boolean -} -const FileImageRender = ({ - imageUrl, - className, - alt, - onLoad, - onError, - showDownloadAction, -}: FileImageRenderProps) => { - return ( -
- {alt -
- ) -} - -export default FileImageRender diff --git a/app/components/base/file-uploader-in-attachment/file-input.tsx b/app/components/base/file-uploader-in-attachment/file-input.tsx deleted file mode 100644 index d4cca19..0000000 --- a/app/components/base/file-uploader-in-attachment/file-input.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { useFile } from './hooks' -import { useStore } from './store' -import type { FileUpload } from './types' -import { FILE_EXTS } from './constants' -import { SupportUploadFileTypes } from './types' - -type FileInputProps = { - fileConfig: FileUpload -} -const FileInput = ({ - fileConfig, -}: FileInputProps) => { - const files = useStore(s => s.files) - const { handleLocalFileUpload } = useFile(fileConfig) - const handleChange = (e: React.ChangeEvent) => { - const targetFiles = e.target.files - - if (targetFiles) { - if (fileConfig.number_limits) { - for (let i = 0; i < targetFiles.length; i++) { - if (i + 1 + files.length <= fileConfig.number_limits) - handleLocalFileUpload(targetFiles[i]) - } - } - else { - handleLocalFileUpload(targetFiles[0]) - } - } - } - - const allowedFileTypes = fileConfig.allowed_file_types - const isCustom = allowedFileTypes?.includes(SupportUploadFileTypes.custom) - const exts = isCustom ? (fileConfig.allowed_file_extensions || []) : (allowedFileTypes?.map(type => FILE_EXTS[type]) || []).flat().map(item => `.${item}`) - const accept = exts.join(',') - - return ( - ((e.target as HTMLInputElement).value = '')} - type='file' - onChange={handleChange} - accept={accept} - disabled={!!(fileConfig.number_limits && files.length >= fileConfig?.number_limits)} - multiple={!!fileConfig.number_limits && fileConfig.number_limits > 1} - /> - ) -} - -export default FileInput diff --git a/app/components/base/file-uploader-in-attachment/file-item.tsx b/app/components/base/file-uploader-in-attachment/file-item.tsx deleted file mode 100644 index 4517251..0000000 --- a/app/components/base/file-uploader-in-attachment/file-item.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import { - memo, - useState, -} from 'react' -import { - RiDeleteBinLine, - RiDownloadLine, - RiEyeLine, -} from '@remixicon/react' -import FileTypeIcon from './file-type-icon' -import FileImageRender from './file-image-render' -import type { FileEntity } from './types' -import { - downloadFile, - fileIsUploaded, - getFileAppearanceType, - getFileExtension, -} from './utils' -import { SupportUploadFileTypes } from './types' -import ActionButton from '@/app/components/base/action-button' -import ProgressCircle from '@/app/components/base/progress-bar/progress-circle' -import { formatFileSize } from '@/utils/format' -import cn from '@/utils/classnames' -import ReplayLine from '@/app/components/base/icons/other/ReplayLine' -import ImagePreview from '@/app/components/base/image-uploader/image-preview' - -type FileInAttachmentItemProps = { - file: FileEntity - showDeleteAction?: boolean - showDownloadAction?: boolean - onRemove?: (fileId: string) => void - onReUpload?: (fileId: string) => void - canPreview?: boolean -} -const FileInAttachmentItem = ({ - file, - showDeleteAction, - showDownloadAction = true, - onRemove, - onReUpload, - canPreview, -}: FileInAttachmentItemProps) => { - const { id, name, type, progress, supportFileType, base64Url, url, isRemote } = file - const ext = getFileExtension(name, type, isRemote) - const isImageFile = supportFileType === SupportUploadFileTypes.image - const [imagePreviewUrl, setImagePreviewUrl] = useState('') - return ( - <> -
-
- { - isImageFile && ( - - ) - } - { - !isImageFile && ( - - ) - } -
-
-
-
{name}
-
-
- { - ext && ( - {ext.toLowerCase()} - ) - } - { - ext && ( - - ) - } - { - !!file.size && ( - {formatFileSize(file.size)} - ) - } -
-
-
- { - progress >= 0 && !fileIsUploaded(file) && ( - - ) - } - { - progress === -1 && ( - onReUpload?.(id)} - > - - - ) - } - { - showDeleteAction && ( - onRemove?.(id)}> - - - ) - } - { - canPreview && isImageFile && ( - setImagePreviewUrl(url || '')}> - - - ) - } - { - showDownloadAction && ( - { - e.stopPropagation() - downloadFile(url || base64Url || '', name) - }}> - - - ) - } -
-
- { - imagePreviewUrl && canPreview && ( - setImagePreviewUrl('')} - /> - ) - } - - ) -} - -export default memo(FileInAttachmentItem) diff --git a/app/components/base/file-uploader-in-attachment/file-type-icon.tsx b/app/components/base/file-uploader-in-attachment/file-type-icon.tsx deleted file mode 100644 index 08d0131..0000000 --- a/app/components/base/file-uploader-in-attachment/file-type-icon.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { memo } from 'react' -import { - RiFile3Fill, - RiFileCodeFill, - RiFileExcelFill, - RiFileGifFill, - RiFileImageFill, - RiFileMusicFill, - RiFilePdf2Fill, - RiFilePpt2Fill, - RiFileTextFill, - RiFileVideoFill, - RiFileWordFill, - RiMarkdownFill, -} from '@remixicon/react' -import { FileAppearanceTypeEnum } from './types' -import type { FileAppearanceType } from './types' -import cn from '@/utils/classnames' - -const FILE_TYPE_ICON_MAP = { - [FileAppearanceTypeEnum.pdf]: { - component: RiFilePdf2Fill, - color: 'text-[#EA3434]', - }, - [FileAppearanceTypeEnum.image]: { - component: RiFileImageFill, - color: 'text-[#00B2EA]', - }, - [FileAppearanceTypeEnum.video]: { - component: RiFileVideoFill, - color: 'text-[#844FDA]', - }, - [FileAppearanceTypeEnum.audio]: { - component: RiFileMusicFill, - color: 'text-[#FF3093]', - }, - [FileAppearanceTypeEnum.document]: { - component: RiFileTextFill, - color: 'text-[#6F8BB5]', - }, - [FileAppearanceTypeEnum.code]: { - component: RiFileCodeFill, - color: 'text-[#BCC0D1]', - }, - [FileAppearanceTypeEnum.markdown]: { - component: RiMarkdownFill, - color: 'text-[#309BEC]', - }, - [FileAppearanceTypeEnum.custom]: { - component: RiFile3Fill, - color: 'text-[#BCC0D1]', - }, - [FileAppearanceTypeEnum.excel]: { - component: RiFileExcelFill, - color: 'text-[#01AC49]', - }, - [FileAppearanceTypeEnum.word]: { - component: RiFileWordFill, - color: 'text-[#2684FF]', - }, - [FileAppearanceTypeEnum.ppt]: { - component: RiFilePpt2Fill, - color: 'text-[#FF650F]', - }, - [FileAppearanceTypeEnum.gif]: { - component: RiFileGifFill, - color: 'text-[#00B2EA]', - }, -} -type FileTypeIconProps = { - type: FileAppearanceType - size?: 'sm' | 'lg' | 'md' - className?: string -} -const SizeMap = { - sm: 'w-4 h-4', - md: 'w-5 h-5', - lg: 'w-6 h-6', -} -const FileTypeIcon = ({ - type, - size = 'sm', - className, -}: FileTypeIconProps) => { - const Icon = FILE_TYPE_ICON_MAP[type]?.component || FILE_TYPE_ICON_MAP[FileAppearanceTypeEnum.document].component - const color = FILE_TYPE_ICON_MAP[type]?.color || FILE_TYPE_ICON_MAP[FileAppearanceTypeEnum.document].color - - return -} - -export default memo(FileTypeIcon) diff --git a/app/components/base/file-uploader-in-attachment/hooks.ts b/app/components/base/file-uploader-in-attachment/hooks.ts deleted file mode 100644 index bd49b52..0000000 --- a/app/components/base/file-uploader-in-attachment/hooks.ts +++ /dev/null @@ -1,368 +0,0 @@ -import type { ClipboardEvent } from 'react' -import { - useCallback, - useState, -} from 'react' -import { useParams } from 'next/navigation' -import produce from 'immer' -import { v4 as uuid4 } from 'uuid' -import { useTranslation } from 'react-i18next' -import { noop } from 'lodash-es' -import type { FileEntity, FileUpload, FileUploadConfigResponse } from './types' -import { useFileStore } from './store' -import { - fileUpload, - getSupportFileType, - isAllowedFileExtension, -} from './utils' -import { - AUDIO_SIZE_LIMIT, - FILE_SIZE_LIMIT, - IMG_SIZE_LIMIT, - MAX_FILE_UPLOAD_LIMIT, - VIDEO_SIZE_LIMIT, -} from './constants' -import { SupportUploadFileTypes } from './types' -import { useToastContext } from '@/app/components/base/toast' -import { TransferMethod } from '@/types/app' -import { formatFileSize } from '@/utils/format' - -const uploadRemoteFileInfo = () => { - console.log('TODO') -} - -export const useFileSizeLimit = (fileUploadConfig?: FileUploadConfigResponse) => { - const imgSizeLimit = Number(fileUploadConfig?.image_file_size_limit) * 1024 * 1024 || IMG_SIZE_LIMIT - 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, - } -} - -export const useFile = (fileConfig: FileUpload) => { - const { t } = useTranslation() - const { notify } = useToastContext() - const fileStore = useFileStore() - const params = useParams() - const { imgSizeLimit, docSizeLimit, audioSizeLimit, videoSizeLimit } = useFileSizeLimit(fileConfig.fileUploadConfig) - - const checkSizeLimit = useCallback((fileType: string, fileSize: number) => { - switch (fileType) { - case SupportUploadFileTypes.image: { - if (fileSize > imgSizeLimit) { - notify({ - type: 'error', - message: t('common.fileUploader.uploadFromComputerLimit', { - type: SupportUploadFileTypes.image, - size: formatFileSize(imgSizeLimit), - }), - }) - return false - } - return true - } - case SupportUploadFileTypes.document: { - if (fileSize > docSizeLimit) { - notify({ - type: 'error', - message: t('common.fileUploader.uploadFromComputerLimit', { - type: SupportUploadFileTypes.document, - size: formatFileSize(docSizeLimit), - }), - }) - return false - } - return true - } - case SupportUploadFileTypes.audio: { - if (fileSize > audioSizeLimit) { - notify({ - type: 'error', - message: t('common.fileUploader.uploadFromComputerLimit', { - type: SupportUploadFileTypes.audio, - size: formatFileSize(audioSizeLimit), - }), - }) - return false - } - return true - } - case SupportUploadFileTypes.video: { - if (fileSize > videoSizeLimit) { - notify({ - type: 'error', - message: t('common.fileUploader.uploadFromComputerLimit', { - type: SupportUploadFileTypes.video, - size: formatFileSize(videoSizeLimit), - }), - }) - return false - } - return true - } - case SupportUploadFileTypes.custom: { - if (fileSize > docSizeLimit) { - notify({ - type: 'error', - message: t('common.fileUploader.uploadFromComputerLimit', { - type: SupportUploadFileTypes.document, - size: formatFileSize(docSizeLimit), - }), - }) - return false - } - return true - } - default: { - return true - } - } - }, [audioSizeLimit, docSizeLimit, imgSizeLimit, notify, t, videoSizeLimit]) - - const handleAddFile = useCallback((newFile: FileEntity) => { - const { - files, - setFiles, - } = fileStore.getState() - - const newFiles = produce(files, (draft) => { - draft.push(newFile) - }) - setFiles(newFiles) - }, [fileStore]) - - const handleUpdateFile = useCallback((newFile: FileEntity) => { - const { - files, - setFiles, - } = fileStore.getState() - - const newFiles = produce(files, (draft) => { - const index = draft.findIndex(file => file.id === newFile.id) - - if (index > -1) - draft[index] = newFile - }) - setFiles(newFiles) - }, [fileStore]) - - const handleRemoveFile = useCallback((fileId: string) => { - const { - files, - setFiles, - } = fileStore.getState() - - const newFiles = files.filter(file => file.id !== fileId) - setFiles(newFiles) - }, [fileStore]) - - const handleReUploadFile = useCallback((fileId: string) => { - const { - files, - setFiles, - } = fileStore.getState() - const index = files.findIndex(file => file.id === fileId) - - if (index > -1) { - const uploadingFile = files[index] - const newFiles = produce(files, (draft) => { - draft[index].progress = 0 - }) - setFiles(newFiles) - fileUpload({ - file: uploadingFile.originalFile!, - onProgressCallback: (progress) => { - handleUpdateFile({ ...uploadingFile, progress }) - }, - onSuccessCallback: (res) => { - handleUpdateFile({ ...uploadingFile, uploadedId: res.id, progress: 100 }) - }, - onErrorCallback: () => { - notify({ type: 'error', message: t('common.fileUploader.uploadFromComputerUploadError') }) - handleUpdateFile({ ...uploadingFile, progress: -1 }) - }, - }) - } - }, [fileStore, notify, t, handleUpdateFile]) - - 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 - - const uploadingFile = { - id: uuid4(), - name: url, - type: '', - size: 0, - progress: 0, - transferMethod: TransferMethod.remote_url, - supportFileType: '', - url, - isRemote: true, - } - handleAddFile(uploadingFile) - startProgressTimer(uploadingFile.id) - - uploadRemoteFileInfo(url, !!params.token).then((res) => { - const newFile = { - ...uploadingFile, - type: res.mime_type, - size: res.size, - progress: 100, - 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) - else - handleUpdateFile(newFile) - }).catch(() => { - notify({ type: 'error', message: t('common.fileUploader.pasteFileLinkInvalid') }) - handleRemoveFile(uploadingFile.id) - }) - }, [checkSizeLimit, handleAddFile, handleUpdateFile, notify, t, handleRemoveFile, fileConfig?.allowed_file_types, fileConfig.allowed_file_extensions, startProgressTimer, params.token]) - - const handleLoadFileFromLinkSuccess = useCallback(noop, []) - - const handleLoadFileFromLinkError = useCallback(noop, []) - - const handleClearFiles = useCallback(() => { - const { - setFiles, - } = fileStore.getState() - setFiles([]) - }, [fileStore]) - - const handleLocalFileUpload = useCallback((file: File) => { - if (!isAllowedFileExtension(file.name, file.type, fileConfig.allowed_file_types || [], fileConfig.allowed_file_extensions || [])) { - notify({ type: 'error', message: t('common.fileUploader.fileExtensionNotSupport') }) - return - } - const allowedFileTypes = fileConfig.allowed_file_types - const fileType = getSupportFileType(file.name, file.type, allowedFileTypes?.includes(SupportUploadFileTypes.custom)) - if (!checkSizeLimit(fileType, file.size)) - return - - const reader = new FileReader() - const isImage = file.type.startsWith('image') - - reader.addEventListener( - 'load', - () => { - const uploadingFile = { - id: uuid4(), - name: file.name, - type: file.type, - size: file.size, - progress: 0, - transferMethod: TransferMethod.local_file, - supportFileType: getSupportFileType(file.name, file.type, allowedFileTypes?.includes(SupportUploadFileTypes.custom)), - originalFile: file, - base64Url: isImage ? reader.result as string : '', - } - handleAddFile(uploadingFile) - fileUpload({ - file: uploadingFile.originalFile, - onProgressCallback: (progress) => { - handleUpdateFile({ ...uploadingFile, progress }) - }, - onSuccessCallback: (res) => { - handleUpdateFile({ ...uploadingFile, uploadedId: res.id, progress: 100 }) - }, - onErrorCallback: () => { - notify({ type: 'error', message: t('common.fileUploader.uploadFromComputerUploadError') }) - handleUpdateFile({ ...uploadingFile, progress: -1 }) - }, - }) - }, - false, - ) - reader.addEventListener( - 'error', - () => { - notify({ type: 'error', message: t('common.fileUploader.uploadFromComputerReadError') }) - }, - false, - ) - reader.readAsDataURL(file) - }, [checkSizeLimit, notify, t, handleAddFile, handleUpdateFile, params.token, fileConfig?.allowed_file_types, fileConfig?.allowed_file_extensions]) - - const handleClipboardPasteFile = useCallback((e: ClipboardEvent) => { - const file = e.clipboardData?.files[0] - const text = e.clipboardData?.getData('text/plain') - if (file && !text) { - e.preventDefault() - handleLocalFileUpload(file) - } - }, [handleLocalFileUpload]) - - const [isDragActive, setIsDragActive] = useState(false) - const handleDragFileEnter = useCallback((e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - setIsDragActive(true) - }, []) - - const handleDragFileOver = useCallback((e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - }, []) - - const handleDragFileLeave = useCallback((e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - setIsDragActive(false) - }, []) - - const handleDropFile = useCallback((e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - setIsDragActive(false) - - const file = e.dataTransfer.files[0] - - if (file) - handleLocalFileUpload(file) - }, [handleLocalFileUpload]) - - return { - handleAddFile, - handleUpdateFile, - handleRemoveFile, - handleReUploadFile, - handleLoadFileFromLink, - handleLoadFileFromLinkSuccess, - handleLoadFileFromLinkError, - handleClearFiles, - handleLocalFileUpload, - handleClipboardPasteFile, - isDragActive, - handleDragFileEnter, - handleDragFileOver, - handleDragFileLeave, - handleDropFile, - } -} diff --git a/app/components/base/file-uploader-in-attachment/index.tsx b/app/components/base/file-uploader-in-attachment/index.tsx deleted file mode 100644 index 697f65f..0000000 --- a/app/components/base/file-uploader-in-attachment/index.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { - useCallback, -} from 'react' -import { - RiLink, - RiUploadCloud2Line, -} from '@remixicon/react' -import { useTranslation } from 'react-i18next' -import { useFile } from './hooks' -import type { FileEntity, FileUpload } from './types' -import FileFromLinkOrLocal from './file-from-link-or-local' -import { - FileContextProvider, - useStore, -} from './store' -import FileInput from './file-input' -import FileItem from './file-item' -import Button from '@/app/components/base/button' -import cn from '@/utils/classnames' -import { TransferMethod } from '@/types/app' - -type Option = { - value: string - label: string - icon: JSX.Element -} -type FileUploaderInAttachmentProps = { - fileConfig: FileUpload -} -const FileUploaderInAttachment = ({ - fileConfig, -}: FileUploaderInAttachmentProps) => { - const { t } = useTranslation() - const files = useStore(s => s.files) - const { - handleRemoveFile, - handleReUploadFile, - } = useFile(fileConfig) - const options = [ - { - value: TransferMethod.local_file, - label: t('common.fileUploader.uploadFromComputer'), - icon: , - }, - { - value: TransferMethod.remote_url, - label: t('common.fileUploader.pasteFileLink'), - icon: , - }, - ] - - const renderButton = useCallback((option: Option, open?: boolean) => { - return ( - - ) - }, [fileConfig, files.length]) - const renderTrigger = useCallback((option: Option) => { - return (open: boolean) => renderButton(option, open) - }, [renderButton]) - const renderOption = useCallback((option: Option) => { - if (option.value === TransferMethod.local_file && fileConfig?.allowed_file_upload_methods?.includes(TransferMethod.local_file)) - return renderButton(option) - - if (option.value === TransferMethod.remote_url && fileConfig?.allowed_file_upload_methods?.includes(TransferMethod.remote_url)) { - return ( - - ) - } - }, [renderButton, renderTrigger, fileConfig]) - - return ( -
-
- {options.map(renderOption)} -
-
- { - files.map(file => ( - handleRemoveFile(file.id)} - onReUpload={() => handleReUploadFile(file.id)} - /> - )) - } -
-
- ) -} - -type FileUploaderInAttachmentWrapperProps = { - value?: FileEntity[] - onChange: (files: FileEntity[]) => void - fileConfig: FileUpload -} -const FileUploaderInAttachmentWrapper = ({ - value, - onChange, - fileConfig, -}: FileUploaderInAttachmentWrapperProps) => { - return ( - - - - ) -} - -export default FileUploaderInAttachmentWrapper diff --git a/app/components/base/file-uploader-in-attachment/store.tsx b/app/components/base/file-uploader-in-attachment/store.tsx deleted file mode 100644 index cddfdf6..0000000 --- a/app/components/base/file-uploader-in-attachment/store.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { - createContext, - useContext, - useRef, -} from 'react' -import { - create, - useStore as useZustandStore, -} from 'zustand' -import type { - FileEntity, -} from './types' - -type Shape = { - files: FileEntity[] - setFiles: (files: FileEntity[]) => void -} - -export const createFileStore = ( - value: FileEntity[] = [], - onChange?: (files: FileEntity[]) => void, -) => { - return create(set => ({ - files: value ? [...value] : [], - setFiles: (files) => { - set({ files }) - onChange?.(files) - }, - })) -} - -type FileStore = ReturnType -export const FileContext = createContext(null) - -export function useStore(selector: (state: Shape) => T): T { - const store = useContext(FileContext) - if (!store) - throw new Error('Missing FileContext.Provider in the tree') - - return useZustandStore(store, selector) -} - -export const useFileStore = () => { - return useContext(FileContext)! -} - -type FileProviderProps = { - children: React.ReactNode - value?: FileEntity[] - onChange?: (files: FileEntity[]) => void -} -export const FileContextProvider = ({ - children, - value, - onChange, -}: FileProviderProps) => { - const storeRef = useRef(undefined) - - if (!storeRef.current) - storeRef.current = createFileStore(value, onChange) - - return ( - - {children} - - ) -} diff --git a/app/components/base/file-uploader-in-attachment/types.ts b/app/components/base/file-uploader-in-attachment/types.ts deleted file mode 100644 index 384355e..0000000 --- a/app/components/base/file-uploader-in-attachment/types.ts +++ /dev/null @@ -1,83 +0,0 @@ -import type { TransferMethod } from '@/types/app' - -export enum FileAppearanceTypeEnum { - image = 'image', - video = 'video', - audio = 'audio', - document = 'document', - code = 'code', - pdf = 'pdf', - markdown = 'markdown', - excel = 'excel', - word = 'word', - ppt = 'ppt', - gif = 'gif', - custom = 'custom', -} - -export type FileAppearanceType = keyof typeof FileAppearanceTypeEnum - -export type FileEntity = { - id: string - name: string - size: number - type: string - progress: number - transferMethod: TransferMethod - supportFileType: string - originalFile?: File - uploadedId?: string - base64Url?: string - url?: string - isRemote?: boolean -} - -export type EnabledOrDisabled = { - enabled?: boolean -} - -export enum Resolution { - low = 'low', - high = 'high', -} - -export type FileUploadConfigResponse = { - batch_count_limit: number - image_file_size_limit?: number | string // default is 10MB - file_size_limit: number // default is 15MB - audio_file_size_limit?: number // default is 50MB - video_file_size_limit?: number // default is 100MB - workflow_file_upload_limit?: number // default is 10 -} - -export type FileUpload = { - image?: EnabledOrDisabled & { - detail?: Resolution - number_limits?: number - transfer_methods?: TransferMethod[] - } - allowed_file_types?: string[] - allowed_file_extensions?: string[] - allowed_file_upload_methods?: TransferMethod[] - number_limits?: number - fileUploadConfig?: FileUploadConfigResponse -} & EnabledOrDisabled - -export enum SupportUploadFileTypes { - image = 'image', - document = 'document', - audio = 'audio', - video = 'video', - custom = 'custom', -} - -export type FileResponse = { - related_id: string - extension: string - filename: string - size: number - mime_type: string - transfer_method: TransferMethod - type: string - url: string -} diff --git a/app/components/base/file-uploader-in-attachment/utils.ts b/app/components/base/file-uploader-in-attachment/utils.ts deleted file mode 100644 index b63532e..0000000 --- a/app/components/base/file-uploader-in-attachment/utils.ts +++ /dev/null @@ -1,194 +0,0 @@ -import mime from 'mime' -import { FileAppearanceTypeEnum, SupportUploadFileTypes } from './types' -import type { FileEntity, FileResponse } from './types' -import { FILE_EXTS } from './constants' -import { upload } from '@/service/base' -import { TransferMethod } from '@/types/app' - -type FileUploadParams = { - file: File - onProgressCallback: (progress: number) => void - onSuccessCallback: (res: { id: string }) => void - onErrorCallback: () => void -} -type FileUpload = (v: FileUploadParams, isPublic?: boolean, url?: string) => void -export const fileUpload: FileUpload = ({ - file, - onProgressCallback, - onSuccessCallback, - onErrorCallback, -}) => { - const formData = new FormData() - formData.append('file', file) - const onProgress = (e: ProgressEvent) => { - if (e.lengthComputable) { - const percent = Math.floor(e.loaded / e.total * 100) - onProgressCallback(percent) - } - } - - upload({ - xhr: new XMLHttpRequest(), - data: formData, - onprogress: onProgress, - }) - .then((res: { id: string }) => { - onSuccessCallback(res) - }) - .catch(() => { - onErrorCallback() - }) -} - -export const getFileExtension = (fileName: string, fileMimetype: string, isRemote?: boolean) => { - let extension = '' - if (fileMimetype) - extension = mime.getExtension(fileMimetype) || '' - - if (fileName && !extension) { - const fileNamePair = fileName.split('.') - const fileNamePairLength = fileNamePair.length - - if (fileNamePairLength > 1) - extension = fileNamePair[fileNamePairLength - 1] - else - extension = '' - } - - if (isRemote) - extension = '' - - return extension -} - -export const getFileAppearanceType = (fileName: string, fileMimetype: string) => { - const extension = getFileExtension(fileName, fileMimetype) - - if (extension === 'gif') - return FileAppearanceTypeEnum.gif - - if (FILE_EXTS.image.includes(extension.toUpperCase())) - return FileAppearanceTypeEnum.image - - if (FILE_EXTS.video.includes(extension.toUpperCase())) - return FileAppearanceTypeEnum.video - - if (FILE_EXTS.audio.includes(extension.toUpperCase())) - return FileAppearanceTypeEnum.audio - - if (extension === 'html') - return FileAppearanceTypeEnum.code - - if (extension === 'pdf') - return FileAppearanceTypeEnum.pdf - - if (extension === 'md' || extension === 'markdown' || extension === 'mdx') - return FileAppearanceTypeEnum.markdown - - if (extension === 'xlsx' || extension === 'xls') - return FileAppearanceTypeEnum.excel - - if (extension === 'docx' || extension === 'doc') - return FileAppearanceTypeEnum.word - - if (extension === 'pptx' || extension === 'ppt') - return FileAppearanceTypeEnum.ppt - - if (FILE_EXTS.document.includes(extension.toUpperCase())) - return FileAppearanceTypeEnum.document - - return FileAppearanceTypeEnum.custom -} - -export const getSupportFileType = (fileName: string, fileMimetype: string, isCustom?: boolean) => { - if (isCustom) - return SupportUploadFileTypes.custom - - const extension = getFileExtension(fileName, fileMimetype) - for (const key in FILE_EXTS) { - if ((FILE_EXTS[key]).includes(extension.toUpperCase())) - return key - } - - return '' -} - -export const getProcessedFiles = (files: FileEntity[]) => { - return files.filter(file => file.progress !== -1).map(fileItem => ({ - type: fileItem.supportFileType, - transfer_method: fileItem.transferMethod, - url: fileItem.url || '', - upload_file_id: fileItem.uploadedId || '', - })) -} - -export const getProcessedFilesFromResponse = (files: FileResponse[]) => { - return files.map((fileItem) => { - return { - id: fileItem.related_id, - name: fileItem.filename, - size: fileItem.size || 0, - type: fileItem.mime_type, - progress: 100, - transferMethod: fileItem.transfer_method, - supportFileType: fileItem.type, - uploadedId: fileItem.related_id, - url: fileItem.url, - } - }) -} - -export const getFileNameFromUrl = (url: string) => { - const urlParts = url.split('/') - return urlParts[urlParts.length - 1] || '' -} - -export const getSupportFileExtensionList = (allowFileTypes: string[], allowFileExtensions: string[]) => { - if (allowFileTypes.includes(SupportUploadFileTypes.custom)) - return allowFileExtensions.map(item => item.slice(1).toUpperCase()) - - return allowFileTypes.map(type => FILE_EXTS[type]).flat() -} - -export const isAllowedFileExtension = (fileName: string, fileMimetype: string, allowFileTypes: string[], allowFileExtensions: string[]) => { - return getSupportFileExtensionList(allowFileTypes, allowFileExtensions).includes(getFileExtension(fileName, fileMimetype).toUpperCase()) -} - -export const getFilesInLogs = (rawData: any) => { - const result = Object.keys(rawData || {}).map((key) => { - if (typeof rawData[key] === 'object' && rawData[key]?.dify_model_identity === '__dify__file__') { - return { - varName: key, - list: getProcessedFilesFromResponse([rawData[key]]), - } - } - if (Array.isArray(rawData[key]) && rawData[key].some(item => item?.dify_model_identity === '__dify__file__')) { - return { - varName: key, - list: getProcessedFilesFromResponse(rawData[key]), - } - } - return undefined - }).filter(Boolean) - return result -} - -export const fileIsUploaded = (file: FileEntity) => { - if (file.uploadedId) - return true - - if (file.transferMethod === TransferMethod.remote_url && file.progress === 100) - return true -} - -export const downloadFile = (url: string, filename: string) => { - const anchor = document.createElement('a') - anchor.href = url - anchor.download = filename - anchor.style.display = 'none' - anchor.target = '_blank' - anchor.title = filename - document.body.appendChild(anchor) - anchor.click() - document.body.removeChild(anchor) -} diff --git a/app/components/base/icons/other/ReplayLine.json b/app/components/base/icons/other/ReplayLine.json deleted file mode 100644 index 0fffbc9..0000000 --- a/app/components/base/icons/other/ReplayLine.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "icon": { - "type": "element", - "isRootNode": true, - "name": "svg", - "attributes": { - "width": "20", - "height": "20", - "viewBox": "0 0 20 20", - "fill": "none", - "xmlns": "http://www.w3.org/2000/svg" - }, - "children": [ - { - "type": "element", - "name": "g", - "attributes": { - "id": "Retry" - }, - "children": [ - { - "type": "element", - "name": "path", - "attributes": { - "id": "Vector", - "d": "M9.99996 1.66669C14.6023 1.66669 18.3333 5.39765 18.3333 10C18.3333 14.6024 14.6023 18.3334 9.99996 18.3334C5.39758 18.3334 1.66663 14.6024 1.66663 10H3.33329C3.33329 13.6819 6.31806 16.6667 9.99996 16.6667C13.6819 16.6667 16.6666 13.6819 16.6666 10C16.6666 6.31812 13.6819 3.33335 9.99996 3.33335C7.70848 3.33335 5.68702 4.48947 4.48705 6.25022L6.66663 6.25002V7.91669H1.66663V2.91669H3.33329L3.3332 4.99934C4.85358 2.97565 7.2739 1.66669 9.99996 1.66669Z", - "fill": "currentColor" - }, - "children": [] - } - ] - } - ] - }, - "name": "ReplayLine" -} \ No newline at end of file diff --git a/app/components/base/icons/other/ReplayLine.tsx b/app/components/base/icons/other/ReplayLine.tsx deleted file mode 100644 index 29f7137..0000000 --- a/app/components/base/icons/other/ReplayLine.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import * as React from 'react' -import data from './ReplayLine.json' -import IconBase from '@/app/components/base/icons/IconBase' -import type { IconData } from '@/app/components/base/icons/IconBase' - -const Icon = ( - { - ref, - ...props - }: React.SVGProps & { - ref?: React.RefObject>; - }, -) => - -Icon.displayName = 'ReplayLine' - -export default Icon diff --git a/app/components/base/progress-bar/index.tsx b/app/components/base/progress-bar/index.tsx deleted file mode 100644 index 759c9ea..0000000 --- a/app/components/base/progress-bar/index.tsx +++ /dev/null @@ -1,20 +0,0 @@ -type ProgressBarProps = { - percent: number -} -const ProgressBar = ({ - percent = 0, -}: ProgressBarProps) => { - return ( -
-
-
-
-
{percent}%
-
- ) -} - -export default ProgressBar diff --git a/app/components/base/progress-bar/progress-circle.tsx b/app/components/base/progress-bar/progress-circle.tsx deleted file mode 100644 index b9b280e..0000000 --- a/app/components/base/progress-bar/progress-circle.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { memo } from 'react' -import cn from '@/utils/classnames' - -type ProgressCircleProps = { - className?: string - percentage?: number - size?: number - circleStrokeWidth?: number - circleStrokeColor?: string - circleFillColor?: string - sectorFillColor?: string -} - -const ProgressCircle: React.FC = ({ - className, - percentage = 0, - size = 12, - circleStrokeWidth = 1, - circleStrokeColor = 'stroke-components-progress-brand-border', - circleFillColor = 'fill-components-progress-brand-bg', - sectorFillColor = 'fill-components-progress-brand-progress', -}) => { - const radius = size / 2 - const center = size / 2 - const angle = (percentage / 101) * 360 - const radians = (angle * Math.PI) / 180 - const x = center + radius * Math.cos(radians - Math.PI / 2) - const y = center + radius * Math.sin(radians - Math.PI / 2) - const largeArcFlag = percentage > 50 ? 1 : 0 - - const pathData = ` - M ${center},${center} - L ${center},${center - radius} - A ${radius},${radius} 0 ${largeArcFlag} 1 ${x},${y} - Z - ` - - return ( - - - - - ) -} - -export default memo(ProgressCircle) diff --git a/app/components/chat/answer/index.tsx b/app/components/chat/answer/index.tsx index 2bce5b9..8505e65 100644 --- a/app/components/chat/answer/index.tsx +++ b/app/components/chat/answer/index.tsx @@ -13,7 +13,6 @@ import type { ChatItem, MessageRating, VisionFile } from '@/types/app' import Tooltip from '@/app/components/base/tooltip' import WorkflowProcess from '@/app/components/workflow/workflow-process' import { Markdown } from '@/app/components/base/markdown' -import Button from '@/app/components/base/button' import type { Emoji } from '@/types/tools' const OperationBtn = ({ innerContent, onClick, className }: { innerContent: React.ReactNode; onClick?: () => void; className?: string }) => ( @@ -61,7 +60,6 @@ type IAnswerProps = { onFeedback?: FeedbackFunc isResponding?: boolean allToolIcons?: Record - suggestionClick?: (suggestion: string) => void } // The component needs to maintain its own state to control whether to display input component @@ -71,9 +69,8 @@ const Answer: FC = ({ onFeedback, isResponding, allToolIcons, - suggestionClick = () => { }, }) => { - const { id, content, feedback, agent_thoughts, workflowProcess, suggestedQuestions = [] } = item + const { id, content, feedback, agent_thoughts, workflowProcess } = item const isAgentMode = !!agent_thoughts && agent_thoughts.length > 0 const { t } = useTranslation() @@ -195,17 +192,6 @@ const Answer: FC = ({ : ( ))} - {suggestedQuestions.length > 0 && ( -
-
- {suggestedQuestions.map((suggestion, index) => ( -
- -
- ))} -
-
- )}
{!feedbackDisabled && !item.feedbackDisabled && renderItemOperation()} diff --git a/app/components/chat/index.tsx b/app/components/chat/index.tsx index 0f95615..e257a6c 100644 --- a/app/components/chat/index.tsx +++ b/app/components/chat/index.tsx @@ -52,12 +52,9 @@ const Chat: FC = ({ const isUseInputMethod = useRef(false) const [query, setQuery] = React.useState('') - const queryRef = useRef('') - const handleContentChange = (e: any) => { const value = e.target.value setQuery(value) - queryRef.current = value } const logError = (message: string) => { @@ -65,19 +62,16 @@ const Chat: FC = ({ } const valid = () => { - const query = queryRef.current if (!query || query.trim() === '') { - logError(t('app.errorMessage.valueOfVarRequired')) + logError('Message cannot be empty') return false } return true } useEffect(() => { - if (controlClearQuery) { + if (controlClearQuery) setQuery('') - queryRef.current = '' - } }, [controlClearQuery]) const { files, @@ -92,7 +86,7 @@ const Chat: FC = ({ const handleSend = () => { if (!valid() || (checkCanSend && !checkCanSend())) return - onSend(queryRef.current, files.filter(file => file.progress !== -1).map(fileItem => ({ + onSend(query, files.filter(file => file.progress !== -1).map(fileItem => ({ type: 'image', transfer_method: fileItem.type, url: fileItem.url, @@ -101,10 +95,8 @@ const Chat: FC = ({ if (!files.find(item => item.type === TransferMethod.local_file && !item.fileId)) { if (files.length) onClear() - if (!isResponding) { + if (!isResponding) setQuery('') - queryRef.current = '' - } } } @@ -120,19 +112,11 @@ const Chat: FC = ({ const handleKeyDown = (e: any) => { isUseInputMethod.current = e.nativeEvent.isComposing if (e.code === 'Enter' && !e.shiftKey) { - const result = query.replace(/\n$/, '') - setQuery(result) - queryRef.current = result + setQuery(query.replace(/\n$/, '')) e.preventDefault() } } - const suggestionClick = (suggestion: string) => { - setQuery(suggestion) - queryRef.current = suggestion - handleSend() - } - return (
{/* Chat List */} @@ -146,7 +130,6 @@ const Chat: FC = ({ feedbackDisabled={feedbackDisabled} onFeedback={onFeedback} isResponding={isResponding && isLast} - suggestionClick={suggestionClick} /> } return ( diff --git a/app/components/index.tsx b/app/components/index.tsx index 889fd3b..4d7b28e 100644 --- a/app/components/index.tsx +++ b/app/components/index.tsx @@ -51,7 +51,7 @@ const Main: FC = () => { useEffect(() => { if (APP_INFO?.title) - document.title = `${APP_INFO.title}` + document.title = `${APP_INFO.title} - Powered by Dify` }, [APP_INFO?.title]) // onData change thought (the produce obj). https://github.com/immerjs/immer/issues/576 @@ -101,7 +101,6 @@ const Main: FC = () => { const conversationName = currConversationInfo?.name || t('app.chat.newChatDefaultName') as string const conversationIntroduction = currConversationInfo?.introduction || '' - const suggestedQuestions = currConversationInfo?.suggested_questions || [] const handleConversationSwitch = () => { if (!inited) @@ -118,7 +117,6 @@ const Main: FC = () => { setExistConversationInfo({ name: item?.name || '', introduction: notSyncToStateIntroduction, - suggested_questions: suggestedQuestions, }) } else { @@ -194,7 +192,6 @@ const Main: FC = () => { name: t('app.chat.newChatDefaultName'), inputs: newConversationInputs, introduction: conversationIntroduction, - suggested_questions: suggestedQuestions, }) })) } @@ -212,7 +209,6 @@ const Main: FC = () => { isAnswer: true, feedbackDisabled: true, isOpeningStatement: isShowPrompt, - suggestedQuestions: suggestedQuestions, } if (calculatedIntroduction) return [openStatement] @@ -237,24 +233,15 @@ const Main: FC = () => { return } const _conversationId = getConversationIdFromStorage(APP_ID) - const currentConversation = conversations.find(item => item.id === _conversationId) - const isNotNewConversation = !!currentConversation + const isNotNewConversation = conversations.some(item => item.id === _conversationId) // fetch new conversation info - const { user_input_form, opening_statement: introduction, file_upload, system_parameters, suggested_questions = [] }: any = appParams + const { user_input_form, opening_statement: introduction, file_upload, system_parameters }: any = appParams setLocaleOnClient(APP_INFO.default_language, true) setNewConversationInfo({ name: t('app.chat.newChatDefaultName'), introduction, - suggested_questions }) - if (isNotNewConversation) { - setExistConversationInfo({ - name: currentConversation.name || t('app.chat.newChatDefaultName'), - introduction, - suggested_questions - }) - } const prompt_variables = userInputsFormToPromptVariables(user_input_form) setPromptConfig({ prompt_template: promptTemplate, @@ -338,37 +325,13 @@ const Main: FC = () => { setChatList(newListWithAnswer) } - const transformToServerFile = (fileItem: any) => { - return { - type: 'image', - transfer_method: fileItem.transferMethod, - url: fileItem.url, - upload_file_id: fileItem.id, - } - } - const handleSend = async (message: string, files?: VisionFile[]) => { if (isResponding) { notify({ type: 'info', message: t('app.errorMessage.waitForResponse') }) return } - const toServerInputs: Record = {} - if (currInputs) { - Object.keys(currInputs).forEach((key) => { - const value = currInputs[key] - if (value.supportFileType) - toServerInputs[key] = transformToServerFile(value) - - else if (value[0]?.supportFileType) - toServerInputs[key] = value.map((item: any) => transformToServerFile(item)) - - else - toServerInputs[key] = value - }) - } - const data: Record = { - inputs: toServerInputs, + inputs: currInputs, query: message, conversation_id: isNewConversation ? null : currConversationId, } diff --git a/app/components/welcome/index.tsx b/app/components/welcome/index.tsx index 9365e24..4edc25b 100644 --- a/app/components/welcome/index.tsx +++ b/app/components/welcome/index.tsx @@ -3,7 +3,6 @@ import type { FC } from 'react' import React, { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import TemplateVarPanel, { PanelTitle, VarOpBtnGroup } from '../value-panel' -import FileUploaderInAttachmentWrapper from '../base/file-uploader-in-attachment' import s from './style.module.css' import { AppInfoComp, ChatBtn, EditBtn, FootLogo, PromptTemplate } from './massive-component' import type { AppInfo, PromptConfig } from '@/types/app' @@ -37,7 +36,6 @@ const Welcome: FC = ({ savedInputs, onInputsChange, }) => { - console.log(promptConfig) const { t } = useTranslation() const hasVar = promptConfig.prompt_variables.length > 0 const [isFold, setIsFold] = useState(true) @@ -133,41 +131,6 @@ const Welcome: FC = ({ onChange={(e) => { onInputsChange({ ...inputs, [item.key]: e.target.value }) }} /> )} - - { - item.type === 'file' && ( - { - setInputs({ ...inputs, [item.key]: files[0] }) - }} - value={inputs?.[item.key] || []} - /> - ) - } - { - item.type === 'file-list' && ( - { - setInputs({ ...inputs, [item.key]: files }) - }} - value={inputs?.[item.key] || []} - /> - ) - }
))}
@@ -177,10 +140,7 @@ const Welcome: FC = ({ const canChat = () => { const inputLens = Object.values(inputs).length const promptVariablesLens = promptConfig.prompt_variables.length - const emptyInput = inputLens < promptVariablesLens || Object.entries(inputs).filter(([k, v]) => { - const isRequired = promptConfig.prompt_variables.find(item => item.key === k)?.required ?? true - return isRequired && v === '' - }).length > 0 + const emptyInput = inputLens < promptVariablesLens || Object.values(inputs).filter(v => v === '').length > 0 if (emptyInput) { logError(t('app.errorMessage.valueOfVarRequired')) return false @@ -380,7 +340,10 @@ const Welcome: FC = ({
:
} - + + {t('app.chat.powerBy')} + +
)} diff --git a/i18n/lang/common.en.ts b/i18n/lang/common.en.ts index 4856297..934bd87 100644 --- a/i18n/lang/common.en.ts +++ b/i18n/lang/common.en.ts @@ -28,16 +28,6 @@ const translation = { pasteImageLinkInvalid: 'Invalid image link', imageUpload: 'Image Upload', }, - fileUploader: { - uploadFromComputer: 'Local upload', - pasteFileLink: 'Paste file link', - pasteFileLinkInputPlaceholder: 'Enter URL...', - uploadFromComputerReadError: 'File reading failed, please try again.', - uploadFromComputerUploadError: 'File upload failed, please upload again.', - uploadFromComputerLimit: 'Upload {{type}} cannot exceed {{size}}', - pasteFileLinkInvalid: 'Invalid file link', - fileExtensionNotSupport: 'File extension not supported', - }, } export default translation diff --git a/i18n/lang/common.es.ts b/i18n/lang/common.es.ts index 893d21e..8b5472d 100644 --- a/i18n/lang/common.es.ts +++ b/i18n/lang/common.es.ts @@ -28,16 +28,6 @@ const translation = { pasteImageLinkInvalid: 'Enlace de imagen no válido', imageUpload: 'Subir imagen', }, - fileUploader: { - uploadFromComputer: 'Carga local', - pasteFileLink: 'Pegar enlace de archivo', - uploadFromComputerReadError: 'Error en la lectura del archivo, inténtelo de nuevo.', - uploadFromComputerUploadError: 'Error en la carga del archivo, vuelva a cargarlo.', - pasteFileLinkInvalid: 'Enlace de archivo no válido', - fileExtensionNotSupport: 'Extensión de archivo no compatible', - pasteFileLinkInputPlaceholder: 'Introduzca la URL...', - uploadFromComputerLimit: 'El archivo de carga no puede exceder {{size}}', - }, } export default translation diff --git a/i18n/lang/common.ja.ts b/i18n/lang/common.ja.ts index 87532c2..1fc9540 100644 --- a/i18n/lang/common.ja.ts +++ b/i18n/lang/common.ja.ts @@ -28,16 +28,6 @@ const translation = { pasteImageLinkInvalid: '無効な画像リンクです', imageUpload: '画像アップロード', }, - fileUploader: { - uploadFromComputer: 'ローカルアップロード', - pasteFileLink: 'ファイルリンクの貼り付け', - pasteFileLinkInputPlaceholder: 'URLを入力...', - uploadFromComputerLimit: 'アップロードファイルは{{size}}を超えてはなりません', - uploadFromComputerUploadError: 'ファイルのアップロードに失敗しました。再度アップロードしてください。', - uploadFromComputerReadError: 'ファイルの読み取りに失敗しました。もう一度やり直してください。', - fileExtensionNotSupport: 'ファイル拡張子はサポートされていません', - pasteFileLinkInvalid: '無効なファイルリンク', - }, } export default translation diff --git a/i18n/lang/common.vi.ts b/i18n/lang/common.vi.ts index 2d62fe5..83c5f54 100644 --- a/i18n/lang/common.vi.ts +++ b/i18n/lang/common.vi.ts @@ -28,16 +28,6 @@ const translation = { pasteImageLinkInvalid: 'Liên kết ảnh không hợp lệ', imageUpload: 'Tải ảnh lên', }, - fileUploader: { - uploadFromComputer: 'Tải lên cục bộ', - pasteFileLink: 'Dán liên kết tệp', - pasteFileLinkInputPlaceholder: 'Nhập URL...', - uploadFromComputerLimit: 'Tải lên tệp không được vượt quá {{size}}', - fileExtensionNotSupport: 'Phần mở rộng tệp không được hỗ trợ', - pasteFileLinkInvalid: 'Liên kết tệp không hợp lệ', - uploadFromComputerUploadError: 'Tải lên tệp không thành công, vui lòng tải lên lại.', - uploadFromComputerReadError: 'Đọc tệp không thành công, vui lòng thử lại.', - }, } -export default translation +export default translation; diff --git a/i18n/lang/common.zh.ts b/i18n/lang/common.zh.ts index bad2255..d3b50fd 100644 --- a/i18n/lang/common.zh.ts +++ b/i18n/lang/common.zh.ts @@ -28,16 +28,6 @@ const translation = { pasteImageLinkInvalid: '图片链接无效', imageUpload: '图片上传', }, - fileUploader: { - uploadFromComputer: '从本地上传', - pasteFileLink: '粘贴文件链接', - pasteFileLinkInputPlaceholder: '输入文件链接', - uploadFromComputerReadError: '文件读取失败,请重新选择。', - uploadFromComputerUploadError: '文件上传失败,请重新上传。', - uploadFromComputerLimit: '上传 {{type}} 不能超过 {{size}}', - pasteFileLinkInvalid: '文件链接无效', - fileExtensionNotSupport: '文件类型不支持', - }, } export default translation diff --git a/package.json b/package.json index e7e20b5..d0059b6 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ "@mdx-js/loader": "^2.3.0", "@mdx-js/react": "^2.3.0", "@monaco-editor/react": "^4.6.0", - "@remixicon/react": "^4.6.0", "@tailwindcss/line-clamp": "^0.4.2", "@types/node": "18.15.0", "@types/react": "18.0.28", @@ -27,7 +26,6 @@ "@types/react-syntax-highlighter": "^15.5.6", "ahooks": "^3.7.5", "axios": "^1.3.5", - "class-variance-authority": "^0.7.1", "classnames": "^2.3.2", "copy-to-clipboard": "^3.3.3", "dify-client": "^2.3.1", @@ -40,8 +38,6 @@ "immer": "^9.0.19", "js-cookie": "^3.0.1", "katex": "^0.16.7", - "lodash-es": "^4.17.21", - "mime": "^4.0.7", "negotiator": "^0.6.3", "next": "^14.0.4", "rc-textarea": "^1.5.3", @@ -61,18 +57,15 @@ "scheduler": "^0.23.0", "server-only": "^0.0.1", "swr": "^2.1.0", - "tailwind-merge": "^3.2.0", "typescript": "4.9.5", "use-context-selector": "^1.4.1", - "uuid": "^9.0.0", - "zustand": "^4.5.2" + "uuid": "^9.0.0" }, "devDependencies": { "@antfu/eslint-config": "0.36.0", "@faker-js/faker": "^7.6.0", "@tailwindcss/typography": "^0.5.9", "@types/js-cookie": "^3.0.3", - "@types/lodash-es": "^4.17.12", "@types/negotiator": "^0.6.1", "autoprefixer": "^10.4.14", "eslint-plugin-react-hooks": "^4.6.0", @@ -88,4 +81,4 @@ "eslint --fix" ] } -} +} \ No newline at end of file diff --git a/types/app.ts b/types/app.ts index 58a1641..3d62c4f 100644 --- a/types/app.ts +++ b/types/app.ts @@ -10,9 +10,6 @@ export type PromptVariable = { options?: string[] max_length?: number required: boolean - allowed_file_extensions?: string[] - allowed_file_types?: string[] - allowed_file_upload_methods?: TransferMethod[] } export type PromptConfig = { @@ -102,8 +99,7 @@ export type ConversationItem = { id: string name: string inputs: Record | null - introduction: string, - suggested_questions?: string[] + introduction: string } export type AppInfo = { diff --git a/utils/classnames.ts b/utils/classnames.ts deleted file mode 100644 index 6ce2284..0000000 --- a/utils/classnames.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { twMerge } from 'tailwind-merge' -import cn from 'classnames' - -const classNames = (...cls: cn.ArgumentArray) => { - return twMerge(cn(cls)) -} - -export default classNames diff --git a/utils/format.ts b/utils/format.ts deleted file mode 100644 index 720c8f6..0000000 --- a/utils/format.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Formats a number with comma separators. - * @example formatNumber(1234567) will return '1,234,567' - * @example formatNumber(1234567.89) will return '1,234,567.89' - */ -export const formatNumber = (num: number | string) => { - if (!num) - return num - const parts = num.toString().split('.') - parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',') - return parts.join('.') -} - -/** - * Format file size into standard string format. - * @param fileSize file size (Byte) - * @example formatFileSize(1024) will return '1.00KB' - * @example formatFileSize(1024 * 1024) will return '1.00MB' - */ -export const formatFileSize = (fileSize: number) => { - if (!fileSize) - return fileSize - const units = ['', 'K', 'M', 'G', 'T', 'P'] - let index = 0 - while (fileSize >= 1024 && index < units.length) { - fileSize = fileSize / 1024 - index++ - } - return `${fileSize.toFixed(2)}${units[index]}B` -} - -/** - * Format time into standard string format. - * @example formatTime(60) will return '1.00 min' - * @example formatTime(60 * 60) will return '1.00 h' - */ -export const formatTime = (seconds: number) => { - if (!seconds) - return seconds - const units = ['sec', 'min', 'h'] - let index = 0 - while (seconds >= 60 && index < units.length) { - seconds = seconds / 60 - index++ - } - return `${seconds.toFixed(2)} ${units[index]}` -} - -export const downloadFile = ({ data, fileName }: { data: Blob; fileName: string }) => { - const url = window.URL.createObjectURL(data) - const a = document.createElement('a') - a.href = url - a.download = fileName - document.body.appendChild(a) - a.click() - a.remove() - window.URL.revokeObjectURL(url) -} diff --git a/utils/prompt.ts b/utils/prompt.ts index be0a754..e099422 100644 --- a/utils/prompt.ts +++ b/utils/prompt.ts @@ -21,7 +21,7 @@ export const userInputsFormToPromptVariables = (useInputs: UserInputFormItem[] | return [type === 'text-input' ? 'string' : type, item[type]] })() - if (type === 'string' || type === 'paragraph') { + if (type === 'string' || type === 'paragraph' || type === 'file' || type === 'file-list') { promptVariables.push({ key: content.variable, name: content.label, @@ -40,17 +40,6 @@ export const userInputsFormToPromptVariables = (useInputs: UserInputFormItem[] | options: [], }) } - else if (type === 'file' || type === 'file-list') { - promptVariables.push({ - ...content, - key: content.variable, - name: content.label, - required: content.required, - type, - max_length: content.max_length, - options: [], - }) - } else { promptVariables.push({ key: content.variable,