mirror of
https://github.com/langgenius/dify.git
synced 2026-07-26 06:08:38 +08:00
248 lines
6.9 KiB
TypeScript
248 lines
6.9 KiB
TypeScript
import type { Area } from 'react-easy-crop'
|
|
import type { OnImageInput } from './ImageInput'
|
|
import type { AppIconType, ImageFile } from '@/types/app'
|
|
import { Button } from '@langgenius/dify-ui/button'
|
|
import { cn } from '@langgenius/dify-ui/cn'
|
|
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
|
|
import { RiImageCircleAiLine } from '@remixicon/react'
|
|
import { useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { DISABLE_UPLOAD_IMAGE_AS_ICON } from '@/config'
|
|
import Divider from '../divider'
|
|
import { defaultEmojiBackground } from '../emoji-picker/constants'
|
|
import EmojiPickerInner from '../emoji-picker/Inner'
|
|
import { useLocalFileUploader } from '../image-uploader/hooks'
|
|
import ImageInput from './ImageInput'
|
|
import s from './style.module.css'
|
|
import getCroppedImg from './utils'
|
|
|
|
export type AppIconEmojiSelection = {
|
|
type: 'emoji'
|
|
icon: string
|
|
background: string
|
|
}
|
|
|
|
export type AppIconImageSelection = {
|
|
type: 'image'
|
|
fileId: string
|
|
url: string
|
|
}
|
|
|
|
export type AppIconSelection = AppIconEmojiSelection | AppIconImageSelection
|
|
|
|
type AppIconPickerProps = {
|
|
open: boolean
|
|
onOpenChange: (open: boolean) => void
|
|
onSelect?: (payload: AppIconSelection) => void
|
|
enableImageUpload?: boolean
|
|
initialEmoji?: {
|
|
icon: string
|
|
background?: string | null
|
|
}
|
|
className?: string
|
|
}
|
|
|
|
function AppIconPicker({
|
|
open,
|
|
onOpenChange,
|
|
onSelect,
|
|
enableImageUpload = true,
|
|
initialEmoji,
|
|
className,
|
|
}: AppIconPickerProps) {
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
{open ? (
|
|
<AppIconPickerContent
|
|
key={`${initialEmoji?.icon ?? ''}:${initialEmoji?.background ?? ''}`}
|
|
initialEmoji={initialEmoji}
|
|
enableImageUpload={enableImageUpload}
|
|
className={className}
|
|
onOpenChange={onOpenChange}
|
|
onSelect={onSelect}
|
|
/>
|
|
) : null}
|
|
</Dialog>
|
|
)
|
|
}
|
|
|
|
type AppIconPickerContentProps = {
|
|
className?: string
|
|
initialEmoji?: {
|
|
icon: string
|
|
background?: string | null
|
|
}
|
|
enableImageUpload: boolean
|
|
onOpenChange: (open: boolean) => void
|
|
onSelect?: (payload: AppIconSelection) => void
|
|
}
|
|
|
|
function AppIconPickerContent({
|
|
className,
|
|
initialEmoji,
|
|
enableImageUpload,
|
|
onOpenChange,
|
|
onSelect,
|
|
}: AppIconPickerContentProps) {
|
|
const { t } = useTranslation()
|
|
|
|
const tabs = [
|
|
{
|
|
key: 'emoji',
|
|
label: t(($) => $['iconPicker.emoji'], { ns: 'app' }),
|
|
icon: <span className="text-lg">🤖</span>,
|
|
},
|
|
{
|
|
key: 'image',
|
|
label: t(($) => $['iconPicker.image'], { ns: 'app' }),
|
|
icon: <RiImageCircleAiLine className="size-4" />,
|
|
},
|
|
]
|
|
const [activeTab, setActiveTab] = useState<AppIconType>('emoji')
|
|
const showImageUpload = enableImageUpload && !DISABLE_UPLOAD_IMAGE_AS_ICON
|
|
|
|
const [emoji, setEmoji] = useState<{ emoji: string; background: string } | undefined>(() => {
|
|
if (!initialEmoji?.icon) return undefined
|
|
|
|
return {
|
|
emoji: initialEmoji.icon,
|
|
background: initialEmoji.background ?? defaultEmojiBackground,
|
|
}
|
|
})
|
|
|
|
const [uploading, setUploading] = useState<boolean>()
|
|
|
|
const { handleLocalFileUpload } = useLocalFileUploader({
|
|
limit: 3,
|
|
disabled: false,
|
|
onUpload: (imageFile: ImageFile) => {
|
|
if (imageFile.fileId) {
|
|
setUploading(false)
|
|
onSelect?.({
|
|
type: 'image',
|
|
fileId: imageFile.fileId,
|
|
url: imageFile.url,
|
|
})
|
|
onOpenChange(false)
|
|
}
|
|
},
|
|
})
|
|
|
|
type InputImageInfo =
|
|
| { file: File }
|
|
| { tempUrl: string; croppedAreaPixels: Area; fileName: string }
|
|
const [inputImageInfo, setInputImageInfo] = useState<InputImageInfo>()
|
|
|
|
const handleImageInput: OnImageInput = async (
|
|
isCropped: boolean,
|
|
fileOrTempUrl: string | File,
|
|
croppedAreaPixels?: Area,
|
|
fileName?: string,
|
|
) => {
|
|
setInputImageInfo(
|
|
isCropped
|
|
? {
|
|
tempUrl: fileOrTempUrl as string,
|
|
croppedAreaPixels: croppedAreaPixels!,
|
|
fileName: fileName!,
|
|
}
|
|
: { file: fileOrTempUrl as File },
|
|
)
|
|
}
|
|
|
|
const handleSelect = async () => {
|
|
if (activeTab === 'emoji') {
|
|
if (emoji) {
|
|
onSelect?.({
|
|
type: 'emoji',
|
|
icon: emoji.emoji,
|
|
background: emoji.background,
|
|
})
|
|
onOpenChange(false)
|
|
}
|
|
} else {
|
|
if (!inputImageInfo) return
|
|
setUploading(true)
|
|
if ('file' in inputImageInfo) {
|
|
handleLocalFileUpload(inputImageInfo.file)
|
|
return
|
|
}
|
|
const blob = await getCroppedImg(
|
|
inputImageInfo.tempUrl,
|
|
inputImageInfo.croppedAreaPixels,
|
|
inputImageInfo.fileName,
|
|
)
|
|
const file = new File([blob], inputImageInfo.fileName, { type: blob.type })
|
|
handleLocalFileUpload(file)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<DialogContent
|
|
className={cn(
|
|
'w-full overflow-hidden! border-none text-left align-middle',
|
|
s.container,
|
|
'h-[min(462px,calc(100dvh-2rem))]! max-h-none! w-[362px]! p-0!',
|
|
className,
|
|
)}
|
|
>
|
|
<DialogTitle className="sr-only">
|
|
{t(($) => $['iconPicker.emoji'], { ns: 'app' })}
|
|
</DialogTitle>
|
|
|
|
{showImageUpload && (
|
|
<div className="w-full p-2 pb-0">
|
|
<div className="flex items-center justify-center gap-2 rounded-xl bg-background-body p-1 text-text-primary">
|
|
{tabs.map((tab) => (
|
|
<button
|
|
type="button"
|
|
key={tab.key}
|
|
className={cn(
|
|
'flex h-8 flex-1 shrink-0 items-center justify-center rounded-lg p-2 system-sm-medium text-text-tertiary',
|
|
activeTab === tab.key &&
|
|
'bg-components-main-nav-nav-button-bg-active text-text-accent shadow-md',
|
|
)}
|
|
onClick={() => setActiveTab(tab.key as AppIconType)}
|
|
>
|
|
{tab.icon}
|
|
{tab.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'emoji' && (
|
|
<EmojiPickerInner
|
|
className={cn('flex-1 overflow-hidden pt-2')}
|
|
emoji={initialEmoji?.icon}
|
|
background={initialEmoji?.background ?? undefined}
|
|
onSelect={(emoji, background) => setEmoji({ emoji, background })}
|
|
/>
|
|
)}
|
|
{activeTab === 'image' && (
|
|
<ImageInput className={cn('flex-1 overflow-hidden')} onImageInput={handleImageInput} />
|
|
)}
|
|
|
|
<Divider className="m-0" />
|
|
<div className="flex w-full items-center justify-center gap-2 p-3">
|
|
<Button className="w-full" onClick={() => onOpenChange(false)}>
|
|
{t(($) => $['iconPicker.cancel'], { ns: 'app' })}
|
|
</Button>
|
|
|
|
<Button
|
|
variant="primary"
|
|
className="w-full"
|
|
disabled={uploading}
|
|
loading={uploading}
|
|
onClick={handleSelect}
|
|
>
|
|
{t(($) => $['iconPicker.ok'], { ns: 'app' })}
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
)
|
|
}
|
|
|
|
export default AppIconPicker
|