feat: migrate ESLint to v9 flat config

- Replace .eslintrc.json with eslint.config.mjs
- Simplify configuration using @antfu/eslint-config
- Add necessary ESLint plugin dependencies
- Disable overly strict style rules
- Set package.json type to module for ESM support
- Fix ESLint disable comment format

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
lyzno1
2025-09-10 22:21:17 +08:00
parent 2b1882a5e3
commit 05dcfcf0ca
85 changed files with 464 additions and 502 deletions

View File

@ -1,28 +0,0 @@
{
"extends": [
"@antfu",
"plugin:react-hooks/recommended"
],
"rules": {
"@typescript-eslint/consistent-type-definitions": [
"error",
"type"
],
"no-console": "off",
"indent": "off",
"@typescript-eslint/indent": [
"error",
2,
{
"SwitchCase": 1,
"flatTernaryExpressions": false,
"ignoredNodes": [
"PropertyDefinition[decorators]",
"TSUnionType",
"FunctionExpression[params]:has(Identifier[decorators])"
]
}
],
"react-hooks/exhaustive-deps": "warn"
}
}

3
.gitignore vendored
View File

@ -49,3 +49,6 @@ yarn.lock
# pmpm
pnpm-lock.yaml
# mcp
.serena

1
.husky/pre-commit Normal file
View File

@ -0,0 +1 @@
pnpm eslint-fix

View File

@ -1,4 +1,4 @@
import { type NextRequest } from 'next/server'
import type { NextRequest } from 'next/server'
import { client, getInfo } from '@/app/api/utils/common'
export async function POST(request: NextRequest) {

View File

@ -1,4 +1,4 @@
import { type NextRequest } from 'next/server'
import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
import { client, getInfo } from '@/app/api/utils/common'

View File

@ -1,4 +1,4 @@
import { type NextRequest } from 'next/server'
import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
import { client, getInfo, setSession } from '@/app/api/utils/common'

View File

@ -1,4 +1,4 @@
import { type NextRequest } from 'next/server'
import type { NextRequest } from 'next/server'
import { client, getInfo } from '@/app/api/utils/common'
export async function POST(request: NextRequest) {

View File

@ -1,4 +1,4 @@
import { type NextRequest } from 'next/server'
import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
import { client, getInfo } from '@/app/api/utils/common'

View File

@ -1,4 +1,4 @@
import { type NextRequest } from 'next/server'
import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
import { client, getInfo, setSession } from '@/app/api/utils/common'

View File

@ -1,4 +1,4 @@
import { type NextRequest } from 'next/server'
import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
import { client, getInfo, setSession } from '@/app/api/utils/common'

View File

@ -1,4 +1,4 @@
import { type NextRequest } from 'next/server'
import type { NextRequest } from 'next/server'
import { ChatClient } from 'dify-client'
import { v4 } from 'uuid'
import { API_KEY, API_URL, APP_ID } from '@/config'

View File

@ -3,7 +3,7 @@ import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
type IAppUnavailableProps = {
interface IAppUnavailableProps {
isUnknownReason: boolean
errMessage?: string
}
@ -14,8 +14,7 @@ const AppUnavailable: FC<IAppUnavailableProps> = ({
}) => {
const { t } = useTranslation()
let message = errMessage
if (!errMessage)
message = (isUnknownReason ? t('app.common.appUnkonwError') : t('app.common.appUnavailable')) as string
if (!errMessage) { message = (isUnknownReason ? t('app.common.appUnkonwError') : t('app.common.appUnavailable')) as string }
return (
<div className='flex items-center justify-center w-screen h-screen'>

View File

@ -2,7 +2,7 @@ import type { FC } from 'react'
import classNames from 'classnames'
import style from './style.module.css'
export type AppIconProps = {
export interface AppIconProps {
size?: 'xs' | 'tiny' | 'small' | 'medium' | 'large'
rounded?: boolean
icon?: string

View File

@ -1,7 +1,7 @@
import { forwardRef, useEffect, useRef } from 'react'
import cn from 'classnames'
type IProps = {
interface IProps {
placeholder?: string
value: string
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void
@ -36,19 +36,16 @@ const AutoHeightTextarea = forwardRef(
let hasFocus = false
const runId = setInterval(() => {
hasFocus = doFocus()
if (hasFocus)
clearInterval(runId)
if (hasFocus) { clearInterval(runId) }
}, 100)
}
}
useEffect(() => {
if (autoFocus)
focus()
if (autoFocus) { focus() }
}, [])
useEffect(() => {
if (controlFocus)
focus()
if (controlFocus) { focus() }
}, [controlFocus])
return (

View File

@ -2,7 +2,7 @@ import type { FC, MouseEventHandler } from 'react'
import React from 'react'
import Spinner from '@/app/components/base/spinner'
export type IButtonProps = {
export interface IButtonProps {
type?: string
className?: string
disabled?: boolean

View File

@ -17,7 +17,7 @@ import {
import Button from '@/app/components/base/button'
import cn from '@/utils/classnames'
type FileFromLinkOrLocalProps = {
interface FileFromLinkOrLocalProps {
showFromLink?: boolean
showFromLocal?: boolean
trigger: (open: boolean) => React.ReactNode
@ -38,8 +38,7 @@ const FileFromLinkOrLocal = ({
const disabled = !!fileConfig.number_limits && files.length >= fileConfig.number_limits
const handleSaveUrl = () => {
if (!url)
return
if (!url) { return }
if (!FILE_URL_REGEX.test(url)) {
setShowError(true)

View File

@ -1,6 +1,6 @@
import cn from '@/utils/classnames'
type FileImageRenderProps = {
interface FileImageRenderProps {
imageUrl: string
className?: string
alt?: string

View File

@ -4,7 +4,7 @@ import type { FileUpload } from './types'
import { FILE_EXTS } from './constants'
import { SupportUploadFileTypes } from './types'
type FileInputProps = {
interface FileInputProps {
fileConfig: FileUpload
}
const FileInput = ({
@ -18,8 +18,7 @@ const FileInput = ({
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])
if (i + 1 + files.length <= fileConfig.number_limits) { handleLocalFileUpload(targetFiles[i]) }
}
}
else {

View File

@ -24,7 +24,7 @@ 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 = {
interface FileInAttachmentItemProps {
file: FileEntity
showDeleteAction?: boolean
showDownloadAction?: boolean

View File

@ -67,7 +67,7 @@ const FILE_TYPE_ICON_MAP = {
color: 'text-[#00B2EA]',
},
}
type FileTypeIconProps = {
interface FileTypeIconProps {
type: FileAppearanceType
size?: 'sm' | 'lg' | 'md'
className?: string

View File

@ -148,8 +148,7 @@ export const useFile = (fileConfig: FileUpload) => {
const newFiles = produce(files, (draft) => {
const index = draft.findIndex(file => file.id === newFile.id)
if (index > -1)
draft[index] = newFile
if (index > -1) { draft[index] = newFile }
})
setFiles(newFiles)
}, [fileStore])
@ -198,10 +197,8 @@ export const useFile = (fileConfig: FileUpload) => {
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)
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) => {
@ -235,10 +232,8 @@ export const useFile = (fileConfig: FileUpload) => {
notify({ type: 'error', message: t('common.fileUploader.fileExtensionNotSupport') })
handleRemoveFile(uploadingFile.id)
}
if (!checkSizeLimit(newFile.supportFileType, newFile.size))
handleRemoveFile(uploadingFile.id)
else
handleUpdateFile(newFile)
if (!checkSizeLimit(newFile.supportFileType, newFile.size)) { handleRemoveFile(uploadingFile.id) }
else { handleUpdateFile(newFile) }
}).catch(() => {
notify({ type: 'error', message: t('common.fileUploader.pasteFileLinkInvalid') })
handleRemoveFile(uploadingFile.id)
@ -263,8 +258,7 @@ export const useFile = (fileConfig: FileUpload) => {
}
const allowedFileTypes = fileConfig.allowed_file_types
const fileType = getSupportFileType(file.name, file.type, allowedFileTypes?.includes(SupportUploadFileTypes.custom))
if (!checkSizeLimit(fileType, file.size))
return
if (!checkSizeLimit(fileType, file.size)) { return }
const reader = new FileReader()
const isImage = file.type.startsWith('image')
@ -344,8 +338,7 @@ export const useFile = (fileConfig: FileUpload) => {
const file = e.dataTransfer.files[0]
if (file)
handleLocalFileUpload(file)
if (file) { handleLocalFileUpload(file) }
}, [handleLocalFileUpload])
return {

View File

@ -19,12 +19,12 @@ import Button from '@/app/components/base/button'
import cn from '@/utils/classnames'
import { TransferMethod } from '@/types/app'
type Option = {
interface Option {
value: string
label: string
icon: JSX.Element
}
type FileUploaderInAttachmentProps = {
interface FileUploaderInAttachmentProps {
fileConfig: FileUpload
}
const FileUploaderInAttachment = ({
@ -71,8 +71,7 @@ const FileUploaderInAttachment = ({
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.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 (
@ -109,7 +108,7 @@ const FileUploaderInAttachment = ({
)
}
type FileUploaderInAttachmentWrapperProps = {
interface FileUploaderInAttachmentWrapperProps {
value?: FileEntity[]
onChange: (files: FileEntity[]) => void
fileConfig: FileUpload

View File

@ -11,7 +11,7 @@ import type {
FileEntity,
} from './types'
type Shape = {
interface Shape {
files: FileEntity[]
setFiles: (files: FileEntity[]) => void
}
@ -34,8 +34,7 @@ export const FileContext = createContext<FileStore | null>(null)
export function useStore<T>(selector: (state: Shape) => T): T {
const store = useContext(FileContext)
if (!store)
throw new Error('Missing FileContext.Provider in the tree')
if (!store) { throw new Error('Missing FileContext.Provider in the tree') }
return useZustandStore(store, selector)
}
@ -44,7 +43,7 @@ export const useFileStore = () => {
return useContext(FileContext)!
}
type FileProviderProps = {
interface FileProviderProps {
children: React.ReactNode
value?: FileEntity[]
onChange?: (files: FileEntity[]) => void
@ -56,8 +55,7 @@ export const FileContextProvider = ({
}: FileProviderProps) => {
const storeRef = useRef<FileStore | undefined>(undefined)
if (!storeRef.current)
storeRef.current = createFileStore(value, onChange)
if (!storeRef.current) { storeRef.current = createFileStore(value, onChange) }
return (
<FileContext.Provider value={storeRef.current}>

View File

@ -17,7 +17,7 @@ export enum FileAppearanceTypeEnum {
export type FileAppearanceType = keyof typeof FileAppearanceTypeEnum
export type FileEntity = {
export interface FileEntity {
id: string
name: string
size: number
@ -32,7 +32,7 @@ export type FileEntity = {
isRemote?: boolean
}
export type EnabledOrDisabled = {
export interface EnabledOrDisabled {
enabled?: boolean
}
@ -41,7 +41,7 @@ export enum Resolution {
high = 'high',
}
export type FileUploadConfigResponse = {
export interface FileUploadConfigResponse {
batch_count_limit: number
image_file_size_limit?: number | string // default is 10MB
file_size_limit: number // default is 15MB
@ -71,7 +71,7 @@ export enum SupportUploadFileTypes {
custom = 'custom',
}
export type FileResponse = {
export interface FileResponse {
related_id: string
extension: string
filename: string

View File

@ -5,7 +5,7 @@ import { FILE_EXTS } from './constants'
import { upload } from '@/service/base'
import { TransferMethod } from '@/types/app'
type FileUploadParams = {
interface FileUploadParams {
file: File
onProgressCallback: (progress: number) => void
onSuccessCallback: (res: { id: string }) => void
@ -42,21 +42,17 @@ export const fileUpload: FileUpload = ({
export const getFileExtension = (fileName: string, fileMimetype: string, isRemote?: boolean) => {
let extension = ''
if (fileMimetype)
extension = mime.getExtension(fileMimetype) || ''
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 (fileNamePairLength > 1) { extension = fileNamePair[fileNamePairLength - 1] }
else { extension = '' }
}
if (isRemote)
extension = ''
if (isRemote) { extension = '' }
return extension
}
@ -64,50 +60,37 @@ export const getFileExtension = (fileName: string, fileMimetype: string, isRemot
export const getFileAppearanceType = (fileName: string, fileMimetype: string) => {
const extension = getFileExtension(fileName, fileMimetype)
if (extension === 'gif')
return FileAppearanceTypeEnum.gif
if (extension === 'gif') { return FileAppearanceTypeEnum.gif }
if (FILE_EXTS.image.includes(extension.toUpperCase()))
return FileAppearanceTypeEnum.image
if (FILE_EXTS.image.includes(extension.toUpperCase())) { return FileAppearanceTypeEnum.image }
if (FILE_EXTS.video.includes(extension.toUpperCase()))
return FileAppearanceTypeEnum.video
if (FILE_EXTS.video.includes(extension.toUpperCase())) { return FileAppearanceTypeEnum.video }
if (FILE_EXTS.audio.includes(extension.toUpperCase()))
return FileAppearanceTypeEnum.audio
if (FILE_EXTS.audio.includes(extension.toUpperCase())) { return FileAppearanceTypeEnum.audio }
if (extension === 'html')
return FileAppearanceTypeEnum.code
if (extension === 'html') { return FileAppearanceTypeEnum.code }
if (extension === 'pdf')
return FileAppearanceTypeEnum.pdf
if (extension === 'pdf') { return FileAppearanceTypeEnum.pdf }
if (extension === 'md' || extension === 'markdown' || extension === 'mdx')
return FileAppearanceTypeEnum.markdown
if (extension === 'md' || extension === 'markdown' || extension === 'mdx') { return FileAppearanceTypeEnum.markdown }
if (extension === 'xlsx' || extension === 'xls')
return FileAppearanceTypeEnum.excel
if (extension === 'xlsx' || extension === 'xls') { return FileAppearanceTypeEnum.excel }
if (extension === 'docx' || extension === 'doc')
return FileAppearanceTypeEnum.word
if (extension === 'docx' || extension === 'doc') { return FileAppearanceTypeEnum.word }
if (extension === 'pptx' || extension === 'ppt')
return FileAppearanceTypeEnum.ppt
if (extension === 'pptx' || extension === 'ppt') { return FileAppearanceTypeEnum.ppt }
if (FILE_EXTS.document.includes(extension.toUpperCase()))
return FileAppearanceTypeEnum.document
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
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
if ((FILE_EXTS[key]).includes(extension.toUpperCase())) { return key }
}
return ''
@ -144,8 +127,7 @@ export const getFileNameFromUrl = (url: string) => {
}
export const getSupportFileExtensionList = (allowFileTypes: string[], allowFileExtensions: string[]) => {
if (allowFileTypes.includes(SupportUploadFileTypes.custom))
return allowFileExtensions.map(item => item.slice(1).toUpperCase())
if (allowFileTypes.includes(SupportUploadFileTypes.custom)) { return allowFileExtensions.map(item => item.slice(1).toUpperCase()) }
return allowFileTypes.map(type => FILE_EXTS[type]).flat()
}
@ -174,11 +156,9 @@ export const getFilesInLogs = (rawData: any) => {
}
export const fileIsUploaded = (file: FileEntity) => {
if (file.uploadedId)
return true
if (file.uploadedId) { return true }
if (file.transferMethod === TransferMethod.remote_url && file.progress === 100)
return true
if (file.transferMethod === TransferMethod.remote_url && file.progress === 100) { return true }
}
export const downloadFile = (url: string, filename: string) => {

View File

@ -2,12 +2,12 @@ import { forwardRef } from 'react'
import { generate } from './utils'
import type { AbstractNode } from './utils'
export type IconData = {
export interface IconData {
name: string
icon: AbstractNode
}
export type IconBaseProps = {
export interface IconBaseProps {
data: IconData
className?: string
onClick?: React.MouseEventHandler<SVGElement>

View File

@ -11,7 +11,7 @@ const Icon = (
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>
},
) => <IconBase {...props} ref={ref} data={data as IconData} />

View File

@ -1,6 +1,6 @@
import React from 'react'
export type AbstractNode = {
export interface AbstractNode {
name: string
attributes: {
[key: string]: string
@ -8,7 +8,7 @@ export type AbstractNode = {
children?: AbstractNode[]
}
export type Attrs = {
export interface Attrs {
[key: string]: string
}

View File

@ -5,7 +5,7 @@ import cn from 'classnames'
import s from './style.module.css'
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
type Props = {
interface Props {
srcs: string[]
}
@ -65,9 +65,9 @@ export const ImageGalleryTest = () => {
const imgGallerySrcs = (() => {
const srcs = []
for (let i = 0; i < 6; i++)
// srcs.push('https://placekitten.com/640/360')
// srcs.push('https://placekitten.com/360/640')
srcs.push('https://placekitten.com/360/360')
// srcs.push('https://placekitten.com/640/360')
// srcs.push('https://placekitten.com/360/640')
{ srcs.push('https://placekitten.com/360/360') }
return srcs
})()

View File

@ -13,7 +13,7 @@ import {
import Upload03 from '@/app/components/base/icons/line/upload-03'
import type { ImageFile, VisionSettings } from '@/types/app'
type UploadOnlyFromLocalProps = {
interface UploadOnlyFromLocalProps {
onUpload: (imageFile: ImageFile) => void
disabled?: boolean
limit?: number
@ -39,7 +39,7 @@ const UploadOnlyFromLocal: FC<UploadOnlyFromLocalProps> = ({
)
}
type UploaderButtonProps = {
interface UploaderButtonProps {
methods: VisionSettings['transfer_methods']
onUpload: (imageFile: ImageFile) => void
disabled?: boolean
@ -62,8 +62,7 @@ const UploaderButton: FC<UploaderButtonProps> = ({
}
const handleToggle = () => {
if (disabled)
return
if (disabled) { return }
setOpen(v => !v)
}
@ -115,7 +114,7 @@ const UploaderButton: FC<UploaderButtonProps> = ({
)
}
type ChatImageUploaderProps = {
interface ChatImageUploaderProps {
settings: VisionSettings
onUpload: (imageFile: ImageFile) => void
disabled?: boolean

View File

@ -5,7 +5,7 @@ import Button from '@/app/components/base/button'
import type { ImageFile } from '@/types/app'
import { TransferMethod } from '@/types/app'
type ImageLinkInputProps = {
interface ImageLinkInputProps {
onUpload: (imageFile: ImageFile) => void
}
const regex = /^(https?|ftp):\/\//

View File

@ -10,7 +10,7 @@ import type { ImageFile } from '@/types/app'
import { TransferMethod } from '@/types/app'
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
type ImageListProps = {
interface ImageListProps {
list: ImageFile[]
readonly?: boolean
onRemove?: (imageFileId: string) => void
@ -31,12 +31,10 @@ const ImageList: FC<ImageListProps> = ({
const [imagePreviewUrl, setImagePreviewUrl] = useState('')
const handleImageLinkLoadSuccess = (item: ImageFile) => {
if (item.type === TransferMethod.remote_url && onImageLinkLoadSuccess && item.progress !== -1)
onImageLinkLoadSuccess(item._id)
if (item.type === TransferMethod.remote_url && onImageLinkLoadSuccess && item.progress !== -1) { onImageLinkLoadSuccess(item._id) }
}
const handleImageLinkLoadError = (item: ImageFile) => {
if (item.type === TransferMethod.remote_url && onImageLinkLoadError)
onImageLinkLoadError(item._id)
if (item.type === TransferMethod.remote_url && onImageLinkLoadError) { onImageLinkLoadError(item._id) }
}
return (

View File

@ -2,7 +2,7 @@ import type { FC } from 'react'
import { createPortal } from 'react-dom'
import XClose from '@/app/components/base/icons/line/x-close'
type ImagePreviewProps = {
interface ImagePreviewProps {
url: string
onCancel: () => void
}

View File

@ -8,7 +8,7 @@ import type { ImageFile } from '@/types/app'
import { TransferMethod } from '@/types/app'
import Toast from '@/app/components/base/toast'
type UploaderProps = {
interface UploaderProps {
children: (hovering: boolean) => JSX.Element
onUpload: (imageFile: ImageFile) => void
limit?: number
@ -28,8 +28,7 @@ const Uploader: FC<UploaderProps> = ({
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file)
return
if (!file) { return }
if (limit && file.size > limit * 1024 * 1024) {
notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerLimit', { size: limit }) })

View File

@ -2,7 +2,7 @@
import { upload } from '@/service/base'
type ImageUploadParams = {
interface ImageUploadParams {
file: File
onProgressCallback: (progress: number) => void
onSuccessCallback: (res: { id: string }) => void

View File

@ -2,7 +2,7 @@ import React from 'react'
import './style.css'
type ILoadingProps = {
interface ILoadingProps {
type?: 'area' | 'app'
}
const Loading = (

View File

@ -17,7 +17,7 @@ import {
import type { OffsetOptions, Placement } from '@floating-ui/react'
type PortalToFollowElemOptions = {
interface PortalToFollowElemOptions {
/*
* top, bottom, left, right
* start, end. Default is middle
@ -85,8 +85,7 @@ const PortalToFollowElemContext = React.createContext<ContextType>(null)
export function usePortalToFollowElemContext() {
const context = React.useContext(PortalToFollowElemContext)
if (context == null)
throw new Error('PortalToFollowElem components must be wrapped in <PortalToFollowElem />')
if (context == null) { throw new Error('PortalToFollowElem components must be wrapped in <PortalToFollowElem />') }
return context
}
@ -106,7 +105,7 @@ export function PortalToFollowElem({
}
export const PortalToFollowElemTrigger = React.forwardRef<
HTMLElement,
HTMLElement,
React.HTMLProps<HTMLElement> & { asChild?: boolean }
>(({ children, asChild = false, ...props }, propRef) => {
const context = usePortalToFollowElemContext()
@ -141,14 +140,13 @@ React.HTMLProps<HTMLElement> & { asChild?: boolean }
PortalToFollowElemTrigger.displayName = 'PortalToFollowElemTrigger'
export const PortalToFollowElemContent = React.forwardRef<
HTMLDivElement,
React.HTMLProps<HTMLDivElement>
HTMLDivElement,
React.HTMLProps<HTMLDivElement>
>(({ style, ...props }, propRef) => {
const context = usePortalToFollowElemContext()
const ref = useMergeRefs([context.refs.setFloating, propRef])
if (!context.open)
return null
if (!context.open) { return null }
return (
<FloatingPortal>

View File

@ -1,4 +1,4 @@
type ProgressBarProps = {
interface ProgressBarProps {
percent: number
}
const ProgressBar = ({

View File

@ -1,7 +1,7 @@
import { memo } from 'react'
import cn from '@/utils/classnames'
type ProgressCircleProps = {
interface ProgressCircleProps {
className?: string
percentage?: number
size?: number

View File

@ -15,12 +15,12 @@ const defaultItems = [
{ value: 7, name: 'option7' },
]
export type Item = {
export interface Item {
value: number | string
name: string
}
export type ISelectProps = {
export interface ISelectProps {
className?: string
items?: Item[]
defaultValue?: number | string
@ -45,8 +45,7 @@ const Select: FC<ISelectProps> = ({
useEffect(() => {
let defaultSelect = null
const existed = items.find((item: Item) => item.value === defaultValue)
if (existed)
defaultSelect = existed
if (existed) { defaultSelect = existed }
setSelectedItem(defaultSelect)
}, [defaultValue])
@ -77,23 +76,20 @@ const Select: FC<ISelectProps> = ({
? <Combobox.Input
className={`w-full rounded-lg border-0 ${bgClassName} py-1.5 pl-3 pr-10 shadow-sm sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover:bg-gray-200 cursor-not-allowed`}
onChange={(event) => {
if (!disabled)
setQuery(event.target.value)
if (!disabled) { setQuery(event.target.value) }
}}
displayValue={(item: Item) => item?.name}
/>
: <Combobox.Button onClick={
() => {
if (!disabled)
setOpen(!open)
if (!disabled) { setOpen(!open) }
}
} className={`flex items-center h-9 w-full rounded-lg border-0 ${bgClassName} py-1.5 pl-3 pr-10 shadow-sm sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover:bg-gray-200`}>
{selectedItem?.name}
</Combobox.Button>}
<Combobox.Button className="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none group-hover:bg-gray-200" onClick={
() => {
if (!disabled)
setOpen(!open)
if (!disabled) { setOpen(!open) }
}
}>
{open ? <ChevronUpIcon className="h-5 w-5" /> : <ChevronDownIcon className="h-5 w-5" />}
@ -147,8 +143,7 @@ const SimpleSelect: FC<ISelectProps> = ({
useEffect(() => {
let defaultSelect = null
const existed = items.find((item: Item) => item.value === defaultValue)
if (existed)
defaultSelect = existed
if (existed) { defaultSelect = existed }
setSelectedItem(defaultSelect)
}, [defaultValue])

View File

@ -1,7 +1,7 @@
import type { FC } from 'react'
import React from 'react'
type Props = {
interface Props {
loading?: boolean
className?: string
children?: React.ReactNode | string

View File

@ -0,0 +1,18 @@
'use client'
import { Streamdown } from 'streamdown'
import 'katex/dist/katex.min.css'
interface StreamdownMarkdownProps {
content: string
className?: string
}
export function StreamdownMarkdown({ content, className = '' }: StreamdownMarkdownProps) {
return (
<div className={`markdown-body streamdown-markdown ${className}`}>
<Streamdown>{content}</Streamdown>
</div>
)
}
export default StreamdownMarkdown

View File

@ -11,14 +11,14 @@ import {
} from '@heroicons/react/20/solid'
import { createContext, useContext } from 'use-context-selector'
export type IToastProps = {
export interface IToastProps {
type?: 'success' | 'error' | 'warning' | 'info'
duration?: number
message: string
children?: ReactNode
onClose?: () => void
}
type IToastContext = {
interface IToastContext {
notify: (props: IToastProps) => void
}
const defaultDuring = 3000
@ -33,8 +33,7 @@ const Toast = ({
children,
}: IToastProps) => {
// sometimes message is react node array. Not handle it.
if (typeof message !== 'string')
return null
if (typeof message !== 'string') { return null }
return <div className={classNames(
'fixed rounded-md p-4 my-4 mx-8 z-50',
@ -124,8 +123,7 @@ Toast.notify = ({
root.render(<Toast type={type} message={message} duration={duration} />)
document.body.appendChild(holder)
setTimeout(() => {
if (holder)
holder.remove()
if (holder) { holder.remove() }
}, duration || defaultDuring)
}
}

View File

@ -2,7 +2,7 @@
import type { FC } from 'react'
import React, { useState } from 'react'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
export type TooltipProps = {
export interface TooltipProps {
position?: 'top' | 'right' | 'bottom' | 'left'
triggerMethod?: 'hover' | 'click'
popupContent: React.ReactNode
@ -13,7 +13,7 @@ const arrow = (
<svg className="absolute text-white h-2 w-full left-0 top-full" x="0px" y="0px" viewBox="0 0 255 255"><polygon className="fill-current" points="0,0 127.5,127.5 255,0"></polygon></svg>
)
const Tooltip: FC< TooltipProps> = ({
const Tooltip: FC<TooltipProps> = ({
position = 'top',
triggerMethod = 'hover',
popupContent,

View File

@ -5,7 +5,7 @@ import React from 'react'
import { Tooltip as ReactTooltip } from 'react-tooltip' // fixed version to 5.8.3 https://github.com/ReactTooltip/react-tooltip/issues/972
import 'react-tooltip/dist/react-tooltip.css'
type TooltipProps = {
interface TooltipProps {
selector: string
content?: string
htmlContent?: React.ReactNode

View File

@ -1,30 +1,32 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline'
import { useTranslation } from 'react-i18next'
import LoadingAnim from '../loading-anim'
import type { FeedbackFunc } from '../type'
import s from '../style.module.css'
import ImageGallery from '../../base/image-gallery'
import Thought from '../thought'
import { randomString } from '@/utils/string'
import type { ChatItem, MessageRating, VisionFile } from '@/types/app'
import type { Emoji } from '@/types/tools'
import { HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline'
import React from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import StreamdownMarkdown from '@/app/components/base/streamdown-markdown'
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'
import { randomString } from '@/utils/string'
import ImageGallery from '../../base/image-gallery'
import LoadingAnim from '../loading-anim'
import s from '../style.module.css'
import Thought from '../thought'
const OperationBtn = ({ innerContent, onClick, className }: { innerContent: React.ReactNode; onClick?: () => void; className?: string }) => (
<div
className={`relative box-border flex items-center justify-center h-7 w-7 p-0.5 rounded-lg bg-white cursor-pointer text-gray-500 hover:text-gray-800 ${className ?? ''}`}
style={{ boxShadow: '0px 4px 6px -1px rgba(0, 0, 0, 0.1), 0px 2px 4px -2px rgba(0, 0, 0, 0.05)' }}
onClick={onClick && onClick}
>
{innerContent}
</div>
)
function OperationBtn({ innerContent, onClick, className }: { innerContent: React.ReactNode, onClick?: () => void, className?: string }) {
return (
<div
className={`relative box-border flex items-center justify-center h-7 w-7 p-0.5 rounded-lg bg-white cursor-pointer text-gray-500 hover:text-gray-800 ${className ?? ''}`}
style={{ boxShadow: '0px 4px 6px -1px rgba(0, 0, 0, 0.1), 0px 2px 4px -2px rgba(0, 0, 0, 0.05)' }}
onClick={onClick && onClick}
>
{innerContent}
</div>
)
}
const OpeningStatementIcon: FC<{ className?: string }> = ({ className }) => (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
@ -33,29 +35,35 @@ const OpeningStatementIcon: FC<{ className?: string }> = ({ className }) => (
)
const RatingIcon: FC<{ isLike: boolean }> = ({ isLike }) => {
return isLike ? <HandThumbUpIcon className='w-4 h-4' /> : <HandThumbDownIcon className='w-4 h-4' />
return isLike ? <HandThumbUpIcon className="w-4 h-4" /> : <HandThumbDownIcon className="w-4 h-4" />
}
const EditIcon: FC<{ className?: string }> = ({ className }) => {
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path d="M14 11.9998L13.3332 12.7292C12.9796 13.1159 12.5001 13.3332 12.0001 13.3332C11.5001 13.3332 11.0205 13.1159 10.6669 12.7292C10.3128 12.3432 9.83332 12.1265 9.33345 12.1265C8.83359 12.1265 8.35409 12.3432 7.99998 12.7292M2 13.3332H3.11636C3.44248 13.3332 3.60554 13.3332 3.75899 13.2963C3.89504 13.2637 4.0251 13.2098 4.1444 13.1367C4.27895 13.0542 4.39425 12.9389 4.62486 12.7083L13 4.33316C13.5523 3.78087 13.5523 2.88544 13 2.33316C12.4477 1.78087 11.5523 1.78087 11 2.33316L2.62484 10.7083C2.39424 10.9389 2.27894 11.0542 2.19648 11.1888C2.12338 11.3081 2.0695 11.4381 2.03684 11.5742C2 11.7276 2 11.8907 2 12.2168V13.3332Z" stroke="#6B7280" strokeLinecap="round" strokeLinejoin="round" />
</svg>
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path d="M14 11.9998L13.3332 12.7292C12.9796 13.1159 12.5001 13.3332 12.0001 13.3332C11.5001 13.3332 11.0205 13.1159 10.6669 12.7292C10.3128 12.3432 9.83332 12.1265 9.33345 12.1265C8.83359 12.1265 8.35409 12.3432 7.99998 12.7292M2 13.3332H3.11636C3.44248 13.3332 3.60554 13.3332 3.75899 13.2963C3.89504 13.2637 4.0251 13.2098 4.1444 13.1367C4.27895 13.0542 4.39425 12.9389 4.62486 12.7083L13 4.33316C13.5523 3.78087 13.5523 2.88544 13 2.33316C12.4477 1.78087 11.5523 1.78087 11 2.33316L2.62484 10.7083C2.39424 10.9389 2.27894 11.0542 2.19648 11.1888C2.12338 11.3081 2.0695 11.4381 2.03684 11.5742C2 11.7276 2 11.8907 2 12.2168V13.3332Z" stroke="#6B7280" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)
}
export const EditIconSolid: FC<{ className?: string }> = ({ className }) => {
return <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path fillRule="evenodd" clip-rule="evenodd" d="M10.8374 8.63108C11.0412 8.81739 11.0554 9.13366 10.8691 9.33747L10.369 9.88449C10.0142 10.2725 9.52293 10.5001 9.00011 10.5001C8.47746 10.5001 7.98634 10.2727 7.63157 9.8849C7.45561 9.69325 7.22747 9.59515 7.00014 9.59515C6.77271 9.59515 6.54446 9.69335 6.36846 9.88517C6.18177 10.0886 5.86548 10.1023 5.66201 9.91556C5.45853 9.72888 5.44493 9.41259 5.63161 9.20911C5.98678 8.82201 6.47777 8.59515 7.00014 8.59515C7.52251 8.59515 8.0135 8.82201 8.36867 9.20911L8.36924 9.20974C8.54486 9.4018 8.77291 9.50012 9.00011 9.50012C9.2273 9.50012 9.45533 9.40182 9.63095 9.20979L10.131 8.66276C10.3173 8.45895 10.6336 8.44476 10.8374 8.63108Z" fill="#6B7280" />
<path fillRule="evenodd" clip-rule="evenodd" d="M7.89651 1.39656C8.50599 0.787085 9.49414 0.787084 10.1036 1.39656C10.7131 2.00604 10.7131 2.99419 10.1036 3.60367L3.82225 9.88504C3.81235 9.89494 3.80254 9.90476 3.79281 9.91451C3.64909 10.0585 3.52237 10.1855 3.3696 10.2791C3.23539 10.3613 3.08907 10.4219 2.93602 10.4587C2.7618 10.5005 2.58242 10.5003 2.37897 10.5001C2.3652 10.5001 2.35132 10.5001 2.33732 10.5001H1.50005C1.22391 10.5001 1.00005 10.2763 1.00005 10.0001V9.16286C1.00005 9.14886 1.00004 9.13497 1.00003 9.1212C0.999836 8.91776 0.999669 8.73838 1.0415 8.56416C1.07824 8.4111 1.13885 8.26479 1.22109 8.13058C1.31471 7.97781 1.44166 7.85109 1.58566 7.70736C1.5954 7.69764 1.60523 7.68783 1.61513 7.67793L7.89651 1.39656Z" fill="#6B7280" />
</svg>
return (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path fillRule="evenodd" clip-rule="evenodd" d="M10.8374 8.63108C11.0412 8.81739 11.0554 9.13366 10.8691 9.33747L10.369 9.88449C10.0142 10.2725 9.52293 10.5001 9.00011 10.5001C8.47746 10.5001 7.98634 10.2727 7.63157 9.8849C7.45561 9.69325 7.22747 9.59515 7.00014 9.59515C6.77271 9.59515 6.54446 9.69335 6.36846 9.88517C6.18177 10.0886 5.86548 10.1023 5.66201 9.91556C5.45853 9.72888 5.44493 9.41259 5.63161 9.20911C5.98678 8.82201 6.47777 8.59515 7.00014 8.59515C7.52251 8.59515 8.0135 8.82201 8.36867 9.20911L8.36924 9.20974C8.54486 9.4018 8.77291 9.50012 9.00011 9.50012C9.2273 9.50012 9.45533 9.40182 9.63095 9.20979L10.131 8.66276C10.3173 8.45895 10.6336 8.44476 10.8374 8.63108Z" fill="#6B7280" />
<path fillRule="evenodd" clip-rule="evenodd" d="M7.89651 1.39656C8.50599 0.787085 9.49414 0.787084 10.1036 1.39656C10.7131 2.00604 10.7131 2.99419 10.1036 3.60367L3.82225 9.88504C3.81235 9.89494 3.80254 9.90476 3.79281 9.91451C3.64909 10.0585 3.52237 10.1855 3.3696 10.2791C3.23539 10.3613 3.08907 10.4219 2.93602 10.4587C2.7618 10.5005 2.58242 10.5003 2.37897 10.5001C2.3652 10.5001 2.35132 10.5001 2.33732 10.5001H1.50005C1.22391 10.5001 1.00005 10.2763 1.00005 10.0001V9.16286C1.00005 9.14886 1.00004 9.13497 1.00003 9.1212C0.999836 8.91776 0.999669 8.73838 1.0415 8.56416C1.07824 8.4111 1.13885 8.26479 1.22109 8.13058C1.31471 7.97781 1.44166 7.85109 1.58566 7.70736C1.5954 7.69764 1.60523 7.68783 1.61513 7.67793L7.89651 1.39656Z" fill="#6B7280" />
</svg>
)
}
const IconWrapper: FC<{ children: React.ReactNode | string }> = ({ children }) => {
return <div className={'rounded-lg h-6 w-6 flex items-center justify-center hover:bg-gray-100'}>
{children}
</div>
return (
<div className="rounded-lg h-6 w-6 flex items-center justify-center hover:bg-gray-100">
{children}
</div>
)
}
type IAnswerProps = {
interface IAnswerProps {
item: ChatItem
feedbackDisabled: boolean
onFeedback?: FeedbackFunc
@ -79,15 +87,14 @@ const Answer: FC<IAnswerProps> = ({
const { t } = useTranslation()
/**
* Render feedback results (distinguish between users and administrators)
* User reviews cannot be cancelled in Console
* @param rating feedback result
* @param isUserFeedback Whether it is user's feedback
* @returns comp
*/
* Render feedback results (distinguish between users and administrators)
* User reviews cannot be cancelled in Console
* @param rating feedback result
* @param isUserFeedback Whether it is user's feedback
* @returns comp
*/
const renderFeedbackRating = (rating: MessageRating | undefined) => {
if (!rating)
return null
if (!rating) { return null }
const isLike = rating === 'like'
const ratingIconClassname = isLike ? 'text-primary-600 bg-primary-100 hover:bg-primary-200' : 'text-red-600 bg-red-100 hover:bg-red-200'
@ -98,7 +105,7 @@ const Answer: FC<IAnswerProps> = ({
content={isLike ? '取消赞同' : '取消反对'}
>
<div
className={'relative box-border flex items-center justify-center h-7 w-7 p-0.5 rounded-lg bg-white cursor-pointer text-gray-500 hover:text-gray-800'}
className="relative box-border flex items-center justify-center h-7 w-7 p-0.5 rounded-lg bg-white cursor-pointer text-gray-500 hover:text-gray-800"
style={{ boxShadow: '0px 4px 6px -1px rgba(0, 0, 0, 0.1), 0px 2px 4px -2px rgba(0, 0, 0, 0.05)' }}
onClick={async () => {
await onFeedback?.(id, { rating: null })
@ -120,14 +127,16 @@ const Answer: FC<IAnswerProps> = ({
const userOperation = () => {
return feedback?.rating
? null
: <div className='flex gap-1'>
<Tooltip selector={`user-feedback-${randomString(16)}`} content={t('common.operation.like') as string}>
{OperationBtn({ innerContent: <IconWrapper><RatingIcon isLike={true} /></IconWrapper>, onClick: () => onFeedback?.(id, { rating: 'like' }) })}
</Tooltip>
<Tooltip selector={`user-feedback-${randomString(16)}`} content={t('common.operation.dislike') as string}>
{OperationBtn({ innerContent: <IconWrapper><RatingIcon isLike={false} /></IconWrapper>, onClick: () => onFeedback?.(id, { rating: 'dislike' }) })}
</Tooltip>
</div>
: (
<div className="flex gap-1">
<Tooltip selector={`user-feedback-${randomString(16)}`} content={t('common.operation.like') as string}>
{OperationBtn({ innerContent: <IconWrapper><RatingIcon isLike={true} /></IconWrapper>, onClick: () => onFeedback?.(id, { rating: 'like' }) })}
</Tooltip>
<Tooltip selector={`user-feedback-${randomString(16)}`} content={t('common.operation.dislike') as string}>
{OperationBtn({ innerContent: <IconWrapper><RatingIcon isLike={false} /></IconWrapper>, onClick: () => onFeedback?.(id, { rating: 'dislike' }) })}
</Tooltip>
</div>
)
}
return (
@ -138,8 +147,7 @@ const Answer: FC<IAnswerProps> = ({
}
const getImgs = (list?: VisionFile[]) => {
if (!list)
return []
if (!list) { return [] }
return list.filter(file => file.type === 'image' && file.belongs_to === 'assistant')
}
@ -148,7 +156,7 @@ const Answer: FC<IAnswerProps> = ({
{agent_thoughts?.map((item, index) => (
<div key={index}>
{item.thought && (
<Markdown content={item.thought} />
<StreamdownMarkdown content={item.thought} />
)}
{/* {item.tool} */}
{/* perhaps not use tool */}
@ -170,13 +178,14 @@ const Answer: FC<IAnswerProps> = ({
return (
<div key={id}>
<div className='flex items-start'>
<div className="flex items-start">
<div className={`${s.answerIcon} w-10 h-10 shrink-0`}>
{isResponding
&& <div className={s.typeingIcon}>
<LoadingAnim type='avatar' />
</div>
}
&& (
<div className={s.typeingIcon}>
<LoadingAnim type="avatar" />
</div>
)}
</div>
<div className={`${s.answerWrap}`}>
<div className={`${s.answer} relative text-sm text-gray-900`}>
@ -186,28 +195,28 @@ const Answer: FC<IAnswerProps> = ({
)}
{(isResponding && (isAgentMode ? (!content && (agent_thoughts || []).filter(item => !!item.thought || !!item.tool).length === 0) : !content))
? (
<div className='flex items-center justify-center w-6 h-5'>
<LoadingAnim type='text' />
<div className="flex items-center justify-center w-6 h-5">
<LoadingAnim type="text" />
</div>
)
: (isAgentMode
? agentModeAnswer
: (
<Markdown content={content} />
<StreamdownMarkdown content={content} />
))}
{suggestedQuestions.length > 0 && (
<div className='mt-3'>
<div className='flex gap-1 mt-1 flex-wrap'>
<div className="mt-3">
<div className="flex gap-1 mt-1 flex-wrap">
{suggestedQuestions.map((suggestion, index) => (
<div key={index} className='flex items-center gap-1'>
<Button className='text-sm' type='link' onClick={() => suggestionClick(suggestion)}>{suggestion}</Button>
<div key={index} className="flex items-center gap-1">
<Button className="text-sm" type="link" onClick={() => suggestionClick(suggestion)}>{suggestion}</Button>
</div>
))}
</div>
</div>
)}
</div>
<div className='absolute top-[-14px] right-[-14px] flex flex-row justify-end gap-1'>
<div className="absolute top-[-14px] right-[-14px] flex flex-row justify-end gap-1">
{!feedbackDisabled && !item.feedbackDisabled && renderItemOperation()}
{/* User feedback must be displayed */}
{!feedbackDisabled && renderFeedbackRating(feedback?.rating)}

View File

@ -19,7 +19,7 @@ import FileUploaderInAttachmentWrapper from '@/app/components/base/file-uploader
import type { FileEntity, FileUpload } from '@/app/components/base/file-uploader-in-attachment/types'
import { getProcessedFiles } from '@/app/components/base/file-uploader-in-attachment/utils'
export type IChatProps = {
export interface IChatProps {
chatList: ChatItem[]
/**
* Whether to display the editing area and rating status
@ -97,8 +97,7 @@ const Chat: FC<IChatProps> = ({
const [attachmentFiles, setAttachmentFiles] = React.useState<FileEntity[]>([])
const handleSend = () => {
if (!valid() || (checkCanSend && !checkCanSend()))
return
if (!valid() || (checkCanSend && !checkCanSend())) { return }
const imageFiles: VisionFile[] = files.filter(file => file.progress !== -1).map(fileItem => ({
type: 'image',
transfer_method: fileItem.type,
@ -109,23 +108,20 @@ const Chat: FC<IChatProps> = ({
const combinedFiles: VisionFile[] = [...imageFiles, ...docAndOtherFiles]
onSend(queryRef.current, combinedFiles)
if (!files.find(item => item.type === TransferMethod.local_file && !item.fileId)) {
if (files.length)
onClear()
if (files.length) { onClear() }
if (!isResponding) {
setQuery('')
queryRef.current = ''
}
}
if (!attachmentFiles.find(item => item.transferMethod === TransferMethod.local_file && !item.uploadedId))
setAttachmentFiles([])
if (!attachmentFiles.find(item => item.transferMethod === TransferMethod.local_file && !item.uploadedId)) { setAttachmentFiles([]) }
}
const handleKeyUp = (e: any) => {
if (e.code === 'Enter') {
e.preventDefault()
// prevent send message when using input method enter
if (!e.shiftKey && !isUseInputMethod.current)
handleSend()
if (!e.shiftKey && !isUseInputMethod.current) { handleSend() }
}
}

View File

@ -3,7 +3,7 @@ import type { FC } from 'react'
import React from 'react'
import s from './style.module.css'
export type ILoaidingAnimProps = {
export interface ILoaidingAnimProps {
type: 'text' | 'avatar'
}

View File

@ -5,7 +5,7 @@ import type { ThoughtItem, ToolInfoInThought } from '../type'
import Tool from './tool'
import type { Emoji } from '@/types/tools'
export type IThoughtProps = {
export interface IThoughtProps {
thought: ThoughtItem
allToolIcons: Record<string, string | Emoji>
isFinished: boolean
@ -29,8 +29,7 @@ const Thought: FC<IThoughtProps> = ({
}) => {
const [toolNames, isValueArray]: [string[], boolean] = (() => {
try {
if (Array.isArray(JSON.parse(thought.tool)))
return [JSON.parse(thought.tool), true]
if (Array.isArray(JSON.parse(thought.tool))) { return [JSON.parse(thought.tool), true] }
}
catch (e) {
}

View File

@ -3,7 +3,7 @@ import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
type Props = {
interface Props {
isRequest: boolean
toolName: string
content: string

View File

@ -13,17 +13,15 @@ import DataSetIcon from '@/app/components/base/icons/public/data-set'
import type { Emoji } from '@/types/tools'
import AppIcon from '@/app/components/base/app-icon'
type Props = {
interface Props {
payload: ToolInfoInThought
allToolIcons?: Record<string, string | Emoji>
}
const getIcon = (toolName: string, allToolIcons: Record<string, string | Emoji>) => {
if (toolName.startsWith('dataset-'))
return <DataSetIcon className='shrink-0'></DataSetIcon>
if (toolName.startsWith('dataset-')) { return <DataSetIcon className='shrink-0'></DataSetIcon> }
const icon = allToolIcons[toolName]
if (!icon)
return null
if (!icon) { return null }
return (
typeof icon === 'string'
? (
@ -87,12 +85,14 @@ const Tool: FC<Props> = ({
<Panel
isRequest={true}
toolName={toolName}
content={input} />
content={input}
/>
{output && (
<Panel
isRequest={false}
toolName={toolName}
content={output as string} />
content={output as string}
/>
)}
</div>
)}

View File

@ -1,6 +1,6 @@
import type { VisionFile } from '@/types/app'
export type LogAnnotation = {
export interface LogAnnotation {
content: string
account: {
id: string
@ -10,7 +10,7 @@ export type LogAnnotation = {
created_at: number
}
export type Annotation = {
export interface Annotation {
id: string
authorName: string
logAnnotation?: LogAnnotation
@ -20,13 +20,13 @@ export type Annotation = {
export const MessageRatings = ['like', 'dislike', null] as const
export type MessageRating = typeof MessageRatings[number]
export type MessageMore = {
export interface MessageMore {
time: string
tokens: number
latency: number | string
}
export type Feedbacktype = {
export interface Feedbacktype {
rating: MessageRating
content?: string | null
}
@ -36,14 +36,14 @@ export type SubmitAnnotationFunc = (messageId: string, content: string) => Promi
export type DisplayScene = 'web' | 'console'
export type ToolInfoInThought = {
export interface ToolInfoInThought {
name: string
input: string
output: string
isFinished: boolean
}
export type ThoughtItem = {
export interface ThoughtItem {
id: string
tool: string // plugin or dataset. May has multi.
thought: string
@ -55,7 +55,7 @@ export type ThoughtItem = {
message_files?: VisionFile[]
}
export type CitationItem = {
export interface CitationItem {
content: string
data_source_type: string
dataset_name: string
@ -70,7 +70,7 @@ export type CitationItem = {
word_count: number
}
export type IChatItem = {
export interface IChatItem {
id: string
content: string
citation?: CitationItem[]
@ -98,12 +98,12 @@ export type IChatItem = {
useCurrentUserAvatar?: boolean
isOpeningStatement?: boolean
suggestedQuestions?: string[]
log?: { role: string; text: string }[]
log?: { role: string, text: string }[]
agent_thoughts?: ThoughtItem[]
message_files?: VisionFile[]
}
export type MessageEnd = {
export interface MessageEnd {
id: string
metadata: {
retriever_resources?: CitationItem[]
@ -117,14 +117,14 @@ export type MessageEnd = {
}
}
export type MessageReplace = {
export interface MessageReplace {
id: string
task_id: string
answer: string
conversation_id: string
}
export type AnnotationReply = {
export interface AnnotationReply {
id: string
task_id: string
answer: string

View File

@ -5,7 +5,7 @@ import {
PencilSquareIcon,
} from '@heroicons/react/24/solid'
import AppIcon from '@/app/components/base/app-icon'
export type IHeaderProps = {
export interface IHeaderProps {
title: string
isMobile?: boolean
onShowSideBar?: () => void
@ -35,9 +35,7 @@ const Header: FC<IHeaderProps> = ({
</div>
{isMobile
? (
<div className='flex items-center justify-center h-8 w-8 cursor-pointer'
onClick={() => onCreateNewChat?.()}
>
<div className='flex items-center justify-center h-8 w-8 cursor-pointer' onClick={() => onCreateNewChat?.()} >
<PencilSquareIcon className="h-4 w-4 text-gray-500" />
</div>)
: <div></div>}

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
'use client'
import type { FC } from 'react'
import React, { useEffect, useRef, useState } from 'react'
@ -24,7 +23,7 @@ import { API_KEY, APP_ID, APP_INFO, isShowPrompt, promptTemplate } from '@/confi
import type { Annotation as AnnotationType } from '@/types/log'
import { addFileInfos, sortAgentSorts } from '@/utils/tools'
export type IMainProps = {
export interface IMainProps {
params: any
}
@ -52,8 +51,7 @@ const Main: FC<IMainProps> = () => {
const [fileConfig, setFileConfig] = useState<FileUpload | undefined>()
useEffect(() => {
if (APP_INFO?.title)
document.title = `${APP_INFO.title} - Powered by Dify`
if (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
@ -95,8 +93,7 @@ const Main: FC<IMainProps> = () => {
setChatList(generateNewChatListWithOpenStatement('', inputs))
}
const hasSetInputs = (() => {
if (!isNewConversation)
return true
if (!isNewConversation) { return true }
return isChatStarted
})()
@ -106,8 +103,7 @@ const Main: FC<IMainProps> = () => {
const suggestedQuestions = currConversationInfo?.suggested_questions || []
const handleConversationSwitch = () => {
if (!inited)
return
if (!inited) { return }
// update inputs of current conversation
let notSyncToStateIntroduction = ''
@ -155,8 +151,7 @@ const Main: FC<IMainProps> = () => {
})
}
if (isNewConversation && isChatStarted)
setChatList(generateNewChatListWithOpenStatement())
if (isNewConversation && isChatStarted) { setChatList(generateNewChatListWithOpenStatement()) }
}
useEffect(handleConversationSwitch, [currConversationId, inited])
@ -180,15 +175,13 @@ const Main: FC<IMainProps> = () => {
const chatListDomRef = useRef<HTMLDivElement>(null)
useEffect(() => {
// scroll to bottom
if (chatListDomRef.current)
chatListDomRef.current.scrollTop = chatListDomRef.current.scrollHeight
if (chatListDomRef.current) { chatListDomRef.current.scrollTop = chatListDomRef.current.scrollHeight }
}, [chatList, currConversationId])
// user can not edit inputs if user had send message
const canEditInputs = !chatList.some(item => item.isAnswer === false) && isNewConversation
const createNewChat = () => {
// if new chat is already exist, do not create new chat
if (conversationList.some(item => item.id === '-1'))
return
if (conversationList.some(item => item.id === '-1')) { return }
setConversationList(produce(conversationList, (draft) => {
draft.unshift({
@ -205,8 +198,7 @@ const Main: FC<IMainProps> = () => {
const generateNewChatListWithOpenStatement = (introduction?: string, inputs?: Record<string, any> | null) => {
let calculatedIntroduction = introduction || conversationIntroduction || ''
const calculatedPromptVariables = inputs || currInputs || null
if (calculatedIntroduction && calculatedPromptVariables)
calculatedIntroduction = replaceVarWithValues(calculatedIntroduction, promptConfig?.prompt_variables || [], calculatedPromptVariables)
if (calculatedIntroduction && calculatedPromptVariables) { calculatedIntroduction = replaceVarWithValues(calculatedIntroduction, promptConfig?.prompt_variables || [], calculatedPromptVariables) }
const openStatement = {
id: `${Date.now()}`,
@ -214,10 +206,9 @@ const Main: FC<IMainProps> = () => {
isAnswer: true,
feedbackDisabled: true,
isOpeningStatement: isShowPrompt,
suggestedQuestions: suggestedQuestions,
suggestedQuestions,
}
if (calculatedIntroduction)
return [openStatement]
if (calculatedIntroduction) { return [openStatement] }
return []
}
@ -232,7 +223,7 @@ const Main: FC<IMainProps> = () => {
try {
const [conversationData, appParams] = await Promise.all([fetchConversations(), fetchAppParams()])
// handle current conversation id
const { data: conversations, error } = conversationData as { data: ConversationItem[]; error: string }
const { data: conversations, error } = conversationData as { data: ConversationItem[], error: string }
if (error) {
Toast.notify({ type: 'error', message: error })
throw new Error(error)
@ -248,13 +239,13 @@ const Main: FC<IMainProps> = () => {
setNewConversationInfo({
name: t('app.chat.newChatDefaultName'),
introduction,
suggested_questions
suggested_questions,
})
if (isNotNewConversation) {
setExistConversationInfo({
name: currentConversation.name || t('app.chat.newChatDefaultName'),
introduction,
suggested_questions
suggested_questions,
})
}
const prompt_variables = userInputsFormToPromptVariables(user_input_form)
@ -278,8 +269,7 @@ const Main: FC<IMainProps> = () => {
})
setConversationList(conversations as ConversationItem[])
if (isNotNewConversation)
setCurrConversationId(_conversationId, APP_ID, false)
if (isNotNewConversation) { setCurrConversationId(_conversationId, APP_ID, false) }
setInited(true)
}
@ -303,11 +293,9 @@ const Main: FC<IMainProps> = () => {
}
const checkCanSend = () => {
if (currConversationId !== '-1')
return true
if (currConversationId !== '-1') { return true }
if (!currInputs || !promptConfig?.prompt_variables)
return true
if (!currInputs || !promptConfig?.prompt_variables) { return true }
const inputLens = Object.values(currInputs).length
const promptVariablesLens = promptConfig.prompt_variables.length
@ -342,11 +330,11 @@ const Main: FC<IMainProps> = () => {
const newListWithAnswer = produce(
getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
(draft) => {
if (!draft.find(item => item.id === questionId))
draft.push({ ...questionItem })
if (!draft.find(item => item.id === questionId)) { draft.push({ ...questionItem }) }
draft.push({ ...responseItem })
})
},
)
setChatList(newListWithAnswer)
}
@ -368,14 +356,11 @@ const Main: FC<IMainProps> = () => {
if (currInputs) {
Object.keys(currInputs).forEach((key) => {
const value = currInputs[key]
if (value.supportFileType)
toServerInputs[key] = transformToServerFile(value)
if (value.supportFileType) { toServerInputs[key] = transformToServerFile(value) }
else if (value[0]?.supportFileType)
toServerInputs[key] = value.map((item: any) => transformToServerFile(item))
else if (value[0]?.supportFileType) { toServerInputs[key] = value.map((item: any) => transformToServerFile(item)) }
else
toServerInputs[key] = value
else { toServerInputs[key] = value }
})
}
@ -442,16 +427,14 @@ const Main: FC<IMainProps> = () => {
}
else {
const lastThought = responseItem.agent_thoughts?.[responseItem.agent_thoughts?.length - 1]
if (lastThought)
lastThought.thought = lastThought.thought + message // need immer setAutoFreeze
if (lastThought) { lastThought.thought = lastThought.thought + message } // need immer setAutoFreeze
}
if (messageId && !hasSetResponseId) {
responseItem.id = messageId
hasSetResponseId = true
}
if (isFirstMessage && newConversationId)
tempNewConversationId = newConversationId
if (isFirstMessage && newConversationId) { tempNewConversationId = newConversationId }
setMessageTaskId(taskId)
// has switched to other conversation
@ -467,8 +450,7 @@ const Main: FC<IMainProps> = () => {
})
},
async onCompleted(hasError?: boolean) {
if (hasError)
return
if (hasError) { return }
if (getConversationIdChangeBecauseOfNew()) {
const { data: allConversations }: any = await fetchConversations()
@ -487,8 +469,7 @@ const Main: FC<IMainProps> = () => {
},
onFile(file) {
const lastThought = responseItem.agent_thoughts?.[responseItem.agent_thoughts?.length - 1]
if (lastThought)
lastThought.message_files = [...(lastThought as any).message_files, { ...file }]
if (lastThought) { lastThought.message_files = [...(lastThought as any).message_files, { ...file }] }
updateCurrentQA({
responseItem,
@ -543,13 +524,13 @@ const Main: FC<IMainProps> = () => {
const newListWithAnswer = produce(
getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
(draft) => {
if (!draft.find(item => item.id === questionId))
draft.push({ ...questionItem })
if (!draft.find(item => item.id === questionId)) { draft.push({ ...questionItem }) }
draft.push({
...responseItem,
})
})
},
)
setChatList(newListWithAnswer)
return
}
@ -558,11 +539,11 @@ const Main: FC<IMainProps> = () => {
const newListWithAnswer = produce(
getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
(draft) => {
if (!draft.find(item => item.id === questionId))
draft.push({ ...questionItem })
if (!draft.find(item => item.id === questionId)) { draft.push({ ...questionItem }) }
draft.push({ ...responseItem })
})
},
)
setChatList(newListWithAnswer)
},
onMessageReplace: (messageReplace) => {
@ -571,8 +552,7 @@ const Main: FC<IMainProps> = () => {
(draft) => {
const current = draft.find(item => item.id === messageReplace.id)
if (current)
current.content = messageReplace.answer
if (current) { current.content = messageReplace.answer }
},
))
},
@ -648,8 +628,7 @@ const Main: FC<IMainProps> = () => {
}
const renderSidebar = () => {
if (!APP_ID || !APP_INFO || !promptConfig)
return null
if (!APP_ID || !APP_INFO || !promptConfig) { return null }
return (
<Sidebar
list={conversationList}
@ -660,11 +639,9 @@ const Main: FC<IMainProps> = () => {
)
}
if (appUnavailable)
return <AppUnavailable isUnknownReason={isUnknownReason} errMessage={!hasSetAppConfig ? 'Please set APP_ID and API_KEY in config/index.tsx' : ''} />
if (appUnavailable) { return <AppUnavailable isUnknownReason={isUnknownReason} errMessage={!hasSetAppConfig ? 'Please set APP_ID and API_KEY in config/index.tsx' : ''} /> }
if (!APP_ID || !APP_INFO || !promptConfig)
return <Loading type='app' />
if (!APP_ID || !APP_INFO || !promptConfig) { return <Loading type='app' /> }
return (
<div className='bg-gray-100'>
@ -678,10 +655,7 @@ const Main: FC<IMainProps> = () => {
{/* sidebar */}
{!isMobile && renderSidebar()}
{isMobile && isShowSidebar && (
<div className='fixed inset-0 z-50'
style={{ backgroundColor: 'rgba(35, 56, 118, 0.2)' }}
onClick={hideSidebar}
>
<div className='fixed inset-0 z-50' style={{ backgroundColor: 'rgba(35, 56, 118, 0.2)' }} onClick={hideSidebar} >
<div className='inline-block' onClick={e => e.stopPropagation()}>
{renderSidebar()}
</div>

View File

@ -2,7 +2,7 @@ import React from 'react'
import { useTranslation } from 'react-i18next'
import s from './card.module.css'
type PropType = {
interface PropType {
children: React.ReactNode
text?: string
}

View File

@ -16,7 +16,7 @@ function classNames(...classes: any[]) {
const MAX_CONVERSATION_LENTH = 20
export type ISidebarProps = {
export interface ISidebarProps {
copyRight: string
currentId: string
onCurrentIdChange: (id: string) => void
@ -38,7 +38,8 @@ const Sidebar: FC<ISidebarProps> = ({
<div className="flex flex-shrink-0 p-4 !pb-0">
<Button
onClick={() => { onCurrentIdChange('-1') }}
className="group block w-full flex-shrink-0 !justify-start !h-9 text-primary-600 items-center text-sm">
className="group block w-full flex-shrink-0 !justify-start !h-9 text-primary-600 items-center text-sm"
>
<PencilSquareIcon className="mr-2 h-4 w-4" /> {t('app.chat.newChat')}
</Button>
</div>

View File

@ -7,7 +7,7 @@ import s from './style.module.css'
import { StarIcon } from '@/app/components//welcome/massive-component'
import Button from '@/app/components/base/button'
export type ITemplateVarPanelProps = {
export interface ITemplateVarPanelProps {
className?: string
header: ReactNode
children?: ReactNode | null
@ -38,7 +38,7 @@ const TemplateVarPanel: FC<ITemplateVarPanelProps> = ({
)
}
export const PanelTitle: FC<{ title: string; className?: string }> = ({
export const PanelTitle: FC<{ title: string, className?: string }> = ({
title,
className,
}) => {
@ -50,7 +50,7 @@ export const PanelTitle: FC<{ title: string; className?: string }> = ({
)
}
export const VarOpBtnGroup: FC<{ className?: string; onConfirm: () => void; onCancel: () => void }> = ({
export const VarOpBtnGroup: FC<{ className?: string, onConfirm: () => void, onCancel: () => void }> = ({
className,
onConfirm,
onCancel,

View File

@ -14,7 +14,7 @@ import { DEFAULT_VALUE_MAX_LEN } from '@/config'
// regex to match the {{}} and replace it with a span
const regex = /\{\{([^}]+)\}\}/g
export type IWelcomeProps = {
export interface IWelcomeProps {
conversationName: string
hasSetInputs: boolean
isPublicVersion: boolean
@ -42,8 +42,7 @@ const Welcome: FC<IWelcomeProps> = ({
const hasVar = promptConfig.prompt_variables.length > 0
const [isFold, setIsFold] = useState<boolean>(true)
const [inputs, setInputs] = useState<Record<string, any>>((() => {
if (hasSetInputs)
return savedInputs
if (hasSetInputs) { return savedInputs }
const res: Record<string, any> = {}
if (promptConfig) {
@ -69,8 +68,7 @@ const Welcome: FC<IWelcomeProps> = ({
}, [savedInputs])
const highLightPromoptTemplate = (() => {
if (!promptConfig)
return ''
if (!promptConfig) { return '' }
const res = promptConfig.prompt_template.replace(regex, (match, p1) => {
return `<span class='text-gray-800 font-bold'>${inputs?.[p1] ? inputs?.[p1] : match}</span>`
})
@ -189,8 +187,7 @@ const Welcome: FC<IWelcomeProps> = ({
}
const handleChat = () => {
if (!canChat())
return
if (!canChat()) { return }
onStartChat(inputs)
}
@ -251,8 +248,7 @@ const Welcome: FC<IWelcomeProps> = ({
return (
<VarOpBtnGroup
onConfirm={() => {
if (!canChat())
return
if (!canChat()) { return }
onInputsChange(inputs)
setIsFold(true)
@ -309,8 +305,7 @@ const Welcome: FC<IWelcomeProps> = ({
}
const renderHasSetInputsPrivate = () => {
if (!canEditInputs || !hasVar)
return null
if (!canEditInputs || !hasVar) { return null }
return (
<TemplateVarPanel
@ -333,8 +328,7 @@ const Welcome: FC<IWelcomeProps> = ({
}
const renderHasSetInputs = () => {
if ((!isPublicVersion && !canEditInputs) || !hasVar)
return null
if ((!isPublicVersion && !canEditInputs) || !hasVar) { return null }
return (
<div
@ -375,7 +369,8 @@ const Welcome: FC<IWelcomeProps> = ({
<a
className='text-gray-500'
href={siteInfo.privacy_policy}
target='_blank'>{t('app.chat.privacyPolicyMiddle')}</a>
target='_blank'
>{t('app.chat.privacyPolicyMiddle')}</a>
{t('app.chat.privacyPolicyRight')}
</div>
: <div>

View File

@ -37,7 +37,7 @@ export const StarIcon = () => (
</svg>
)
export const ChatBtn: FC<{ onClick: () => void; className?: string }> = ({
export const ChatBtn: FC<{ onClick: () => void, className?: string }> = ({
className,
onClick,
}) => {
@ -46,7 +46,8 @@ export const ChatBtn: FC<{ onClick: () => void; className?: string }> = ({
<Button
type='primary'
className={cn(className, `space-x-2 flex items-center ${s.customBtn}`)}
onClick={onClick}>
onClick={onClick}
>
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M18 10.5C18 14.366 14.418 17.5 10 17.5C8.58005 17.506 7.17955 17.1698 5.917 16.52L2 17.5L3.338 14.377C2.493 13.267 2 11.934 2 10.5C2 6.634 5.582 3.5 10 3.5C14.418 3.5 18 6.634 18 10.5ZM7 9.5H5V11.5H7V9.5ZM15 9.5H13V11.5H15V9.5ZM9 9.5H11V11.5H9V9.5Z" fill="white" />
</svg>
@ -55,7 +56,7 @@ export const ChatBtn: FC<{ onClick: () => void; className?: string }> = ({
)
}
export const EditBtn = ({ className, onClick }: { className?: string; onClick: () => void }) => {
export const EditBtn = ({ className, onClick }: { className?: string, onClick: () => void }) => {
const { t } = useTranslation()
return (

View File

@ -16,11 +16,11 @@ import {
} from '@/app/components/base/icons/workflow'
import AppIcon from '@/app/components/base/app-icon'
type BlockIconProps = {
interface BlockIconProps {
type: BlockEnum
size?: string
className?: string
toolIcon?: string | { content: string; background: string }
toolIcon?: string | { content: string, background: string }
}
const ICON_CONTAINER_CLASSNAME_SIZE_MAP: Record<string, string> = {
xs: 'w-4 h-4 rounded-[5px] shadow-xs',

View File

@ -9,7 +9,7 @@ import './style.css'
// load file from local instead of cdn https://github.com/suren-atoyan/monaco-react/issues/482
loader.config({ paths: { vs: '/vs' } })
type Props = {
interface Props {
value?: string | object
onChange?: (value: string) => void
title: JSX.Element
@ -72,8 +72,7 @@ const CodeEditor: FC<Props> = ({
}
const outPutValue = (() => {
if (!isJSONStringifyBeauty)
return value as string
if (!isJSONStringifyBeauty) { return value as string }
try {
return JSON.stringify(value as object, null, 2)
}

View File

@ -8,7 +8,7 @@ import ToggleExpandBtn from './toggle-expand-btn'
import useToggleExpend from './use-toggle-expend'
import { Clipboard, ClipboardCheck } from '@/app/components/base/icons/line/files'
type Props = {
interface Props {
className?: string
title: JSX.Element | string
headerRight?: JSX.Element

View File

@ -4,7 +4,7 @@ import type { FC } from 'react'
import { useDebounceFn } from 'ahooks'
import cn from 'classnames'
type Props = {
interface Props {
className?: string
height: number
minHeight: number
@ -40,14 +40,12 @@ const PromptEditorHeightResizeWrap: FC<Props> = ({
}, [prevUserSelectStyle])
const { run: didHandleResize } = useDebounceFn((e) => {
if (!isResizing)
return
if (!isResizing) { return }
const offset = e.clientY - clientY
let newHeight = height + offset
setClientY(e.clientY)
if (newHeight < minHeight)
newHeight = minHeight
if (newHeight < minHeight) { newHeight = minHeight }
onHeightChange(newHeight)
}, {
wait: 0,
@ -85,7 +83,8 @@ const PromptEditorHeightResizeWrap: FC<Props> = ({
{!hideResize && (
<div
className='absolute bottom-0 left-0 w-full flex justify-center h-2 cursor-row-resize'
onMouseDown={handleStartResize}>
onMouseDown={handleStartResize}
>
<div className='w-5 h-[3px] rounded-sm bg-gray-300'></div>
</div>
)}

View File

@ -4,7 +4,7 @@ import React, { useCallback } from 'react'
import Expand04 from '@/app/components/base/icons/solid/expand-04'
import Collapse04 from '@/app/components/base/icons/line/arrows/collapse-04'
type Props = {
interface Props {
isExpand: boolean
onExpandChange: (isExpand: boolean) => void
}

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'
type Params = {
interface Params {
ref: React.RefObject<HTMLDivElement>
hasFooter?: boolean
}

View File

@ -9,7 +9,7 @@ import Loading02 from '@/app/components/base/icons/line/loading-02'
import CheckCircle from '@/app/components/base/icons/line/check-circle'
import type { NodeTracing } from '@/types/app'
type Props = {
interface Props {
nodeInfo: NodeTracing
hideInfo?: boolean
}
@ -18,20 +18,15 @@ const NodePanel: FC<Props> = ({ nodeInfo, hideInfo = false }) => {
const [collapseState, setCollapseState] = useState<boolean>(true)
const getTime = (time: number) => {
if (time < 1)
return `${(time * 1000).toFixed(3)} ms`
if (time > 60)
return `${parseInt(Math.round(time / 60).toString())} m ${(time % 60).toFixed(3)} s`
if (time < 1) { return `${(time * 1000).toFixed(3)} ms` }
if (time > 60) { return `${parseInt(Math.round(time / 60).toString())} m ${(time % 60).toFixed(3)} s` }
return `${time.toFixed(3)} s`
}
const getTokenCount = (tokens: number) => {
if (tokens < 1000)
return tokens
if (tokens >= 1000 && tokens < 1000000)
return `${parseFloat((tokens / 1000).toFixed(3))}K`
if (tokens >= 1000000)
return `${parseFloat((tokens / 1000000).toFixed(3))}M`
if (tokens < 1000) { return tokens }
if (tokens >= 1000 && tokens < 1000000) { return `${parseFloat((tokens / 1000).toFixed(3))}K` }
if (tokens >= 1000000) { return `${parseFloat((tokens / 1000000).toFixed(3))}M` }
}
useEffect(() => {

View File

@ -12,7 +12,7 @@ import Loading02 from '@/app/components/base/icons/line/loading-02'
import ChevronRight from '@/app/components/base/icons/line/chevron-right'
import { WorkflowRunningStatus } from '@/types/app'
type WorkflowProcessProps = {
interface WorkflowProcessProps {
data: WorkflowProcess
grayBg?: boolean
expand?: boolean
@ -30,14 +30,11 @@ const WorkflowProcessItem = ({
const failed = data.status === WorkflowRunningStatus.Failed || data.status === WorkflowRunningStatus.Stopped
const background = useMemo(() => {
if (running && !collapse)
return 'linear-gradient(180deg, #E1E4EA 0%, #EAECF0 100%)'
if (running && !collapse) { return 'linear-gradient(180deg, #E1E4EA 0%, #EAECF0 100%)' }
if (succeeded && !collapse)
return 'linear-gradient(180deg, #ECFDF3 0%, #F6FEF9 100%)'
if (succeeded && !collapse) { return 'linear-gradient(180deg, #ECFDF3 0%, #F6FEF9 100%)' }
if (failed && !collapse)
return 'linear-gradient(180deg, #FEE4E2 0%, #FEF3F2 100%)'
if (failed && !collapse) { return 'linear-gradient(180deg, #FEE4E2 0%, #FEF3F2 100%)' }
}, [running, succeeded, failed, collapse])
useEffect(() => {

View File

@ -2,6 +2,8 @@
@tailwind components;
@tailwind utilities;
@source "../../node_modules/streamdown/dist/index.js";
:root {
--max-width: 1100px;
--border-radius: 12px;

67
eslint.config.mjs Normal file
View File

@ -0,0 +1,67 @@
import { combine, javascript, typescript, stylistic } from '@antfu/eslint-config'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
export default combine(
javascript({
overrides: {
'no-unused-vars': 'off',
'no-console': 'off',
},
}),
typescript(),
stylistic({
lessOpinionated: true,
jsx: false,
semi: false,
quotes: 'single',
overrides: {
'style/indent': ['error', 2],
'style/quotes': ['error', 'single'],
'style/max-statements-per-line': 'off',
},
}),
{
plugins: {
'react-hooks': reactHooks,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-hooks/exhaustive-deps': 'warn',
'unused-imports/no-unused-vars': 'warn',
'unused-imports/no-unused-imports': 'warn',
'@typescript-eslint/no-use-before-define': 'off',
'ts/no-use-before-define': 'off',
'style/brace-style': 'off',
},
},
{
ignores: [
'**/node_modules/**',
'**/dist/**',
'**/build/**',
'**/out/**',
'**/.next/**',
'**/public/**',
'**/*.json',
'tailwind.config.js',
'next.config.js',
],
},
{
languageOptions: {
globals: {
...globals.browser,
...globals.es2025,
...globals.node,
React: 'readable',
JSX: 'readable',
},
},
},
)

View File

@ -10,10 +10,8 @@ export enum MediaType {
const useBreakpoints = () => {
const [width, setWidth] = React.useState(globalThis.innerWidth)
const media = (() => {
if (width <= 640)
return MediaType.mobile
if (width <= 768)
return MediaType.tablet
if (width <= 640) { return MediaType.mobile }
if (width <= 768) { return MediaType.tablet }
return MediaType.pc
})()

View File

@ -30,8 +30,7 @@ function useConversation() {
// input can be updated by user
const [newConversationInputs, setNewConversationInputs] = useState<Record<string, any> | null>(null)
const resetNewConversationInputs = () => {
if (!newConversationInputs)
return
if (!newConversationInputs) { return }
setNewConversationInputs(produce(newConversationInputs, (draft) => {
Object.keys(draft).forEach((key) => {
draft[key] = ''

View File

@ -12,6 +12,5 @@ export const getLocaleOnClient = (): Locale => {
export const setLocaleOnClient = (locale: Locale, notReload?: boolean) => {
Cookies.set(LOCALE_COOKIE_NAME, locale)
changeLanguage(locale)
if (!notReload)
location.reload()
if (!notReload) { location.reload() }
}

View File

@ -33,4 +33,4 @@ const translation = {
},
}
export default translation;
export default translation

View File

@ -100,4 +100,4 @@ const translation = {
notAuthorized: 'Công cụ chưa được ủy quyền',
}
export default translation;
export default translation

View File

@ -7,9 +7,9 @@
"build": "next build",
"start": "next start",
"lint": "next lint",
"fix": "next lint --fix",
"fix": "eslint . --fix",
"eslint-fix": "eslint . --fix",
"prepare": "husky install ./.husky"
"prepare": "husky"
},
"dependencies": {
"@floating-ui/react": "^0.26.25",
@ -41,10 +41,10 @@
"lodash-es": "^4.17.21",
"mime": "^4.0.7",
"negotiator": "^0.6.3",
"next": "^14.2.32",
"next": "^15.5.2",
"rc-textarea": "^1.5.3",
"react": "~18.3.0",
"react-dom": "~18.3.0",
"react": "~19.1.1",
"react-dom": "~19.1.1",
"react-error-boundary": "^4.0.2",
"react-headless-pagination": "^1.1.4",
"react-i18next": "^12.2.0",
@ -58,15 +58,16 @@
"sass": "^1.61.0",
"scheduler": "^0.23.0",
"server-only": "^0.0.1",
"streamdown": "^1.2.0",
"swr": "^2.3.0",
"tailwind-merge": "^3.2.0",
"typescript": "4.9.5",
"use-context-selector": "^2.0.0",
"uuid": "^10.0.0",
"zustand": "^4.5.2"
},
"devDependencies": {
"@antfu/eslint-config": "~5.2.2",
"@eslint-react/eslint-plugin": "^1.53.0",
"@faker-js/faker": "^9.0.3",
"@tailwindcss/typography": "^0.5.9",
"@types/js-cookie": "^3.0.6",
@ -74,10 +75,14 @@
"@types/negotiator": "^0.6.3",
"autoprefixer": "^10.4.20",
"eslint": "~9.35.0",
"eslint-plugin-format": "^1.0.1",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.4.0",
"lint-staged": "^15.2.10",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.14"
"tailwindcss": "^3.4.14",
"typescript": "5.9.2"
},
"lint-staged": {
"**/*.js?(x)": [

View File

@ -22,7 +22,7 @@ const baseOptions = {
redirect: 'follow',
}
export type WorkflowStartedResponse = {
export interface WorkflowStartedResponse {
task_id: string
workflow_run_id: string
event: string
@ -34,7 +34,7 @@ export type WorkflowStartedResponse = {
}
}
export type WorkflowFinishedResponse = {
export interface WorkflowFinishedResponse {
task_id: string
workflow_run_id: string
event: string
@ -52,7 +52,7 @@ export type WorkflowFinishedResponse = {
}
}
export type NodeStartedResponse = {
export interface NodeStartedResponse {
task_id: string
workflow_run_id: string
event: string
@ -68,7 +68,7 @@ export type NodeStartedResponse = {
}
}
export type NodeFinishedResponse = {
export interface NodeFinishedResponse {
task_id: string
workflow_run_id: string
event: string
@ -93,7 +93,7 @@ export type NodeFinishedResponse = {
}
}
export type IOnDataMoreInfo = {
export interface IOnDataMoreInfo {
conversationId?: string
taskId?: string
messageId: string
@ -114,7 +114,7 @@ export type IOnWorkflowFinished = (workflowFinished: WorkflowFinishedResponse) =
export type IOnNodeStarted = (nodeStarted: NodeStartedResponse) => void
export type IOnNodeFinished = (nodeFinished: NodeFinishedResponse) => void
type IOtherOptions = {
interface IOtherOptions {
isPublicAPI?: boolean
bodyStringify?: boolean
needAllResponseContent?: boolean
@ -152,8 +152,7 @@ const handleStream = (
onNodeStarted?: IOnNodeStarted,
onNodeFinished?: IOnNodeFinished,
) => {
if (!response.ok)
throw new Error('Network response was not ok')
if (!response.ok) { throw new Error('Network response was not ok') }
const reader = response.body?.getReader()
const decoder = new TextDecoder('utf-8')
@ -241,8 +240,7 @@ const handleStream = (
onCompleted?.(true)
return
}
if (!hasError)
read()
if (!hasError) { read() }
})
}
read()
@ -262,17 +260,14 @@ const baseFetch = (url: string, fetchOptions: any, { needAllResponseContent }: I
Object.keys(params).forEach(key =>
paramsArray.push(`${key}=${encodeURIComponent(params[key])}`),
)
if (urlWithPrefix.search(/\?/) === -1)
urlWithPrefix += `?${paramsArray.join('&')}`
if (urlWithPrefix.search(/\?/) === -1) { urlWithPrefix += `?${paramsArray.join('&')}` }
else
urlWithPrefix += `&${paramsArray.join('&')}`
else { urlWithPrefix += `&${paramsArray.join('&')}` }
delete options.params
}
if (body)
options.body = JSON.stringify(body)
if (body) { options.body = JSON.stringify(body) }
// Handle timeout
return Promise.race([
@ -344,16 +339,13 @@ export const upload = (fetchOptions: any): Promise<any> => {
return new Promise((resolve, reject) => {
const xhr = options.xhr
xhr.open(options.method, options.url)
for (const key in options.headers)
xhr.setRequestHeader(key, options.headers[key])
for (const key in options.headers) { xhr.setRequestHeader(key, options.headers[key]) }
xhr.withCredentials = true
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200)
resolve({ id: xhr.response })
else
reject(xhr)
if (xhr.status === 200) { resolve({ id: xhr.response }) }
else { reject(xhr) }
}
}
xhr.upload.onprogress = options.onprogress
@ -386,8 +378,7 @@ export const ssePost = (
const urlWithPrefix = `${urlPrefix}${url.startsWith('/') ? url : `/${url}`}`
const { body } = options
if (body)
options.body = JSON.stringify(body)
if (body) { options.body = JSON.stringify(body) }
globalThis.fetch(urlWithPrefix, options)
.then((res: any) => {
@ -410,7 +401,8 @@ export const ssePost = (
}, () => {
onCompleted?.()
}, onThought, onMessageEnd, onMessageReplace, onFile, onWorkflowStarted, onWorkflowFinished, onNodeStarted, onNodeFinished)
}).catch((e) => {
})
.catch((e) => {
Toast.notify({ type: 'error', message: e })
onError?.(e)
})

View File

@ -53,7 +53,7 @@ export const fetchAppParams = async () => {
return get('parameters')
}
export const updateFeedback = async ({ url, body }: { url: string; body: Feedbacktype }) => {
export const updateFeedback = async ({ url, body }: { url: string, body: Feedbacktype }) => {
return post(url, { body })
}

View File

@ -2,7 +2,7 @@ import type { Annotation } from './log'
import type { Locale } from '@/i18n'
import type { ThoughtItem } from '@/app/components/chat/type'
export type PromptVariable = {
export interface PromptVariable {
key: string
name: string
type: string
@ -15,19 +15,19 @@ export type PromptVariable = {
allowed_file_upload_methods?: TransferMethod[]
}
export type PromptConfig = {
export interface PromptConfig {
prompt_template: string
prompt_variables: PromptVariable[]
}
export type TextTypeFormItem = {
export interface TextTypeFormItem {
label: string
variable: string
required: boolean
max_length: number
}
export type SelectTypeFormItem = {
export interface SelectTypeFormItem {
label: string
variable: string
required: boolean
@ -39,26 +39,26 @@ export type SelectTypeFormItem = {
export type UserInputFormItem = {
'text-input': TextTypeFormItem
} | {
'select': SelectTypeFormItem
select: SelectTypeFormItem
} | {
'paragraph': TextTypeFormItem
paragraph: TextTypeFormItem
}
export const MessageRatings = ['like', 'dislike', null] as const
export type MessageRating = typeof MessageRatings[number]
export type Feedbacktype = {
export interface Feedbacktype {
rating: MessageRating
content?: string | null
}
export type MessageMore = {
export interface MessageMore {
time: string
tokens: number
latency: number | string
}
export type IChatItem = {
export interface IChatItem {
id: string
content: string
/**
@ -85,7 +85,7 @@ export type IChatItem = {
useCurrentUserAvatar?: boolean
isOpeningStatement?: boolean
suggestedQuestions?: string[]
log?: { role: string; text: string }[]
log?: { role: string, text: string }[]
agent_thoughts?: ThoughtItem[]
message_files?: VisionFile[]
}
@ -96,17 +96,17 @@ export type ChatItem = IChatItem & {
workflowProcess?: WorkflowProcess
}
export type ResponseHolder = {}
export interface ResponseHolder {}
export type ConversationItem = {
export interface ConversationItem {
id: string
name: string
inputs: Record<string, any> | null
introduction: string,
introduction: string
suggested_questions?: string[]
}
export type AppInfo = {
export interface AppInfo {
title: string
description: string
default_language: Locale
@ -125,7 +125,7 @@ export enum TransferMethod {
remote_url = 'remote_url',
}
export type VisionSettings = {
export interface VisionSettings {
enabled: boolean
number_limits: number
detail: Resolution
@ -133,7 +133,7 @@ export type VisionSettings = {
image_file_size_limit?: number | string
}
export type ImageFile = {
export interface ImageFile {
type: TransferMethod
_id: string
fileId: string
@ -144,7 +144,7 @@ export type ImageFile = {
deleted?: boolean
}
export type VisionFile = {
export interface VisionFile {
id?: string
type: string
transfer_method: TransferMethod
@ -168,7 +168,7 @@ export enum BlockEnum {
Tool = 'tool',
}
export type NodeTracing = {
export interface NodeTracing {
id: string
index: number
predecessor_node_id: string
@ -213,7 +213,7 @@ export enum WorkflowRunningStatus {
Stopped = 'stopped',
}
export type WorkflowProcess = {
export interface WorkflowProcess {
status: WorkflowRunningStatus
tracing: NodeTracing[]
expand?: boolean // for UI

View File

@ -1,5 +1,5 @@
export type TypeWithI18N<T = string> = {
'en_US': T
'zh_Hans': T
export interface TypeWithI18N<T = string> {
en_US: T
zh_Hans: T
[key: string]: T
}

View File

@ -1,4 +1,4 @@
export type LogAnnotation = {
export interface LogAnnotation {
content: string
account: {
id: string
@ -8,7 +8,7 @@ export type LogAnnotation = {
created_at: number
}
export type Annotation = {
export interface Annotation {
id: string
authorName: string
logAnnotation?: LogAnnotation

View File

@ -9,10 +9,10 @@ export enum AuthType {
apiKey = 'api_key',
}
export type Credential = {
'auth_type': AuthType
'api_key_header'?: string
'api_key_value'?: string
export interface Credential {
auth_type: AuthType
api_key_header?: string
api_key_value?: string
}
export enum CollectionType {
@ -21,12 +21,12 @@ export enum CollectionType {
custom = 'api',
}
export type Emoji = {
export interface Emoji {
background: string
content: string
}
export type Collection = {
export interface Collection {
id: string
name: string
author: string
@ -39,7 +39,7 @@ export type Collection = {
allow_delete: boolean
}
export type ToolParameter = {
export interface ToolParameter {
name: string
label: TypeWithI18N
human_description: TypeWithI18N
@ -52,14 +52,14 @@ export type ToolParameter = {
}[]
}
export type Tool = {
export interface Tool {
name: string
label: TypeWithI18N
description: any
parameters: ToolParameter[]
}
export type ToolCredential = {
export interface ToolCredential {
name: string
label: TypeWithI18N
help: TypeWithI18N
@ -73,7 +73,7 @@ export type ToolCredential = {
}[]
}
export type CustomCollectionBackend = {
export interface CustomCollectionBackend {
provider: string
original_provider?: string
credentials: Credential
@ -84,7 +84,7 @@ export type CustomCollectionBackend = {
tools?: ParamItem[]
}
export type ParamItem = {
export interface ParamItem {
name: string
label: TypeWithI18N
human_description: TypeWithI18N
@ -99,7 +99,7 @@ export type ParamItem = {
}[]
}
export type CustomParamSchema = {
export interface CustomParamSchema {
operation_id: string // name
summary: string
server_url: string

View File

@ -4,8 +4,7 @@
* @example formatNumber(1234567.89) will return '1,234,567.89'
*/
export const formatNumber = (num: number | string) => {
if (!num)
return num
if (!num) { return num }
const parts = num.toString().split('.')
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',')
return parts.join('.')
@ -18,8 +17,7 @@ export const formatNumber = (num: number | string) => {
* @example formatFileSize(1024 * 1024) will return '1.00MB'
*/
export const formatFileSize = (fileSize: number) => {
if (!fileSize)
return fileSize
if (!fileSize) { return fileSize }
const units = ['', 'K', 'M', 'G', 'T', 'P']
let index = 0
while (fileSize >= 1024 && index < units.length) {
@ -35,8 +33,7 @@ export const formatFileSize = (fileSize: number) => {
* @example formatTime(60 * 60) will return '1.00 h'
*/
export const formatTime = (seconds: number) => {
if (!seconds)
return seconds
if (!seconds) { return seconds }
const units = ['sec', 'min', 'h']
let index = 0
while (seconds >= 60 && index < units.length) {
@ -46,7 +43,7 @@ export const formatTime = (seconds: number) => {
return `${seconds.toFixed(2)} ${units[index]}`
}
export const downloadFile = ({ data, fileName }: { data: Blob; fileName: string }) => {
export const downloadFile = ({ data, fileName }: { data: Blob, fileName: string }) => {
const url = window.URL.createObjectURL(data)
const a = document.createElement('a')
a.href = url

View File

@ -3,8 +3,7 @@ import type { PromptVariable, UserInputFormItem } from '@/types/app'
export function replaceVarWithValues(str: string, promptVariables: PromptVariable[], inputs: Record<string, any>) {
return str.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
const name = inputs[key]
if (name)
return name
if (name) { return name }
const valueObj: PromptVariable | undefined = promptVariables.find(v => v.key === key)
return valueObj ? `{{${valueObj.key}}}` : match
@ -12,8 +11,7 @@ export function replaceVarWithValues(str: string, promptVariables: PromptVariabl
}
export const userInputsFormToPromptVariables = (useInputs: UserInputFormItem[] | null) => {
if (!useInputs)
return []
if (!useInputs) { return [] }
const promptVariables: PromptVariable[] = []
useInputs.forEach((item: any) => {
const [type, content] = (() => {

View File

@ -1,6 +1,6 @@
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_'
export function randomString(length: number) {
let result = ''
for (let i = length; i > 0; --i) result += chars[Math.floor(Math.random() * chars.length)]
for (let i = length; i > 0; --i) { result += chars[Math.floor(Math.random() * chars.length)] }
return result
}

View File

@ -2,18 +2,15 @@ import type { ThoughtItem } from '@/app/components/chat/type'
import type { VisionFile } from '@/types/app'
export const sortAgentSorts = (list: ThoughtItem[]) => {
if (!list)
return list
if (list.some(item => item.position === undefined))
return list
if (!list) { return list }
if (list.some(item => item.position === undefined)) { return list }
const temp = [...list]
temp.sort((a, b) => a.position - b.position)
return temp
}
export const addFileInfos = (list: ThoughtItem[], messageFiles: VisionFile[]) => {
if (!list || !messageFiles)
return list
if (!list || !messageFiles) { return list }
return list.map((item) => {
if (item.files && item.files?.length > 0) {
return {