mirror of
https://gitee.com/dify_ai/dify.git
synced 2024-11-30 02:08:37 +08:00
feat: [frontend] support vision (#1518)
Co-authored-by: Joel <iamjoel007@gmail.com>
This commit is contained in:
parent
41d0a8b295
commit
6b15827246
@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||
import Textarea from 'rc-textarea'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import cn from 'classnames'
|
||||
import Recorder from 'js-audio-recorder'
|
||||
@ -10,9 +11,8 @@ import type { DisplayScene, FeedbackFunc, IChatItem, SubmitAnnotationFunc } from
|
||||
import { TryToAskIcon, stopIcon } from './icon-component'
|
||||
import Answer from './answer'
|
||||
import Question from './question'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import AutoHeightTextarea from '@/app/components/base/auto-height-textarea'
|
||||
import Button from '@/app/components/base/button'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import VoiceInput from '@/app/components/base/voice-input'
|
||||
@ -20,6 +20,10 @@ import { Microphone01 } from '@/app/components/base/icons/src/vender/line/mediaA
|
||||
import { Microphone01 as Microphone01Solid } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
|
||||
import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import ChatImageUploader from '@/app/components/base/image-uploader/chat-image-uploader'
|
||||
import ImageList from '@/app/components/base/image-uploader/image-list'
|
||||
import { TransferMethod, type VisionFile, type VisionSettings } from '@/types/app'
|
||||
import { useImageFiles } from '@/app/components/base/image-uploader/hooks'
|
||||
|
||||
export type IChatProps = {
|
||||
configElem?: React.ReactNode
|
||||
@ -37,7 +41,7 @@ export type IChatProps = {
|
||||
onFeedback?: FeedbackFunc
|
||||
onSubmitAnnotation?: SubmitAnnotationFunc
|
||||
checkCanSend?: () => boolean
|
||||
onSend?: (message: string) => void
|
||||
onSend?: (message: string, files: VisionFile[]) => void
|
||||
displayScene?: DisplayScene
|
||||
useCurrentUserAvatar?: boolean
|
||||
isResponsing?: boolean
|
||||
@ -54,6 +58,7 @@ export type IChatProps = {
|
||||
dataSets?: DataSet[]
|
||||
isShowCitationHitInfo?: boolean
|
||||
isShowPromptLog?: boolean
|
||||
visionConfig?: VisionSettings
|
||||
}
|
||||
|
||||
const Chat: FC<IChatProps> = ({
|
||||
@ -83,9 +88,19 @@ const Chat: FC<IChatProps> = ({
|
||||
dataSets,
|
||||
isShowCitationHitInfo,
|
||||
isShowPromptLog,
|
||||
visionConfig,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const {
|
||||
files,
|
||||
onUpload,
|
||||
onRemove,
|
||||
onReUpload,
|
||||
onImageLinkLoadError,
|
||||
onImageLinkLoadSuccess,
|
||||
onClear,
|
||||
} = useImageFiles()
|
||||
const isUseInputMethod = useRef(false)
|
||||
|
||||
const [query, setQuery] = React.useState('')
|
||||
@ -114,9 +129,18 @@ const Chat: FC<IChatProps> = ({
|
||||
const handleSend = () => {
|
||||
if (!valid() || (checkCanSend && !checkCanSend()))
|
||||
return
|
||||
onSend(query)
|
||||
if (!isResponsing)
|
||||
setQuery('')
|
||||
onSend(query, files.filter(file => file.progress !== -1).map(fileItem => ({
|
||||
type: 'image',
|
||||
transfer_method: fileItem.type,
|
||||
url: fileItem.url,
|
||||
upload_file_id: fileItem.fileId,
|
||||
})))
|
||||
if (!files.find(item => item.type === TransferMethod.local_file && !item.fileId)) {
|
||||
if (files.length)
|
||||
onClear()
|
||||
if (!isResponsing)
|
||||
setQuery('')
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyUp = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
@ -198,6 +222,8 @@ const Chat: FC<IChatProps> = ({
|
||||
item={item}
|
||||
isShowPromptLog={isShowPromptLog}
|
||||
isResponsing={isResponsing}
|
||||
// ['https://placekitten.com/360/360', 'https://placekitten.com/360/640']
|
||||
imgSrcs={(item.message_files && item.message_files?.length > 0) ? item.message_files.map(item => item.url) : []}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
@ -246,18 +272,42 @@ const Chat: FC<IChatProps> = ({
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
<div className="relative">
|
||||
<AutoHeightTextarea
|
||||
<div className='p-[5.5px] max-h-[150px] bg-white border-[1.5px] border-gray-200 rounded-xl overflow-y-auto'>
|
||||
{
|
||||
visionConfig?.enabled && (
|
||||
<>
|
||||
<div className='absolute bottom-2 left-2 flex items-center'>
|
||||
<ChatImageUploader
|
||||
settings={visionConfig}
|
||||
onUpload={onUpload}
|
||||
disabled={files.length >= visionConfig.number_limits}
|
||||
/>
|
||||
<div className='mx-1 w-[1px] h-4 bg-black/5' />
|
||||
</div>
|
||||
<div className='pl-[52px]'>
|
||||
<ImageList
|
||||
list={files}
|
||||
onRemove={onRemove}
|
||||
onReUpload={onReUpload}
|
||||
onImageLinkLoadSuccess={onImageLinkLoadSuccess}
|
||||
onImageLinkLoadError={onImageLinkLoadError}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<Textarea
|
||||
className={`
|
||||
block w-full px-2 pr-[118px] py-[7px] leading-5 max-h-none text-sm text-gray-700 outline-none appearance-none resize-none
|
||||
${visionConfig?.enabled && 'pl-12'}
|
||||
`}
|
||||
value={query}
|
||||
onChange={handleContentChange}
|
||||
onKeyUp={handleKeyUp}
|
||||
onKeyDown={handleKeyDown}
|
||||
minHeight={48}
|
||||
autoFocus
|
||||
controlFocus={controlFocus}
|
||||
className={`${cn(s.textArea)} resize-none block w-full pl-3 bg-gray-50 border border-gray-200 rounded-md focus:outline-none sm:text-sm text-gray-700`}
|
||||
autoSize
|
||||
/>
|
||||
<div className="absolute top-0 right-2 flex items-center h-[48px]">
|
||||
<div className="absolute bottom-2 right-2 flex items-center h-8">
|
||||
<div className={`${s.count} mr-4 h-5 leading-5 text-sm bg-gray-50 text-gray-500`}>{query.trim().length}</div>
|
||||
{
|
||||
query
|
||||
@ -282,9 +332,8 @@ const Chat: FC<IChatProps> = ({
|
||||
{isMobile
|
||||
? sendBtn
|
||||
: (
|
||||
<Tooltip
|
||||
selector='send-tip'
|
||||
htmlContent={
|
||||
<TooltipPlus
|
||||
popupContent={
|
||||
<div>
|
||||
<div>{t('common.operation.send')} Enter</div>
|
||||
<div>{t('common.operation.lineBreak')} Shift Enter</div>
|
||||
@ -292,7 +341,7 @@ const Chat: FC<IChatProps> = ({
|
||||
}
|
||||
>
|
||||
{sendBtn}
|
||||
</Tooltip>
|
||||
</TooltipPlus>
|
||||
)}
|
||||
</div>
|
||||
{
|
||||
|
@ -8,14 +8,16 @@ import Log from '../log'
|
||||
import MoreInfo from '../more-info'
|
||||
import AppContext from '@/context/app-context'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import ImageGallery from '@/app/components/base/image-gallery'
|
||||
|
||||
type IQuestionProps = Pick<IChatItem, 'id' | 'content' | 'more' | 'useCurrentUserAvatar'> & {
|
||||
isShowPromptLog?: boolean
|
||||
item: IChatItem
|
||||
imgSrcs?: string[]
|
||||
isResponsing?: boolean
|
||||
}
|
||||
|
||||
const Question: FC<IQuestionProps> = ({ id, content, more, useCurrentUserAvatar, isShowPromptLog, item, isResponsing }) => {
|
||||
const Question: FC<IQuestionProps> = ({ id, content, imgSrcs, more, useCurrentUserAvatar, isShowPromptLog, item, isResponsing }) => {
|
||||
const { userProfile } = useContext(AppContext)
|
||||
const userName = userProfile?.name
|
||||
const ref = useRef(null)
|
||||
@ -23,6 +25,7 @@ const Question: FC<IQuestionProps> = ({ id, content, more, useCurrentUserAvatar,
|
||||
return (
|
||||
<div className={`flex items-start justify-end ${isShowPromptLog && 'first-of-type:pt-[14px]'}`} key={id} ref={ref}>
|
||||
<div className={s.questionWrapWrap}>
|
||||
|
||||
<div className={`${s.question} group relative text-sm text-gray-900`}>
|
||||
{
|
||||
isShowPromptLog && !isResponsing && (
|
||||
@ -32,6 +35,9 @@ const Question: FC<IQuestionProps> = ({ id, content, more, useCurrentUserAvatar,
|
||||
<div
|
||||
className={'mr-2 py-3 px-4 bg-blue-500 rounded-tl-2xl rounded-b-2xl'}
|
||||
>
|
||||
{imgSrcs && imgSrcs.length > 0 && (
|
||||
<ImageGallery srcs={imgSrcs} />
|
||||
)}
|
||||
<Markdown content={content} />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import type { Annotation, MessageRating } from '@/models/log'
|
||||
|
||||
import type { VisionFile } from '@/types/app'
|
||||
export type MessageMore = {
|
||||
time: string
|
||||
tokens: number
|
||||
@ -67,6 +67,7 @@ export type IChatItem = {
|
||||
useCurrentUserAvatar?: boolean
|
||||
isOpeningStatement?: boolean
|
||||
log?: { role: string; text: string }[]
|
||||
message_files?: VisionFile[]
|
||||
}
|
||||
|
||||
export type MessageEnd = {
|
||||
|
@ -33,7 +33,7 @@ export type IConfigModelProps = {
|
||||
mode: string
|
||||
modelId: string
|
||||
provider: ProviderEnum
|
||||
setModel: (model: { id: string; provider: ProviderEnum; mode: ModelModeType }) => void
|
||||
setModel: (model: { id: string; provider: ProviderEnum; mode: ModelModeType; features: string[] }) => void
|
||||
completionParams: CompletionParams
|
||||
onCompletionParamsChange: (newParams: CompletionParams) => void
|
||||
disabled: boolean
|
||||
@ -121,7 +121,7 @@ const ConfigModel: FC<IConfigModelProps> = ({
|
||||
return adjustedValue
|
||||
}
|
||||
|
||||
const handleSelectModel = ({ id, provider: nextProvider, mode }: { id: string; provider: ProviderEnum; mode: ModelModeType }) => {
|
||||
const handleSelectModel = ({ id, provider: nextProvider, mode, features }: { id: string; provider: ProviderEnum; mode: ModelModeType; features: string[] }) => {
|
||||
return async () => {
|
||||
const prevParamsRule = getAllParams()[provider]?.[modelId]
|
||||
|
||||
@ -129,6 +129,7 @@ const ConfigModel: FC<IConfigModelProps> = ({
|
||||
id,
|
||||
provider: nextProvider || ProviderEnum.openai,
|
||||
mode,
|
||||
features,
|
||||
})
|
||||
|
||||
await ensureModelParamLoaded(nextProvider, id)
|
||||
@ -320,6 +321,7 @@ const ConfigModel: FC<IConfigModelProps> = ({
|
||||
id: model.model_name,
|
||||
provider: model.model_provider.provider_name as ProviderEnum,
|
||||
mode: model.model_mode,
|
||||
features: model.features,
|
||||
})()
|
||||
}}
|
||||
/>
|
||||
|
@ -169,7 +169,7 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
|
||||
}
|
||||
title={
|
||||
<div className='flex items-center'>
|
||||
<div className='ml-1 mr-1'>{t('appDebug.variableTitle')}</div>
|
||||
<div className='mr-1'>{t('appDebug.variableTitle')}</div>
|
||||
{!readonly && (
|
||||
<Tooltip htmlContent={<div className='w-[180px]'>
|
||||
{t('appDebug.variableTip')}
|
||||
|
60
web/app/components/app/configuration/config-vision/index.tsx
Normal file
60
web/app/components/app/configuration/config-vision/index.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Panel from '../base/feature-panel'
|
||||
import ParamConfig from './param-config'
|
||||
import { HelpCircle } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import { Eye } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
|
||||
const ConfigVision: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
isShowVisionConfig,
|
||||
visionConfig,
|
||||
setVisionConfig,
|
||||
} = useContext(ConfigContext)
|
||||
|
||||
if (!isShowVisionConfig)
|
||||
return null
|
||||
|
||||
return (<>
|
||||
<Panel
|
||||
className="mt-4"
|
||||
headerIcon={
|
||||
<Eye className='w-4 h-4 text-[#6938EF]'/>
|
||||
}
|
||||
title={
|
||||
<div className='flex items-center'>
|
||||
<div className='mr-1'>{t('appDebug.vision.name')}</div>
|
||||
<Tooltip htmlContent={<div className='w-[180px]' >
|
||||
{t('appDebug.vision.description')}
|
||||
</div>} selector='config-vision-tooltip'>
|
||||
<HelpCircle className='w-[14px] h-[14px] text-gray-400' />
|
||||
</Tooltip>
|
||||
</div>
|
||||
}
|
||||
headerRight={
|
||||
<div className='flex items-center'>
|
||||
<ParamConfig />
|
||||
<div className='ml-4 mr-3 w-[1px] h-3.5 bg-gray-200'></div>
|
||||
<Switch
|
||||
defaultValue={visionConfig.enabled}
|
||||
onChange={value => setVisionConfig({
|
||||
...visionConfig,
|
||||
enabled: value,
|
||||
})}
|
||||
size='md'
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
noBodySpacing
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default React.memo(ConfigVision)
|
@ -0,0 +1,132 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import RadioGroup from './radio-group'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
import { Resolution, TransferMethod } from '@/types/app'
|
||||
import ParamItem from '@/app/components/base/param-item'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { HelpCircle } from '@/app/components/base/icons/src/vender/line/general'
|
||||
|
||||
const MIN = 1
|
||||
const MAX = 6
|
||||
const ParamConfigContent: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
visionConfig,
|
||||
setVisionConfig,
|
||||
} = useContext(ConfigContext)
|
||||
|
||||
const transferMethod = (() => {
|
||||
if (!visionConfig.transfer_methods || visionConfig.transfer_methods.length === 2)
|
||||
return TransferMethod.all
|
||||
|
||||
return visionConfig.transfer_methods[0]
|
||||
})()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<div className='leading-6 text-base font-semibold text-gray-800'>{t('appDebug.vision.visionSettings.title')}</div>
|
||||
<div className='pt-3 space-y-6'>
|
||||
<div>
|
||||
<div className='mb-2 flex items-center space-x-1'>
|
||||
<div className='leading-[18px] text-[13px] font-semibold text-gray-800'>{t('appDebug.vision.visionSettings.resolution')}</div>
|
||||
<Tooltip htmlContent={<div className='w-[180px]' >
|
||||
{t('appDebug.vision.visionSettings.resolutionTooltip').split('\n').map(item => (
|
||||
<div key={item}>{item}</div>
|
||||
))}
|
||||
</div>} selector='config-resolution-tooltip'>
|
||||
<HelpCircle className='w-[14px] h-[14px] text-gray-400' />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<RadioGroup
|
||||
className='space-x-3'
|
||||
options={[
|
||||
{
|
||||
label: t('appDebug.vision.visionSettings.high'),
|
||||
value: Resolution.high,
|
||||
},
|
||||
{
|
||||
label: t('appDebug.vision.visionSettings.low'),
|
||||
value: Resolution.low,
|
||||
},
|
||||
]}
|
||||
value={visionConfig.detail}
|
||||
onChange={(value: Resolution) => {
|
||||
setVisionConfig({
|
||||
...visionConfig,
|
||||
detail: value,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className='mb-2 leading-[18px] text-[13px] font-semibold text-gray-800'>{t('appDebug.vision.visionSettings.uploadMethod')}</div>
|
||||
<RadioGroup
|
||||
className='space-x-3'
|
||||
options={[
|
||||
{
|
||||
label: t('appDebug.vision.visionSettings.both'),
|
||||
value: TransferMethod.all,
|
||||
},
|
||||
{
|
||||
label: t('appDebug.vision.visionSettings.localUpload'),
|
||||
value: TransferMethod.local_file,
|
||||
},
|
||||
{
|
||||
label: t('appDebug.vision.visionSettings.url'),
|
||||
value: TransferMethod.remote_url,
|
||||
},
|
||||
]}
|
||||
value={transferMethod}
|
||||
onChange={(value: TransferMethod) => {
|
||||
if (value === TransferMethod.all) {
|
||||
setVisionConfig({
|
||||
...visionConfig,
|
||||
transfer_methods: [TransferMethod.remote_url, TransferMethod.local_file],
|
||||
})
|
||||
return
|
||||
}
|
||||
setVisionConfig({
|
||||
...visionConfig,
|
||||
transfer_methods: [value],
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<ParamItem
|
||||
id='upload_limit'
|
||||
className=''
|
||||
name={t('appDebug.vision.visionSettings.uploadLimit')}
|
||||
noTooltip
|
||||
{...{
|
||||
default: 2,
|
||||
step: 1,
|
||||
min: MIN,
|
||||
max: MAX,
|
||||
}}
|
||||
value={visionConfig.number_limits}
|
||||
enable={true}
|
||||
onChange={(_key: string, value: number) => {
|
||||
if (!value)
|
||||
return
|
||||
|
||||
setVisionConfig({
|
||||
...visionConfig,
|
||||
number_limits: value,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(ParamConfigContent)
|
@ -0,0 +1,41 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { memo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import ParamConfigContent from './param-config-content'
|
||||
import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
|
||||
const ParamsConfig: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-end'
|
||||
offset={{
|
||||
mainAxis: 4,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
|
||||
<div className={cn('flex items-center rounded-md h-7 px-3 space-x-1 text-gray-700 cursor-pointer hover:bg-gray-200', open && 'bg-gray-200')}>
|
||||
<Settings01 className='w-3.5 h-3.5 ' />
|
||||
<div className='ml-1 leading-[18px] text-xs font-medium '>{t('appDebug.vision.settings')}</div>
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent style={{ zIndex: 50 }}>
|
||||
<div className='w-[412px] p-4 bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg space-y-3'>
|
||||
<ParamConfigContent />
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
export default memo(ParamsConfig)
|
@ -0,0 +1,40 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import s from './style.module.css'
|
||||
|
||||
type OPTION = {
|
||||
label: string
|
||||
value: any
|
||||
}
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
options: OPTION[]
|
||||
value: any
|
||||
onChange: (value: any) => void
|
||||
}
|
||||
|
||||
const RadioGroup: FC<Props> = ({
|
||||
className = '',
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn(className, 'flex')}>
|
||||
{options.map(item => (
|
||||
<div
|
||||
key={item.value}
|
||||
className={cn(s.item, item.value === value && s.checked)}
|
||||
onClick={() => onChange(item.value)}
|
||||
>
|
||||
<div className={s.radio}></div>
|
||||
<div className='text-[13px] font-medium text-gray-900'>{item.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(RadioGroup)
|
@ -0,0 +1,24 @@
|
||||
.item {
|
||||
@apply grow flex items-center h-8 px-2.5 rounded-lg bg-gray-25 border border-gray-100 cursor-pointer space-x-2;
|
||||
}
|
||||
|
||||
.item:hover {
|
||||
background-color: #ffffff;
|
||||
border-color: #B2CCFF;
|
||||
box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
|
||||
}
|
||||
|
||||
.item.checked {
|
||||
background-color: #ffffff;
|
||||
border-color: #528BFF;
|
||||
box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.06), 0px 1px 3px 0px rgba(16, 24, 40, 0.10);
|
||||
}
|
||||
|
||||
.radio {
|
||||
@apply w-4 h-4 border-[2px] border-gray-200 rounded-full;
|
||||
}
|
||||
|
||||
.item.checked .radio {
|
||||
border-width: 5px;
|
||||
border-color: #155eef;
|
||||
}
|
@ -10,6 +10,7 @@ import ChatGroup from '../features/chat-group'
|
||||
import ExperienceEnchanceGroup from '../features/experience-enchance-group'
|
||||
import Toolbox from '../toolbox'
|
||||
import HistoryPanel from '../config-prompt/conversation-histroy/history-panel'
|
||||
import ConfigVision from '../config-vision'
|
||||
import AddFeatureBtn from './feature/add-feature-btn'
|
||||
import ChooseFeature from './feature/choose-feature'
|
||||
import useFeature from './feature/use-feature'
|
||||
@ -193,6 +194,8 @@ const Config: FC = () => {
|
||||
|
||||
<Tools />
|
||||
|
||||
<ConfigVision />
|
||||
|
||||
{/* Chat History */}
|
||||
{isAdvancedMode && isChatApp && modelModeType === ModelModeType.completion && (
|
||||
<HistoryPanel
|
||||
|
@ -81,7 +81,7 @@ const DatasetConfig: FC = () => {
|
||||
>
|
||||
{hasData
|
||||
? (
|
||||
<div className='flex flex-wrap mt-1 px-3 justify-between'>
|
||||
<div className='flex flex-wrap mt-1 px-3 pb-3 justify-between'>
|
||||
{dataSet.map(item => (
|
||||
<CardItem
|
||||
key={item.id}
|
||||
|
@ -1,5 +1,6 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import cn from 'classnames'
|
||||
@ -11,7 +12,7 @@ import HasNotSetAPIKEY from '../base/warning-mask/has-not-set-api'
|
||||
import FormattingChanged from '../base/warning-mask/formatting-changed'
|
||||
import GroupName from '../base/group-name'
|
||||
import CannotQueryDataset from '../base/warning-mask/cannot-query-dataset'
|
||||
import { AppType, ModelModeType } from '@/types/app'
|
||||
import { AppType, ModelModeType, TransferMethod } from '@/types/app'
|
||||
import PromptValuePanel, { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel'
|
||||
import type { IChatItem } from '@/app/components/app/chat/type'
|
||||
import Chat from '@/app/components/app/chat'
|
||||
@ -19,12 +20,13 @@ import ConfigContext from '@/context/debug-configuration'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { fetchConvesationMessages, fetchSuggestedQuestions, sendChatMessage, sendCompletionMessage, stopChatMessageResponding } from '@/service/debug'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type { ModelConfig as BackendModelConfig } from '@/types/app'
|
||||
import type { ModelConfig as BackendModelConfig, VisionFile } from '@/types/app'
|
||||
import { promptVariablesToUserInputsForm } from '@/utils/model-config'
|
||||
import TextGeneration from '@/app/components/app/text-generate/item'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import type { Inputs } from '@/models/debug'
|
||||
import { fetchFileUploadConfig } from '@/service/common'
|
||||
|
||||
type IDebug = {
|
||||
hasSetAPIKEY: boolean
|
||||
@ -64,10 +66,12 @@ const Debug: FC<IDebug> = ({
|
||||
hasSetContextVar,
|
||||
datasetConfigs,
|
||||
externalDataToolsConfig,
|
||||
visionConfig,
|
||||
} = useContext(ConfigContext)
|
||||
const { speech2textDefaultModel } = useProviderContext()
|
||||
const [chatList, setChatList, getChatList] = useGetState<IChatItem[]>([])
|
||||
const chatListDomRef = useRef<HTMLDivElement>(null)
|
||||
const { data: fileUploadConfigResponse } = useSWR({ url: '/files/upload' }, fetchFileUploadConfig)
|
||||
useEffect(() => {
|
||||
// scroll to bottom
|
||||
if (chatListDomRef.current)
|
||||
@ -161,17 +165,28 @@ const Debug: FC<IDebug> = ({
|
||||
logError(t('appDebug.errorMessage.valueOfVarRequired', { key: hasEmptyInput }))
|
||||
return false
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
if (completionFiles.find(item => item.transfer_method === TransferMethod.local_file && !item.upload_file_id)) {
|
||||
notify({ type: 'info', message: t('appDebug.errorMessage.waitForImgUpload') })
|
||||
return false
|
||||
}
|
||||
return !hasEmptyInput
|
||||
}
|
||||
|
||||
const doShowSuggestion = isShowSuggestion && !isResponsing
|
||||
const [suggestQuestions, setSuggestQuestions] = useState<string[]>([])
|
||||
const onSend = async (message: string) => {
|
||||
const onSend = async (message: string, files?: VisionFile[]) => {
|
||||
if (isResponsing) {
|
||||
notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') })
|
||||
return false
|
||||
}
|
||||
|
||||
if (files?.find(item => item.transfer_method === TransferMethod.local_file && !item.upload_file_id)) {
|
||||
notify({ type: 'info', message: t('appDebug.errorMessage.waitForImgUpload') })
|
||||
return false
|
||||
}
|
||||
|
||||
const postDatasets = dataSets.map(({ id }) => ({
|
||||
dataset: {
|
||||
enabled: true,
|
||||
@ -207,6 +222,9 @@ const Debug: FC<IDebug> = ({
|
||||
completion_params: completionParams as any,
|
||||
},
|
||||
dataset_configs: datasetConfigs,
|
||||
file_upload: {
|
||||
image: visionConfig,
|
||||
},
|
||||
}
|
||||
|
||||
if (isAdvancedMode) {
|
||||
@ -214,19 +232,32 @@ const Debug: FC<IDebug> = ({
|
||||
postModelConfig.completion_prompt_config = completionPromptConfig
|
||||
}
|
||||
|
||||
const data = {
|
||||
const data: Record<string, any> = {
|
||||
conversation_id: conversationId,
|
||||
inputs,
|
||||
query: message,
|
||||
model_config: postModelConfig,
|
||||
}
|
||||
|
||||
if (visionConfig.enabled && files && files?.length > 0) {
|
||||
data.files = files.map((item) => {
|
||||
if (item.transfer_method === TransferMethod.local_file) {
|
||||
return {
|
||||
...item,
|
||||
url: '',
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
// qustion
|
||||
const questionId = `question-${Date.now()}`
|
||||
const questionItem = {
|
||||
id: questionId,
|
||||
content: message,
|
||||
isAnswer: false,
|
||||
message_files: files,
|
||||
}
|
||||
|
||||
const placeholderAnswerId = `answer-placeholder-${Date.now()}`
|
||||
@ -347,6 +378,7 @@ const Debug: FC<IDebug> = ({
|
||||
const [completionRes, setCompletionRes] = useState('')
|
||||
const [messageId, setMessageId] = useState<string | null>(null)
|
||||
|
||||
const [completionFiles, setCompletionFiles] = useState<VisionFile[]>([])
|
||||
const sendTextCompletion = async () => {
|
||||
if (isResponsing) {
|
||||
notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') })
|
||||
@ -394,6 +426,9 @@ const Debug: FC<IDebug> = ({
|
||||
completion_params: completionParams as any,
|
||||
},
|
||||
dataset_configs: datasetConfigs,
|
||||
file_upload: {
|
||||
image: visionConfig,
|
||||
},
|
||||
}
|
||||
|
||||
if (isAdvancedMode) {
|
||||
@ -401,11 +436,23 @@ const Debug: FC<IDebug> = ({
|
||||
postModelConfig.completion_prompt_config = completionPromptConfig
|
||||
}
|
||||
|
||||
const data = {
|
||||
const data: Record<string, any> = {
|
||||
inputs,
|
||||
model_config: postModelConfig,
|
||||
}
|
||||
|
||||
if (visionConfig.enabled && completionFiles && completionFiles?.length > 0) {
|
||||
data.files = completionFiles.map((item) => {
|
||||
if (item.transfer_method === TransferMethod.local_file) {
|
||||
return {
|
||||
...item,
|
||||
url: '',
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
setCompletionRes('')
|
||||
setMessageId('')
|
||||
let res: string[] = []
|
||||
@ -448,6 +495,11 @@ const Debug: FC<IDebug> = ({
|
||||
appType={mode as AppType}
|
||||
onSend={sendTextCompletion}
|
||||
inputs={inputs}
|
||||
visionConfig={{
|
||||
...visionConfig,
|
||||
image_file_size_limit: fileUploadConfigResponse?.image_file_size_limit,
|
||||
}}
|
||||
onVisionFilesChange={setCompletionFiles}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col grow">
|
||||
@ -475,6 +527,10 @@ const Debug: FC<IDebug> = ({
|
||||
isShowCitation={citationConfig.enabled}
|
||||
isShowCitationHitInfo
|
||||
isShowPromptLog
|
||||
visionConfig={{
|
||||
...visionConfig,
|
||||
image_file_size_limit: fileUploadConfigResponse?.image_file_size_limit,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -25,19 +25,19 @@ import type {
|
||||
} from '@/models/debug'
|
||||
import type { ExternalDataTool } from '@/models/common'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import type { ModelConfig as BackendModelConfig } from '@/types/app'
|
||||
import type { ModelConfig as BackendModelConfig, VisionSettings } from '@/types/app'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
import ConfigModel from '@/app/components/app/configuration/config-model'
|
||||
import Config from '@/app/components/app/configuration/config'
|
||||
import Debug from '@/app/components/app/configuration/debug'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import { ProviderEnum } from '@/app/components/header/account-setting/model-page/declarations'
|
||||
import { ModelFeature, ProviderEnum } from '@/app/components/header/account-setting/model-page/declarations'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { fetchAppDetail, updateAppModelConfig } from '@/service/apps'
|
||||
import { promptVariablesToUserInputsForm, userInputsFormToPromptVariables } from '@/utils/model-config'
|
||||
import { fetchDatasets } from '@/service/datasets'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { AppType, ModelModeType } from '@/types/app'
|
||||
import { AppType, ModelModeType, Resolution, TransferMethod } from '@/types/app'
|
||||
import { FlipBackward } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import { PromptMode } from '@/models/debug'
|
||||
import { DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config'
|
||||
@ -198,6 +198,7 @@ const Configuration: FC = () => {
|
||||
}
|
||||
|
||||
const { textGenerationModelList } = useProviderContext()
|
||||
const currModel = textGenerationModelList.find(item => item.model_name === modelConfig.model_id)
|
||||
const hasSetCustomAPIKEY = !!textGenerationModelList?.find(({ model_provider: provider }) => {
|
||||
if (provider.provider_type === 'system' && provider.quota_type === 'paid')
|
||||
return true
|
||||
@ -271,7 +272,8 @@ const Configuration: FC = () => {
|
||||
id: modelId,
|
||||
provider,
|
||||
mode: modeMode,
|
||||
}: { id: string; provider: ProviderEnum; mode: ModelModeType }) => {
|
||||
features,
|
||||
}: { id: string; provider: ProviderEnum; mode: ModelModeType; features: string[] }) => {
|
||||
if (isAdvancedMode) {
|
||||
const appMode = mode
|
||||
|
||||
@ -297,10 +299,31 @@ const Configuration: FC = () => {
|
||||
})
|
||||
|
||||
setModelConfig(newModelConfig)
|
||||
const supportVision = features && features.includes(ModelFeature.vision)
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
setVisionConfig({
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
...visionConfig,
|
||||
enabled: supportVision,
|
||||
}, true)
|
||||
}
|
||||
|
||||
const isShowVisionConfig = !!currModel?.features.includes(ModelFeature.vision)
|
||||
const [visionConfig, doSetVisionConfig] = useState({
|
||||
enabled: false,
|
||||
number_limits: 2,
|
||||
detail: Resolution.low,
|
||||
transfer_methods: [TransferMethod.local_file],
|
||||
})
|
||||
|
||||
const setVisionConfig = (config: VisionSettings, notNoticeFormattingChanged?: boolean) => {
|
||||
doSetVisionConfig(config)
|
||||
if (!notNoticeFormattingChanged)
|
||||
setFormattingChanged(true)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchAppDetail({ url: '/apps', id: appId }).then(async (res) => {
|
||||
fetchAppDetail({ url: '/apps', id: appId }).then(async (res: any) => {
|
||||
setMode(res.mode)
|
||||
const modelConfig = res.model_config
|
||||
const promptMode = modelConfig.prompt_type === PromptMode.advanced ? PromptMode.advanced : PromptMode.simple
|
||||
@ -362,6 +385,10 @@ const Configuration: FC = () => {
|
||||
},
|
||||
completionParams: model.completion_params,
|
||||
}
|
||||
|
||||
if (modelConfig.file_upload)
|
||||
setVisionConfig(modelConfig.file_upload.image, true)
|
||||
|
||||
syncToPublishedConfig(config)
|
||||
setPublishedConfig(config)
|
||||
setDatasetConfigs(modelConfig.dataset_configs)
|
||||
@ -459,6 +486,9 @@ const Configuration: FC = () => {
|
||||
completion_params: completionParams as any,
|
||||
},
|
||||
dataset_configs: datasetConfigs,
|
||||
file_upload: {
|
||||
image: visionConfig,
|
||||
},
|
||||
}
|
||||
|
||||
if (isAdvancedMode) {
|
||||
@ -557,6 +587,9 @@ const Configuration: FC = () => {
|
||||
datasetConfigs,
|
||||
setDatasetConfigs,
|
||||
hasSetContextVar,
|
||||
isShowVisionConfig,
|
||||
visionConfig,
|
||||
setVisionConfig,
|
||||
}}
|
||||
>
|
||||
<>
|
||||
|
@ -14,17 +14,23 @@ import { DEFAULT_VALUE_MAX_LEN } from '@/config'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { ChevronDown, ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import Tooltip from '@/app/components/base/tooltip-plus'
|
||||
import TextGenerationImageUploader from '@/app/components/base/image-uploader/text-generation-image-uploader'
|
||||
import type { VisionFile, VisionSettings } from '@/types/app'
|
||||
|
||||
export type IPromptValuePanelProps = {
|
||||
appType: AppType
|
||||
onSend?: () => void
|
||||
inputs: Inputs
|
||||
visionConfig: VisionSettings
|
||||
onVisionFilesChange: (files: VisionFile[]) => void
|
||||
}
|
||||
|
||||
const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
appType,
|
||||
onSend,
|
||||
inputs,
|
||||
visionConfig,
|
||||
onVisionFilesChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { modelModeType, modelConfig, setInputs, mode, isAdvancedMode, completionPromptConfig, chatPromptConfig } = useContext(ConfigContext)
|
||||
@ -152,6 +158,24 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
<div className='text-xs text-gray-500'>{t('appDebug.inputs.noVar')}</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
appType === AppType.completion && visionConfig?.enabled && (
|
||||
<div className="mt-3 xl:flex justify-between">
|
||||
<div className="mr-1 py-2 shrink-0 w-[120px] text-sm text-gray-900">Image Upload</div>
|
||||
<div className='grow'>
|
||||
<TextGenerationImageUploader
|
||||
settings={visionConfig}
|
||||
onFilesChange={files => onVisionFilesChange(files.filter(file => file.progress !== -1).map(fileItem => ({
|
||||
type: 'image',
|
||||
transfer_method: fileItem.type,
|
||||
url: fileItem.url,
|
||||
upload_file_id: fileItem.fileId,
|
||||
})))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -33,7 +33,6 @@ import { TONE_LIST } from '@/config'
|
||||
import ModelIcon from '@/app/components/app/configuration/config-model/model-icon'
|
||||
import ModelName from '@/app/components/app/configuration/config-model/model-name'
|
||||
import ModelModeTypeLabel from '@/app/components/app/configuration/config-model/model-mode-type-label'
|
||||
import { ModelModeType } from '@/types/app'
|
||||
|
||||
type IConversationList = {
|
||||
logs?: ChatConversationsResponse | CompletionConversationsResponse
|
||||
@ -81,6 +80,7 @@ const getFormattedChatList = (messages: ChatMessage[]) => {
|
||||
content: item.inputs.query || item.inputs.default_input || item.query, // text generation: item.inputs.query; chat: item.query
|
||||
isAnswer: false,
|
||||
log: item.message as any,
|
||||
message_files: item.message_files,
|
||||
})
|
||||
|
||||
newChatList.push({
|
||||
@ -174,9 +174,12 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
|
||||
const itemContent = item[Object.keys(item)[0]]
|
||||
return {
|
||||
label: itemContent.variable,
|
||||
value: varValues[itemContent.variable],
|
||||
value: varValues[itemContent.variable] || detail.message?.inputs?.[itemContent.variable],
|
||||
}
|
||||
})
|
||||
const message_files = (!isChatMode && detail.message.message_files && detail.message.message_files.length > 0)
|
||||
? detail.message.message_files.map((item: any) => item.url)
|
||||
: []
|
||||
|
||||
const getParamValue = (param: string) => {
|
||||
const value = detail?.model_config.model?.completion_params?.[param] || '-'
|
||||
@ -209,7 +212,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
|
||||
<div className='text-[13px] text-gray-900 font-medium'>
|
||||
<ModelName modelId={modelName} modelDisplayName={modelName} />
|
||||
</div>
|
||||
<ModelModeTypeLabel type={ModelModeType.chat} isHighlight />
|
||||
<ModelModeTypeLabel type={detail?.model_config.model.mode as any} isHighlight />
|
||||
</div>
|
||||
<Popover
|
||||
position='br'
|
||||
@ -239,11 +242,15 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
|
||||
|
||||
</div>
|
||||
{/* Panel Body */}
|
||||
{varList.length > 0 && (
|
||||
{(varList.length > 0 || (!isChatMode && message_files.length > 0)) && (
|
||||
<div className='px-6 pt-4 pb-2'>
|
||||
<VarPanel varList={varList} />
|
||||
<VarPanel
|
||||
varList={varList}
|
||||
message_files={message_files}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isChatMode
|
||||
? <div className="px-2.5 py-4">
|
||||
<Chat
|
||||
|
@ -1,19 +1,24 @@
|
||||
'use client'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ChevronDown, ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
|
||||
|
||||
type Props = {
|
||||
varList: { label: string; value: string }[]
|
||||
message_files: string[]
|
||||
}
|
||||
|
||||
const VarPanel: FC<Props> = ({
|
||||
varList,
|
||||
message_files,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [isCollapse, { toggle: toggleCollapse }] = useBoolean(false)
|
||||
const [imagePreviewUrl, setImagePreviewUrl] = useState('')
|
||||
|
||||
return (
|
||||
<div className='rounded-xl border border-color-indigo-100 bg-indigo-25'>
|
||||
<div
|
||||
@ -30,7 +35,7 @@ const VarPanel: FC<Props> = ({
|
||||
{!isCollapse && (
|
||||
<div className='px-6 pb-3'>
|
||||
{varList.map(({ label, value }, index) => (
|
||||
<div key={index} className='flex py-1 leading-[18px] text-[13px]'>
|
||||
<div key={index} className='flex py-2 leading-[18px] text-[13px]'>
|
||||
<div className='shrink-0 w-[128px] flex text-primary-600'>
|
||||
<span className='shrink-0 opacity-60'>{'{{'}</span>
|
||||
<span className='truncate'>{label}</span>
|
||||
@ -39,9 +44,32 @@ const VarPanel: FC<Props> = ({
|
||||
<div className='pl-2.5 break-all'>{value}</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{message_files.length > 0 && (
|
||||
<div className='mt-1 flex py-2'>
|
||||
<div className='shrink-0 w-[128px] leading-[18px] text-[13px] font-medium text-gray-700'>{t('appLog.detail.uploadImages')}</div>
|
||||
<div className="flex space-x-2">
|
||||
{message_files.map((url, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="ml-2.5 w-16 h-16 rounded-lg bg-no-repeat bg-cover bg-center cursor-pointer"
|
||||
style={{ backgroundImage: `url(${url})` }}
|
||||
onClick={() => setImagePreviewUrl(url)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{
|
||||
imagePreviewUrl && (
|
||||
<ImagePreview
|
||||
url={imagePreviewUrl}
|
||||
onCancel={() => setImagePreviewUrl('')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ type IProps = {
|
||||
value: string
|
||||
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void
|
||||
className?: string
|
||||
wrapperClassName?: string
|
||||
minHeight?: number
|
||||
maxHeight?: number
|
||||
autoFocus?: boolean
|
||||
@ -16,7 +17,7 @@ type IProps = {
|
||||
|
||||
const AutoHeightTextarea = forwardRef(
|
||||
(
|
||||
{ value, onChange, placeholder, className, minHeight = 36, maxHeight = 96, autoFocus, controlFocus, onKeyDown, onKeyUp }: IProps,
|
||||
{ value, onChange, placeholder, className, wrapperClassName, minHeight = 36, maxHeight = 96, autoFocus, controlFocus, onKeyDown, onKeyUp }: IProps,
|
||||
outerRef: any,
|
||||
) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
@ -54,7 +55,7 @@ const AutoHeightTextarea = forwardRef(
|
||||
}, [controlFocus])
|
||||
|
||||
return (
|
||||
<div className='relative'>
|
||||
<div className={`relative ${wrapperClassName}`}>
|
||||
<div className={cn(className, 'invisible whitespace-pre-wrap break-all overflow-y-auto')} style={{
|
||||
minHeight,
|
||||
maxHeight,
|
||||
@ -80,4 +81,6 @@ const AutoHeightTextarea = forwardRef(
|
||||
},
|
||||
)
|
||||
|
||||
AutoHeightTextarea.displayName = 'AutoHeightTextarea'
|
||||
|
||||
export default AutoHeightTextarea
|
||||
|
@ -0,0 +1,8 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="link-03">
|
||||
<g id="Solid">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.01569 1.83378C9.7701 1.10515 10.7805 0.701975 11.8293 0.711089C12.8781 0.720202 13.8813 1.14088 14.623 1.88251C15.3646 2.62414 15.7853 3.62739 15.7944 4.67618C15.8035 5.72497 15.4003 6.73538 14.6717 7.48979L14.6636 7.49805L12.6637 9.49796C12.2581 9.90362 11.7701 10.2173 11.2327 10.4178C10.6953 10.6183 10.1211 10.7008 9.54897 10.6598C8.97686 10.6189 8.42025 10.4553 7.91689 10.1803C7.41354 9.90531 6.97522 9.52527 6.63165 9.06596C6.41112 8.77113 6.47134 8.35334 6.76618 8.1328C7.06101 7.91226 7.4788 7.97249 7.69934 8.26732C7.92838 8.57353 8.2206 8.82689 8.55617 9.01023C8.89174 9.19356 9.26281 9.30259 9.64422 9.3299C10.0256 9.35722 10.4085 9.30219 10.7667 9.16854C11.125 9.0349 11.4503 8.82576 11.7207 8.55532L13.7164 6.55956C14.1998 6.05705 14.4672 5.38513 14.4611 4.68777C14.455 3.98857 14.1746 3.31974 13.6802 2.82532C13.1857 2.3309 12.5169 2.05045 11.8177 2.04437C11.12 2.03831 10.4478 2.30591 9.94526 2.78967L8.80219 3.92609C8.54108 4.18568 8.11898 4.18445 7.85939 3.92334C7.5998 3.66223 7.60103 3.24012 7.86214 2.98053L9.0088 1.84053L9.01569 1.83378Z" fill="#667085"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.76493 5.58217C6.30234 5.3817 6.87657 5.29915 7.44869 5.34012C8.0208 5.3811 8.57741 5.54463 9.08077 5.81964C9.58412 6.09465 10.0224 6.47469 10.366 6.93399C10.5865 7.22882 10.5263 7.64662 10.2315 7.86715C9.93665 8.08769 9.51886 8.02746 9.29832 7.73263C9.06928 7.42643 8.77706 7.17307 8.44149 6.98973C8.10592 6.80639 7.73485 6.69737 7.35344 6.67005C6.97203 6.64274 6.58921 6.69777 6.23094 6.83141C5.87266 6.96506 5.54733 7.17419 5.27699 7.44463L3.28123 9.44039C2.79787 9.94291 2.5305 10.6148 2.53656 11.3122C2.54263 12.0114 2.82309 12.6802 3.31751 13.1746C3.81193 13.6691 4.48076 13.9495 5.17995 13.9556C5.87732 13.9616 6.54923 13.6943 7.05174 13.2109L8.18743 12.0752C8.44777 11.8149 8.86988 11.8149 9.13023 12.0752C9.39058 12.3356 9.39058 12.7577 9.13023 13.018L7.99023 14.158L7.98197 14.1662C7.22756 14.8948 6.21715 15.298 5.16837 15.2889C4.11958 15.2798 3.11633 14.8591 2.3747 14.1174C1.63307 13.3758 1.21239 12.3726 1.20328 11.3238C1.19416 10.275 1.59734 9.26458 2.32597 8.51017L2.33409 8.50191L4.33401 6.50199C4.33398 6.50202 4.33404 6.50196 4.33401 6.50199C4.7395 6.09638 5.22756 5.78262 5.76493 5.58217Z" fill="#667085"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
@ -0,0 +1,10 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Left Icon" clip-path="url(#clip0_12728_40636)">
|
||||
<path id="Icon" d="M10.6654 8.00016L7.9987 5.3335M7.9987 5.3335L5.33203 8.00016M7.9987 5.3335V10.6668M14.6654 8.00016C14.6654 11.6821 11.6806 14.6668 7.9987 14.6668C4.3168 14.6668 1.33203 11.6821 1.33203 8.00016C1.33203 4.31826 4.3168 1.3335 7.9987 1.3335C11.6806 1.3335 14.6654 4.31826 14.6654 8.00016Z" stroke="#155EEF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_12728_40636">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 658 B |
@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="image-plus">
|
||||
<path id="Icon" d="M8.33333 2.00016H5.2C4.0799 2.00016 3.51984 2.00016 3.09202 2.21815C2.71569 2.4099 2.40973 2.71586 2.21799 3.09218C2 3.52001 2 4.08006 2 5.20016V10.8002C2 11.9203 2 12.4803 2.21799 12.9081C2.40973 13.2845 2.71569 13.5904 3.09202 13.7822C3.51984 14.0002 4.07989 14.0002 5.2 14.0002H11.3333C11.9533 14.0002 12.2633 14.0002 12.5176 13.932C13.2078 13.7471 13.7469 13.208 13.9319 12.5178C14 12.2635 14 11.9535 14 11.3335M12.6667 5.3335V1.3335M10.6667 3.3335H14.6667M7 5.66683C7 6.40321 6.40305 7.00016 5.66667 7.00016C4.93029 7.00016 4.33333 6.40321 4.33333 5.66683C4.33333 4.93045 4.93029 4.3335 5.66667 4.3335C6.40305 4.3335 7 4.93045 7 5.66683ZM9.99336 7.94559L4.3541 13.0722C4.03691 13.3605 3.87831 13.5047 3.86429 13.6296C3.85213 13.7379 3.89364 13.8453 3.97546 13.9172C4.06985 14.0002 4.28419 14.0002 4.71286 14.0002H10.9707C11.9301 14.0002 12.4098 14.0002 12.7866 13.839C13.2596 13.6366 13.6365 13.2598 13.8388 12.7868C14 12.41 14 11.9303 14 10.9708C14 10.648 14 10.4866 13.9647 10.3363C13.9204 10.1474 13.8353 9.9704 13.7155 9.81776C13.6202 9.6963 13.4941 9.59546 13.242 9.3938L11.3772 7.90194C11.1249 7.7001 10.9988 7.59919 10.8599 7.56357C10.7374 7.53218 10.6086 7.53624 10.4884 7.57529C10.352 7.61959 10.2324 7.72826 9.99336 7.94559Z" stroke="#667085" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 12C10 10.8954 10.8954 10 12 10C13.1046 10 14 10.8954 14 12C14 13.1046 13.1046 14 12 14C10.8954 14 10 13.1046 10 12Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 4C9.13833 4 6.80535 5.26472 5.07675 6.70743C3.3505 8.14818 2.16697 9.81429 1.57422 10.7528L1.55014 10.7908C1.43252 10.976 1.27981 11.2164 1.2026 11.5532C1.14027 11.8251 1.14027 12.1749 1.2026 12.4468C1.2798 12.7836 1.43252 13.024 1.55014 13.2092L1.57423 13.2472C2.16697 14.1857 3.3505 15.8518 5.07675 17.2926C6.80535 18.7353 9.13833 20 12 20C14.8617 20 17.1947 18.7353 18.9233 17.2926C20.6495 15.8518 21.833 14.1857 22.4258 13.2472L22.4499 13.2092C22.5675 13.024 22.7202 12.7837 22.7974 12.4468C22.8597 12.1749 22.8597 11.8251 22.7974 11.5532C22.7202 11.2163 22.5675 10.976 22.4499 10.7908L22.4258 10.7528C21.833 9.81429 20.6495 8.14818 18.9233 6.70743C17.1947 5.26472 14.8617 4 12 4ZM12 8C9.79086 8 8 9.79086 8 12C8 14.2091 9.79086 16 12 16C14.2091 16 16 14.2091 16 12C16 9.79086 14.2091 8 12 8Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
@ -0,0 +1,57 @@
|
||||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "17",
|
||||
"height": "16",
|
||||
"viewBox": "0 0 17 16",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"id": "link-03"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"id": "Solid"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"fill-rule": "evenodd",
|
||||
"clip-rule": "evenodd",
|
||||
"d": "M9.01569 1.83378C9.7701 1.10515 10.7805 0.701975 11.8293 0.711089C12.8781 0.720202 13.8813 1.14088 14.623 1.88251C15.3646 2.62414 15.7853 3.62739 15.7944 4.67618C15.8035 5.72497 15.4003 6.73538 14.6717 7.48979L14.6636 7.49805L12.6637 9.49796C12.2581 9.90362 11.7701 10.2173 11.2327 10.4178C10.6953 10.6183 10.1211 10.7008 9.54897 10.6598C8.97686 10.6189 8.42025 10.4553 7.91689 10.1803C7.41354 9.90531 6.97522 9.52527 6.63165 9.06596C6.41112 8.77113 6.47134 8.35334 6.76618 8.1328C7.06101 7.91226 7.4788 7.97249 7.69934 8.26732C7.92838 8.57353 8.2206 8.82689 8.55617 9.01023C8.89174 9.19356 9.26281 9.30259 9.64422 9.3299C10.0256 9.35722 10.4085 9.30219 10.7667 9.16854C11.125 9.0349 11.4503 8.82576 11.7207 8.55532L13.7164 6.55956C14.1998 6.05705 14.4672 5.38513 14.4611 4.68777C14.455 3.98857 14.1746 3.31974 13.6802 2.82532C13.1857 2.3309 12.5169 2.05045 11.8177 2.04437C11.12 2.03831 10.4478 2.30591 9.94526 2.78967L8.80219 3.92609C8.54108 4.18568 8.11898 4.18445 7.85939 3.92334C7.5998 3.66223 7.60103 3.24012 7.86214 2.98053L9.0088 1.84053L9.01569 1.83378Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"fill-rule": "evenodd",
|
||||
"clip-rule": "evenodd",
|
||||
"d": "M5.76493 5.58217C6.30234 5.3817 6.87657 5.29915 7.44869 5.34012C8.0208 5.3811 8.57741 5.54463 9.08077 5.81964C9.58412 6.09465 10.0224 6.47469 10.366 6.93399C10.5865 7.22882 10.5263 7.64662 10.2315 7.86715C9.93665 8.08769 9.51886 8.02746 9.29832 7.73263C9.06928 7.42643 8.77706 7.17307 8.44149 6.98973C8.10592 6.80639 7.73485 6.69737 7.35344 6.67005C6.97203 6.64274 6.58921 6.69777 6.23094 6.83141C5.87266 6.96506 5.54733 7.17419 5.27699 7.44463L3.28123 9.44039C2.79787 9.94291 2.5305 10.6148 2.53656 11.3122C2.54263 12.0114 2.82309 12.6802 3.31751 13.1746C3.81193 13.6691 4.48076 13.9495 5.17995 13.9556C5.87732 13.9616 6.54923 13.6943 7.05174 13.2109L8.18743 12.0752C8.44777 11.8149 8.86988 11.8149 9.13023 12.0752C9.39058 12.3356 9.39058 12.7577 9.13023 13.018L7.99023 14.158L7.98197 14.1662C7.22756 14.8948 6.21715 15.298 5.16837 15.2889C4.11958 15.2798 3.11633 14.8591 2.3747 14.1174C1.63307 13.3758 1.21239 12.3726 1.20328 11.3238C1.19416 10.275 1.59734 9.26458 2.32597 8.51017L2.33409 8.50191L4.33401 6.50199C4.33398 6.50202 4.33404 6.50196 4.33401 6.50199C4.7395 6.09638 5.22756 5.78262 5.76493 5.58217Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "Link03"
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './Link03.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'Link03'
|
||||
|
||||
export default Icon
|
@ -0,0 +1,66 @@
|
||||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "16",
|
||||
"height": "16",
|
||||
"viewBox": "0 0 16 16",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"id": "Left Icon",
|
||||
"clip-path": "url(#clip0_12728_40636)"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Icon",
|
||||
"d": "M10.6654 8.00016L7.9987 5.3335M7.9987 5.3335L5.33203 8.00016M7.9987 5.3335V10.6668M14.6654 8.00016C14.6654 11.6821 11.6806 14.6668 7.9987 14.6668C4.3168 14.6668 1.33203 11.6821 1.33203 8.00016C1.33203 4.31826 4.3168 1.3335 7.9987 1.3335C11.6806 1.3335 14.6654 4.31826 14.6654 8.00016Z",
|
||||
"stroke": "currentColor",
|
||||
"stroke-width": "1.5",
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "defs",
|
||||
"attributes": {},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "clipPath",
|
||||
"attributes": {
|
||||
"id": "clip0_12728_40636"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "rect",
|
||||
"attributes": {
|
||||
"width": "16",
|
||||
"height": "16",
|
||||
"fill": "white"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "Upload03"
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './Upload03.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'Upload03'
|
||||
|
||||
export default Icon
|
@ -7,6 +7,7 @@ export { default as Edit03 } from './Edit03'
|
||||
export { default as Hash02 } from './Hash02'
|
||||
export { default as HelpCircle } from './HelpCircle'
|
||||
export { default as InfoCircle } from './InfoCircle'
|
||||
export { default as Link03 } from './Link03'
|
||||
export { default as LinkExternal01 } from './LinkExternal01'
|
||||
export { default as LinkExternal02 } from './LinkExternal02'
|
||||
export { default as Loading02 } from './Loading02'
|
||||
@ -18,5 +19,6 @@ export { default as Settings01 } from './Settings01'
|
||||
export { default as Settings04 } from './Settings04'
|
||||
export { default as Target04 } from './Target04'
|
||||
export { default as Trash03 } from './Trash03'
|
||||
export { default as Upload03 } from './Upload03'
|
||||
export { default as XClose } from './XClose'
|
||||
export { default as X } from './X'
|
||||
|
@ -0,0 +1,39 @@
|
||||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "16",
|
||||
"height": "16",
|
||||
"viewBox": "0 0 16 16",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"id": "image-plus"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Icon",
|
||||
"d": "M8.33333 2.00016H5.2C4.0799 2.00016 3.51984 2.00016 3.09202 2.21815C2.71569 2.4099 2.40973 2.71586 2.21799 3.09218C2 3.52001 2 4.08006 2 5.20016V10.8002C2 11.9203 2 12.4803 2.21799 12.9081C2.40973 13.2845 2.71569 13.5904 3.09202 13.7822C3.51984 14.0002 4.07989 14.0002 5.2 14.0002H11.3333C11.9533 14.0002 12.2633 14.0002 12.5176 13.932C13.2078 13.7471 13.7469 13.208 13.9319 12.5178C14 12.2635 14 11.9535 14 11.3335M12.6667 5.3335V1.3335M10.6667 3.3335H14.6667M7 5.66683C7 6.40321 6.40305 7.00016 5.66667 7.00016C4.93029 7.00016 4.33333 6.40321 4.33333 5.66683C4.33333 4.93045 4.93029 4.3335 5.66667 4.3335C6.40305 4.3335 7 4.93045 7 5.66683ZM9.99336 7.94559L4.3541 13.0722C4.03691 13.3605 3.87831 13.5047 3.86429 13.6296C3.85213 13.7379 3.89364 13.8453 3.97546 13.9172C4.06985 14.0002 4.28419 14.0002 4.71286 14.0002H10.9707C11.9301 14.0002 12.4098 14.0002 12.7866 13.839C13.2596 13.6366 13.6365 13.2598 13.8388 12.7868C14 12.41 14 11.9303 14 10.9708C14 10.648 14 10.4866 13.9647 10.3363C13.9204 10.1474 13.8353 9.9704 13.7155 9.81776C13.6202 9.6963 13.4941 9.59546 13.242 9.3938L11.3772 7.90194C11.1249 7.7001 10.9988 7.59919 10.8599 7.56357C10.7374 7.53218 10.6086 7.53624 10.4884 7.57529C10.352 7.61959 10.2324 7.72826 9.99336 7.94559Z",
|
||||
"stroke": "currentColor",
|
||||
"stroke-width": "1.25",
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "ImagePlus"
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './ImagePlus.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'ImagePlus'
|
||||
|
||||
export default Icon
|
@ -0,0 +1 @@
|
||||
export { default as ImagePlus } from './ImagePlus'
|
@ -0,0 +1,37 @@
|
||||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "24",
|
||||
"height": "24",
|
||||
"viewBox": "0 0 24 24",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M10 12C10 10.8954 10.8954 10 12 10C13.1046 10 14 10.8954 14 12C14 13.1046 13.1046 14 12 14C10.8954 14 10 13.1046 10 12Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"fill-rule": "evenodd",
|
||||
"clip-rule": "evenodd",
|
||||
"d": "M12 4C9.13833 4 6.80535 5.26472 5.07675 6.70743C3.3505 8.14818 2.16697 9.81429 1.57422 10.7528L1.55014 10.7908C1.43252 10.976 1.27981 11.2164 1.2026 11.5532C1.14027 11.8251 1.14027 12.1749 1.2026 12.4468C1.2798 12.7836 1.43252 13.024 1.55014 13.2092L1.57423 13.2472C2.16697 14.1857 3.3505 15.8518 5.07675 17.2926C6.80535 18.7353 9.13833 20 12 20C14.8617 20 17.1947 18.7353 18.9233 17.2926C20.6495 15.8518 21.833 14.1857 22.4258 13.2472L22.4499 13.2092C22.5675 13.024 22.7202 12.7837 22.7974 12.4468C22.8597 12.1749 22.8597 11.8251 22.7974 11.5532C22.7202 11.2163 22.5675 10.976 22.4499 10.7908L22.4258 10.7528C21.833 9.81429 20.6495 8.14818 18.9233 6.70743C17.1947 5.26472 14.8617 4 12 4ZM12 8C9.79086 8 8 9.79086 8 12C8 14.2091 9.79086 16 12 16C14.2091 16 16 14.2091 16 12C16 9.79086 14.2091 8 12 8Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "Eye"
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './Eye.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'Eye'
|
||||
|
||||
export default Icon
|
@ -1,6 +1,7 @@
|
||||
export { default as CheckCircle } from './CheckCircle'
|
||||
export { default as CheckDone01 } from './CheckDone01'
|
||||
export { default as Download02 } from './Download02'
|
||||
export { default as Eye } from './Eye'
|
||||
export { default as MessageClockCircle } from './MessageClockCircle'
|
||||
export { default as Target04 } from './Target04'
|
||||
export { default as Tool03 } from './Tool03'
|
||||
|
84
web/app/components/base/image-gallery/index.tsx
Normal file
84
web/app/components/base/image-gallery/index.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import cn from 'classnames'
|
||||
import s from './style.module.css'
|
||||
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
|
||||
|
||||
type Props = {
|
||||
srcs: string[]
|
||||
}
|
||||
|
||||
const getWidthStyle = (imgNum: number) => {
|
||||
if (imgNum === 1) {
|
||||
return {
|
||||
maxWidth: '100%',
|
||||
}
|
||||
}
|
||||
|
||||
if (imgNum === 2 || imgNum === 4) {
|
||||
return {
|
||||
width: 'calc(50% - 4px)',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
width: 'calc(33.3333% - 5.3333px)',
|
||||
}
|
||||
}
|
||||
|
||||
const ImageGallery: FC<Props> = ({
|
||||
srcs,
|
||||
}) => {
|
||||
const [imagePreviewUrl, setImagePreviewUrl] = useState('')
|
||||
|
||||
const imgNum = srcs.length
|
||||
const imgStyle = getWidthStyle(imgNum)
|
||||
return (
|
||||
<div className={cn(s[`img-${imgNum}`], 'flex flex-wrap')}>
|
||||
{/* TODO: support preview */}
|
||||
{srcs.map((src, index) => (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
key={index}
|
||||
className={s.item}
|
||||
style={imgStyle}
|
||||
src={src}
|
||||
alt=''
|
||||
onClick={() => setImagePreviewUrl(src)}
|
||||
/>
|
||||
))}
|
||||
{
|
||||
imagePreviewUrl && (
|
||||
<ImagePreview
|
||||
url={imagePreviewUrl}
|
||||
onCancel={() => setImagePreviewUrl('')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(ImageGallery)
|
||||
|
||||
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')
|
||||
|
||||
return srcs
|
||||
})()
|
||||
return (
|
||||
<div className='space-y-2'>
|
||||
{imgGallerySrcs.map((_, index) => (
|
||||
<div key={index} className='p-4 pb-2 rounded-lg bg-[#D1E9FF80]'>
|
||||
<ImageGallery srcs={imgGallerySrcs.slice(0, index + 1)} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
22
web/app/components/base/image-gallery/style.module.css
Normal file
22
web/app/components/base/image-gallery/style.module.css
Normal file
@ -0,0 +1,22 @@
|
||||
.item {
|
||||
height: 200px;
|
||||
margin-right: 8px;
|
||||
margin-bottom: 8px;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.item:nth-child(3n) {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.img-2 .item:nth-child(2n),
|
||||
.img-4 .item:nth-child(2n) {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.img-4 .item:nth-child(3n) {
|
||||
margin-right: 8px;
|
||||
}
|
150
web/app/components/base/image-uploader/chat-image-uploader.tsx
Normal file
150
web/app/components/base/image-uploader/chat-image-uploader.tsx
Normal file
@ -0,0 +1,150 @@
|
||||
import type { FC } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Uploader from './uploader'
|
||||
import ImageLinkInput from './image-link-input'
|
||||
import { ImagePlus } from '@/app/components/base/icons/src/vender/line/images'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { Upload03 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import type { ImageFile, VisionSettings } from '@/types/app'
|
||||
|
||||
type UploadOnlyFromLocalProps = {
|
||||
onUpload: (imageFile: ImageFile) => void
|
||||
disabled?: boolean
|
||||
limit?: number
|
||||
}
|
||||
const UploadOnlyFromLocal: FC<UploadOnlyFromLocalProps> = ({
|
||||
onUpload,
|
||||
disabled,
|
||||
limit,
|
||||
}) => {
|
||||
return (
|
||||
<Uploader onUpload={onUpload} disabled={disabled} limit={limit}>
|
||||
{
|
||||
hovering => (
|
||||
<div className={`
|
||||
relative flex items-center justify-center w-8 h-8 rounded-lg cursor-pointer
|
||||
${hovering && 'bg-gray-100'}
|
||||
`}>
|
||||
<ImagePlus className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Uploader>
|
||||
)
|
||||
}
|
||||
|
||||
type UploaderButtonProps = {
|
||||
methods: VisionSettings['transfer_methods']
|
||||
onUpload: (imageFile: ImageFile) => void
|
||||
disabled?: boolean
|
||||
limit?: number
|
||||
}
|
||||
const UploaderButton: FC<UploaderButtonProps> = ({
|
||||
methods,
|
||||
onUpload,
|
||||
disabled,
|
||||
limit,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const hasUploadFromLocal = methods.find(method => method === TransferMethod.local_file)
|
||||
|
||||
const handleUpload = (imageFile: ImageFile) => {
|
||||
setOpen(false)
|
||||
onUpload(imageFile)
|
||||
}
|
||||
|
||||
const handleToggle = () => {
|
||||
if (disabled)
|
||||
return
|
||||
|
||||
setOpen(v => !v)
|
||||
}
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='top-start'
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={handleToggle}>
|
||||
<div className={`
|
||||
relative flex items-center justify-center w-8 h-8 hover:bg-gray-100 rounded-lg
|
||||
${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}
|
||||
`}>
|
||||
<ImagePlus className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-50'>
|
||||
<div className='p-2 w-[260px] bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg'>
|
||||
<ImageLinkInput onUpload={handleUpload} />
|
||||
{
|
||||
hasUploadFromLocal && (
|
||||
<>
|
||||
<div className='flex items-center mt-2 px-2 text-xs font-medium text-gray-400'>
|
||||
<div className='mr-3 w-[93px] h-[1px] bg-gradient-to-l from-[#F3F4F6]' />
|
||||
OR
|
||||
<div className='ml-3 w-[93px] h-[1px] bg-gradient-to-r from-[#F3F4F6]' />
|
||||
</div>
|
||||
<Uploader onUpload={handleUpload} limit={limit}>
|
||||
{
|
||||
hovering => (
|
||||
<div className={`
|
||||
flex items-center justify-center h-8 text-[13px] font-medium text-[#155EEF] rounded-lg cursor-pointer
|
||||
${hovering && 'bg-primary-50'}
|
||||
`}>
|
||||
<Upload03 className='mr-1 w-4 h-4' />
|
||||
{t('common.imageUploader.uploadFromComputer')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Uploader>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
type ChatImageUploaderProps = {
|
||||
settings: VisionSettings
|
||||
onUpload: (imageFile: ImageFile) => void
|
||||
disabled?: boolean
|
||||
}
|
||||
const ChatImageUploader: FC<ChatImageUploaderProps> = ({
|
||||
settings,
|
||||
onUpload,
|
||||
disabled,
|
||||
}) => {
|
||||
const onlyUploadLocal = settings.transfer_methods.length === 1 && settings.transfer_methods[0] === TransferMethod.local_file
|
||||
|
||||
if (onlyUploadLocal) {
|
||||
return (
|
||||
<UploadOnlyFromLocal
|
||||
onUpload={onUpload}
|
||||
disabled={disabled}
|
||||
limit={+settings.image_file_size_limit!}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<UploaderButton
|
||||
methods={settings.transfer_methods}
|
||||
onUpload={onUpload}
|
||||
disabled={disabled}
|
||||
limit={+settings.image_file_size_limit!}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatImageUploader
|
81
web/app/components/base/image-uploader/hooks.ts
Normal file
81
web/app/components/base/image-uploader/hooks.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import { useState } from 'react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { imageUpload } from './utils'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import type { ImageFile } from '@/types/app'
|
||||
|
||||
export const useImageFiles = () => {
|
||||
const params = useParams()
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
const [files, setFiles] = useState<ImageFile[]>([])
|
||||
|
||||
const handleUpload = (imageFile: ImageFile) => {
|
||||
const newFiles = [...files, imageFile]
|
||||
setFiles(newFiles)
|
||||
}
|
||||
const handleRemove = (imageFileId: string) => {
|
||||
const index = files.findIndex(file => file._id === imageFileId)
|
||||
|
||||
if (index > -1) {
|
||||
const newFiles = [...files.slice(0, index), ...files.slice(index + 1)]
|
||||
setFiles(newFiles)
|
||||
}
|
||||
}
|
||||
const handleImageLinkLoadError = (imageFileId: string) => {
|
||||
const index = files.findIndex(file => file._id === imageFileId)
|
||||
|
||||
if (index > -1) {
|
||||
const currentFile = files[index]
|
||||
const newFiles = [...files.slice(0, index), { ...currentFile, progress: -1 }, ...files.slice(index + 1)]
|
||||
setFiles(newFiles)
|
||||
}
|
||||
}
|
||||
const handleImageLinkLoadSuccess = (imageFileId: string) => {
|
||||
const index = files.findIndex(file => file._id === imageFileId)
|
||||
|
||||
if (index > -1) {
|
||||
const currentImageFile = files[index]
|
||||
const newFiles = [...files.slice(0, index), { ...currentImageFile, progress: 100 }, ...files.slice(index + 1)]
|
||||
setFiles(newFiles)
|
||||
}
|
||||
}
|
||||
const handleReUpload = (imageFileId: string) => {
|
||||
const index = files.findIndex(file => file._id === imageFileId)
|
||||
|
||||
if (index > -1) {
|
||||
const currentImageFile = files[index]
|
||||
imageUpload({
|
||||
file: currentImageFile.file!,
|
||||
onProgressCallback: (progress) => {
|
||||
const newFiles = [...files.slice(0, index), { ...currentImageFile, progress }, ...files.slice(index + 1)]
|
||||
setFiles(newFiles)
|
||||
},
|
||||
onSuccessCallback: (res) => {
|
||||
const newFiles = [...files.slice(0, index), { ...currentImageFile, fileId: res.id, progress: 100 }, ...files.slice(index + 1)]
|
||||
setFiles(newFiles)
|
||||
},
|
||||
onErrorCallback: () => {
|
||||
notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerUploadError') })
|
||||
const newFiles = [...files.slice(0, index), { ...currentImageFile, progress: -1 }, ...files.slice(index + 1)]
|
||||
setFiles(newFiles)
|
||||
},
|
||||
}, !!params.token)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClear = () => {
|
||||
setFiles([])
|
||||
}
|
||||
|
||||
return {
|
||||
files,
|
||||
onUpload: handleUpload,
|
||||
onRemove: handleRemove,
|
||||
onImageLinkLoadError: handleImageLinkLoadError,
|
||||
onImageLinkLoadSuccess: handleImageLinkLoadSuccess,
|
||||
onReUpload: handleReUpload,
|
||||
onClear: handleClear,
|
||||
}
|
||||
}
|
50
web/app/components/base/image-uploader/image-link-input.tsx
Normal file
50
web/app/components/base/image-uploader/image-link-input.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import type { FC } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type { ImageFile } from '@/types/app'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
|
||||
type ImageLinkInputProps = {
|
||||
onUpload: (imageFile: ImageFile) => void
|
||||
}
|
||||
const regex = /^(https?|ftp):\/\//
|
||||
const ImageLinkInput: FC<ImageLinkInputProps> = ({
|
||||
onUpload,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [imageLink, setImageLink] = useState('')
|
||||
|
||||
const handleClick = () => {
|
||||
const imageFile = {
|
||||
type: TransferMethod.remote_url,
|
||||
_id: `${Date.now()}`,
|
||||
fileId: '',
|
||||
progress: regex.test(imageLink) ? 0 : -1,
|
||||
url: imageLink,
|
||||
}
|
||||
|
||||
onUpload(imageFile)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex items-center pl-1.5 pr-1 h-8 border border-gray-200 bg-white shadow-xs rounded-lg'>
|
||||
<input
|
||||
className='grow mr-0.5 px-1 h-[18px] text-[13px] outline-none appearance-none'
|
||||
value={imageLink}
|
||||
onChange={e => setImageLink(e.target.value)}
|
||||
placeholder={t('common.imageUploader.pasteImageLinkInputPlaceholder') || ''}
|
||||
/>
|
||||
<Button
|
||||
type='primary'
|
||||
className='!h-6 text-xs font-medium'
|
||||
disabled={!imageLink}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{t('common.operation.ok')}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ImageLinkInput
|
129
web/app/components/base/image-uploader/image-list.tsx
Normal file
129
web/app/components/base/image-uploader/image-list.tsx
Normal file
@ -0,0 +1,129 @@
|
||||
import type { FC } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Loading02, XClose } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
|
||||
import TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||
import type { ImageFile } from '@/types/app'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
|
||||
|
||||
type ImageListProps = {
|
||||
list: ImageFile[]
|
||||
readonly?: boolean
|
||||
onRemove?: (imageFileId: string) => void
|
||||
onReUpload?: (imageFileId: string) => void
|
||||
onImageLinkLoadSuccess?: (imageFileId: string) => void
|
||||
onImageLinkLoadError?: (imageFileId: string) => void
|
||||
}
|
||||
|
||||
const ImageList: FC<ImageListProps> = ({
|
||||
list,
|
||||
readonly,
|
||||
onRemove,
|
||||
onReUpload,
|
||||
onImageLinkLoadSuccess,
|
||||
onImageLinkLoadError,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [imagePreviewUrl, setImagePreviewUrl] = useState('')
|
||||
|
||||
const handleImageLinkLoadSuccess = (item: ImageFile) => {
|
||||
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)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex flex-wrap'>
|
||||
{
|
||||
list.map(item => (
|
||||
<div
|
||||
key={item._id}
|
||||
className='group relative mr-1 border-[0.5px] border-black/5 rounded-lg'
|
||||
>
|
||||
{
|
||||
item.type === TransferMethod.local_file && item.progress !== 100 && (
|
||||
<>
|
||||
<div
|
||||
className='absolute inset-0 flex items-center justify-center z-[1] bg-black/30'
|
||||
style={{ left: item.progress > -1 ? `${item.progress}%` : 0 }}
|
||||
>
|
||||
{
|
||||
item.progress === -1 && (
|
||||
<RefreshCcw01 className='w-5 h-5 text-white' onClick={() => onReUpload && onReUpload(item._id)} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
item.progress > -1 && (
|
||||
<span className='absolute top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] text-sm text-white mix-blend-lighten z-[1]'>{item.progress}%</span>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
item.type === TransferMethod.remote_url && item.progress !== 100 && (
|
||||
<div className={`
|
||||
absolute inset-0 flex items-center justify-center rounded-lg z-[1] border
|
||||
${item.progress === -1 ? 'bg-[#FEF0C7] border-[#DC6803]' : 'bg-black/[0.16] border-transparent'}
|
||||
`}>
|
||||
{
|
||||
item.progress > -1 && (
|
||||
<Loading02 className='animate-spin w-5 h-5 text-white' />
|
||||
)
|
||||
}
|
||||
{
|
||||
item.progress === -1 && (
|
||||
<TooltipPlus popupContent={t('common.imageUploader.pasteImageLinkInvalid')}>
|
||||
<AlertTriangle className='w-4 h-4 text-[#DC6803]' />
|
||||
</TooltipPlus>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<img
|
||||
className='w-16 h-16 rounded-lg object-cover cursor-pointer border-[0.5px] border-black/5'
|
||||
alt=''
|
||||
onLoad={() => handleImageLinkLoadSuccess(item)}
|
||||
onError={() => handleImageLinkLoadError(item)}
|
||||
src={item.type === TransferMethod.remote_url ? item.url : item.base64Url}
|
||||
onClick={() => item.progress === 100 && setImagePreviewUrl((item.type === TransferMethod.remote_url ? item.url : item.base64Url) as string)}
|
||||
/>
|
||||
{
|
||||
!readonly && (
|
||||
<div
|
||||
className={`
|
||||
absolute z-10 -top-[9px] -right-[9px] items-center justify-center w-[18px] h-[18px]
|
||||
bg-white hover:bg-gray-50 border-[0.5px] border-black/[0.02] rounded-2xl shadow-lg
|
||||
cursor-pointer
|
||||
${item.progress === -1 ? 'flex' : 'hidden group-hover:flex'}
|
||||
`}
|
||||
onClick={() => onRemove && onRemove(item._id)}
|
||||
>
|
||||
<XClose className='w-3 h-3 text-gray-500' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
{
|
||||
imagePreviewUrl && (
|
||||
<ImagePreview
|
||||
url={imagePreviewUrl}
|
||||
onCancel={() => setImagePreviewUrl('')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ImageList
|
31
web/app/components/base/image-uploader/image-preview.tsx
Normal file
31
web/app/components/base/image-uploader/image-preview.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import type { FC } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { XClose } from '@/app/components/base/icons/src/vender/line/general'
|
||||
|
||||
type ImagePreviewProps = {
|
||||
url: string
|
||||
onCancel: () => void
|
||||
}
|
||||
const ImagePreview: FC<ImagePreviewProps> = ({
|
||||
url,
|
||||
onCancel,
|
||||
}) => {
|
||||
return createPortal(
|
||||
<div className='fixed inset-0 p-8 flex items-center justify-center bg-black/80 z-[1000]' onClick={e => e.stopPropagation()}>
|
||||
<img
|
||||
alt='preview image'
|
||||
src={url}
|
||||
className='max-w-full max-h-full'
|
||||
/>
|
||||
<div
|
||||
className='absolute top-6 right-6 flex items-center justify-center w-8 h-8 bg-white/[0.08] rounded-lg backdrop-blur-[2px] cursor-pointer'
|
||||
onClick={onCancel}
|
||||
>
|
||||
<XClose className='w-4 h-4 text-white' />
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
)
|
||||
}
|
||||
|
||||
export default ImagePreview
|
@ -0,0 +1,148 @@
|
||||
import type { FC } from 'react'
|
||||
import {
|
||||
Fragment,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Uploader from './uploader'
|
||||
import ImageLinkInput from './image-link-input'
|
||||
import ImageList from './image-list'
|
||||
import { useImageFiles } from './hooks'
|
||||
import { ImagePlus } from '@/app/components/base/icons/src/vender/line/images'
|
||||
import { Link03 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import type { ImageFile, VisionSettings } from '@/types/app'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
|
||||
type PasteImageLinkButtonProps = {
|
||||
onUpload: (imageFile: ImageFile) => void
|
||||
disabled?: boolean
|
||||
}
|
||||
const PasteImageLinkButton: FC<PasteImageLinkButtonProps> = ({
|
||||
onUpload,
|
||||
disabled,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const handleUpload = (imageFile: ImageFile) => {
|
||||
setOpen(false)
|
||||
onUpload(imageFile)
|
||||
}
|
||||
|
||||
const handleToggle = () => {
|
||||
if (disabled)
|
||||
return
|
||||
|
||||
setOpen(v => !v)
|
||||
}
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='top-start'
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={handleToggle}>
|
||||
<div className={`
|
||||
relative flex items-center justify-center px-3 h-8 bg-gray-100 hover:bg-gray-200 text-xs text-gray-500 rounded-lg
|
||||
${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}
|
||||
`}>
|
||||
<Link03 className='mr-2 w-4 h-4' />
|
||||
{t('common.imageUploader.pasteImageLink')}
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-10'>
|
||||
<div className='p-2 w-[320px] bg-white border-[0.5px] border-gray-200 rounded-lg shadow-lg'>
|
||||
<ImageLinkInput onUpload={handleUpload} />
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
type TextGenerationImageUploaderProps = {
|
||||
settings: VisionSettings
|
||||
onFilesChange: (files: ImageFile[]) => void
|
||||
}
|
||||
const TextGenerationImageUploader: FC<TextGenerationImageUploaderProps> = ({
|
||||
settings,
|
||||
onFilesChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
files,
|
||||
onUpload,
|
||||
onRemove,
|
||||
onImageLinkLoadError,
|
||||
onImageLinkLoadSuccess,
|
||||
onReUpload,
|
||||
} = useImageFiles()
|
||||
|
||||
useEffect(() => {
|
||||
onFilesChange(files)
|
||||
}, [files])
|
||||
|
||||
const localUpload = (
|
||||
<Uploader
|
||||
onUpload={onUpload}
|
||||
disabled={files.length >= settings.number_limits}
|
||||
limit={+settings.image_file_size_limit!}
|
||||
>
|
||||
{
|
||||
hovering => (
|
||||
<div className={`
|
||||
flex items-center justify-center px-3 h-8 bg-gray-100
|
||||
text-xs text-gray-500 rounded-lg cursor-pointer
|
||||
${hovering && 'bg-gray-200'}
|
||||
`}>
|
||||
<ImagePlus className='mr-2 w-4 h-4' />
|
||||
{t('common.imageUploader.uploadFromComputer')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Uploader>
|
||||
)
|
||||
|
||||
const urlUpload = (
|
||||
<PasteImageLinkButton
|
||||
onUpload={onUpload}
|
||||
disabled={files.length >= settings.number_limits}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='mb-1'>
|
||||
<ImageList
|
||||
list={files}
|
||||
onRemove={onRemove}
|
||||
onReUpload={onReUpload}
|
||||
onImageLinkLoadError={onImageLinkLoadError}
|
||||
onImageLinkLoadSuccess={onImageLinkLoadSuccess}
|
||||
/>
|
||||
</div>
|
||||
<div className={`grid gap-1 ${settings.transfer_methods.length === 2 ? 'grid-cols-2' : 'grid-cols-1'}`}>
|
||||
{
|
||||
settings.transfer_methods.map((method) => {
|
||||
if (method === TransferMethod.local_file)
|
||||
return <Fragment key={TransferMethod.local_file}>{localUpload}</Fragment>
|
||||
|
||||
if (method === TransferMethod.remote_url)
|
||||
return <Fragment key={TransferMethod.remote_url}>{urlUpload}</Fragment>
|
||||
|
||||
return null
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TextGenerationImageUploader
|
100
web/app/components/base/image-uploader/uploader.tsx
Normal file
100
web/app/components/base/image-uploader/uploader.tsx
Normal file
@ -0,0 +1,100 @@
|
||||
import type { ChangeEvent, FC } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { imageUpload } from './utils'
|
||||
import type { ImageFile } from '@/types/app'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
|
||||
type UploaderProps = {
|
||||
children: (hovering: boolean) => JSX.Element
|
||||
onUpload: (imageFile: ImageFile) => void
|
||||
limit?: number
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const Uploader: FC<UploaderProps> = ({
|
||||
children,
|
||||
onUpload,
|
||||
limit,
|
||||
disabled,
|
||||
}) => {
|
||||
const [hovering, setHovering] = useState(false)
|
||||
const params = useParams()
|
||||
const { notify } = useToastContext()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
|
||||
if (!file)
|
||||
return
|
||||
|
||||
if (limit && file.size > limit * 1024 * 1024) {
|
||||
notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerLimit', { size: limit }) })
|
||||
return
|
||||
}
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.addEventListener(
|
||||
'load',
|
||||
() => {
|
||||
const imageFile = {
|
||||
type: TransferMethod.local_file,
|
||||
_id: `${Date.now()}`,
|
||||
fileId: '',
|
||||
file,
|
||||
url: reader.result as string,
|
||||
base64Url: reader.result as string,
|
||||
progress: 0,
|
||||
}
|
||||
onUpload(imageFile)
|
||||
imageUpload({
|
||||
file: imageFile.file,
|
||||
onProgressCallback: (progress) => {
|
||||
onUpload({ ...imageFile, progress })
|
||||
},
|
||||
onSuccessCallback: (res) => {
|
||||
onUpload({ ...imageFile, fileId: res.id, progress: 100 })
|
||||
},
|
||||
onErrorCallback: () => {
|
||||
notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerUploadError') })
|
||||
onUpload({ ...imageFile, progress: -1 })
|
||||
},
|
||||
}, !!params.token)
|
||||
},
|
||||
false,
|
||||
)
|
||||
reader.addEventListener(
|
||||
'error',
|
||||
() => {
|
||||
notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerReadError') })
|
||||
},
|
||||
false,
|
||||
)
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className='relative'
|
||||
onMouseEnter={() => setHovering(true)}
|
||||
onMouseLeave={() => setHovering(false)}
|
||||
>
|
||||
{children(hovering)}
|
||||
<input
|
||||
className={`
|
||||
absolute block inset-0 opacity-0 text-[0]
|
||||
${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}
|
||||
`}
|
||||
type='file'
|
||||
accept='.png, .jpg, .jpeg, .webp, .gif'
|
||||
onChange={handleChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Uploader
|
36
web/app/components/base/image-uploader/utils.ts
Normal file
36
web/app/components/base/image-uploader/utils.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { upload } from '@/service/base'
|
||||
|
||||
type ImageUploadParams = {
|
||||
file: File
|
||||
onProgressCallback: (progress: number) => void
|
||||
onSuccessCallback: (res: { id: string }) => void
|
||||
onErrorCallback: () => void
|
||||
}
|
||||
type ImageUpload = (v: ImageUploadParams, isPublic?: boolean) => void
|
||||
export const imageUpload: ImageUpload = ({
|
||||
file,
|
||||
onProgressCallback,
|
||||
onSuccessCallback,
|
||||
onErrorCallback,
|
||||
}, isPublic) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
const onProgress = (e: ProgressEvent) => {
|
||||
if (e.lengthComputable) {
|
||||
const percent = Math.floor(e.loaded / e.total * 100)
|
||||
onProgressCallback(percent)
|
||||
}
|
||||
}
|
||||
|
||||
upload({
|
||||
xhr: new XMLHttpRequest(),
|
||||
data: formData,
|
||||
onprogress: onProgress,
|
||||
}, isPublic)
|
||||
.then((res: { id: string }) => {
|
||||
onSuccessCallback(res)
|
||||
})
|
||||
.catch(() => {
|
||||
onErrorCallback()
|
||||
})
|
||||
}
|
73
web/app/components/base/param-item/index.tsx
Normal file
73
web/app/components/base/param-item/index.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { HelpCircle } from '@/app/components/base/icons/src/vender/line/general'
|
||||
|
||||
import Tooltip from '@/app/components/base/tooltip-plus'
|
||||
import Slider from '@/app/components/base/slider'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
id: string
|
||||
name: string
|
||||
noTooltip?: boolean
|
||||
tip?: string
|
||||
value: number
|
||||
enable: boolean
|
||||
step?: number
|
||||
min?: number
|
||||
max: number
|
||||
onChange: (key: string, value: number) => void
|
||||
hasSwitch?: boolean
|
||||
onSwitchChange?: (key: string, enable: boolean) => void
|
||||
}
|
||||
|
||||
const ParamItem: FC<Props> = ({ className, id, name, noTooltip, tip, step = 0.1, min = 0, max, value, enable, onChange, hasSwitch, onSwitchChange }) => {
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="flex items-center h-8 justify-between">
|
||||
<div className="flex items-center">
|
||||
{hasSwitch && (
|
||||
<Switch
|
||||
size='md'
|
||||
defaultValue={enable}
|
||||
onChange={async (val) => {
|
||||
onSwitchChange?.(id, val)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span className="mx-1 text-gray-900 text-[13px] leading-[18px] font-medium">{name}</span>
|
||||
{!noTooltip && (
|
||||
<Tooltip popupContent={<div className="w-[200px]">{tip}</div>}>
|
||||
<HelpCircle className='w-[14px] h-[14px] text-gray-400' />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
</div>
|
||||
<div className="flex items-center"></div>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center">
|
||||
<div className="mr-4 flex shrink-0 items-center">
|
||||
<input disabled={!enable} type="number" min={min} max={max} step={step} className="block w-[48px] h-7 text-xs leading-[18px] rounded-lg border-0 pl-1 pl py-1.5 bg-gray-50 text-gray-900 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-primary-600 disabled:opacity-60" value={value} onChange={(e) => {
|
||||
const value = parseFloat(e.target.value)
|
||||
if (value < min || value > max)
|
||||
return
|
||||
|
||||
onChange(id, value)
|
||||
}} />
|
||||
</div>
|
||||
<div className="flex items-center h-7 grow">
|
||||
<Slider
|
||||
className='w-full'
|
||||
disabled={!enable}
|
||||
value={max < 5 ? value * 100 : value}
|
||||
min={min < 1 ? min * 100 : min}
|
||||
max={max < 5 ? max * 100 : max}
|
||||
onChange={value => onChange(id, value / (max < 5 ? 100 : 1))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default ParamItem
|
54
web/app/components/base/param-item/score-threshold-item.tsx
Normal file
54
web/app/components/base/param-item/score-threshold-item.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ParamItem from '.'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
value: number
|
||||
onChange: (key: string, value: number) => void
|
||||
enable: boolean
|
||||
hasSwitch?: boolean
|
||||
onSwitchChange?: (key: string, enable: boolean) => void
|
||||
}
|
||||
|
||||
const VALUE_LIMIT = {
|
||||
default: 0.7,
|
||||
step: 0.01,
|
||||
min: 0,
|
||||
max: 1,
|
||||
}
|
||||
|
||||
const key = 'score_threshold'
|
||||
const ScoreThresholdItem: FC<Props> = ({
|
||||
className,
|
||||
value,
|
||||
enable,
|
||||
onChange,
|
||||
hasSwitch,
|
||||
onSwitchChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const handleParamChange = (key: string, value: number) => {
|
||||
let notOutRangeValue = parseFloat(value.toFixed(2))
|
||||
notOutRangeValue = Math.max(VALUE_LIMIT.min, notOutRangeValue)
|
||||
notOutRangeValue = Math.min(VALUE_LIMIT.max, notOutRangeValue)
|
||||
onChange(key, notOutRangeValue)
|
||||
}
|
||||
return (
|
||||
<ParamItem
|
||||
className={className}
|
||||
id={key}
|
||||
name={t(`appDebug.datasetConfig.${key}`)}
|
||||
tip={t(`appDebug.datasetConfig.${key}Tip`)}
|
||||
{...VALUE_LIMIT}
|
||||
value={value}
|
||||
enable={enable}
|
||||
onChange={handleParamChange}
|
||||
hasSwitch={hasSwitch}
|
||||
onSwitchChange={onSwitchChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default React.memo(ScoreThresholdItem)
|
48
web/app/components/base/param-item/top-k-item.tsx
Normal file
48
web/app/components/base/param-item/top-k-item.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ParamItem from '.'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
value: number
|
||||
onChange: (key: string, value: number) => void
|
||||
enable: boolean
|
||||
}
|
||||
|
||||
const VALUE_LIMIT = {
|
||||
default: 2,
|
||||
step: 1,
|
||||
min: 1,
|
||||
max: 10,
|
||||
}
|
||||
|
||||
const key = 'top_k'
|
||||
const TopKItem: FC<Props> = ({
|
||||
className,
|
||||
value,
|
||||
enable,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const handleParamChange = (key: string, value: number) => {
|
||||
let notOutRangeValue = parseFloat(value.toFixed(2))
|
||||
notOutRangeValue = Math.max(VALUE_LIMIT.min, notOutRangeValue)
|
||||
notOutRangeValue = Math.min(VALUE_LIMIT.max, notOutRangeValue)
|
||||
onChange(key, notOutRangeValue)
|
||||
}
|
||||
return (
|
||||
<ParamItem
|
||||
className={className}
|
||||
id={key}
|
||||
name={t(`appDebug.datasetConfig.${key}`)}
|
||||
tip={t(`appDebug.datasetConfig.${key}Tip`)}
|
||||
{...VALUE_LIMIT}
|
||||
value={value}
|
||||
enable={enable}
|
||||
onChange={handleParamChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default React.memo(TopKItem)
|
@ -1,6 +1,8 @@
|
||||
import ReactSlider from 'react-slider'
|
||||
import cn from 'classnames'
|
||||
import './style.css'
|
||||
type ISliderProps = {
|
||||
className?: string
|
||||
value: number
|
||||
max?: number
|
||||
min?: number
|
||||
@ -9,14 +11,14 @@ type ISliderProps = {
|
||||
onChange: (value: number) => void
|
||||
}
|
||||
|
||||
const Slider: React.FC<ISliderProps> = ({ max, min, step, value, disabled, onChange }) => {
|
||||
const Slider: React.FC<ISliderProps> = ({ className, max, min, step, value, disabled, onChange }) => {
|
||||
return <ReactSlider
|
||||
disabled={disabled}
|
||||
value={isNaN(value) ? 0 : value}
|
||||
min={min || 0}
|
||||
max={max || 100}
|
||||
step={step || 1}
|
||||
className="slider"
|
||||
className={cn(className, 'slider')}
|
||||
thumbClassName="slider-thumb"
|
||||
trackClassName="slider-track"
|
||||
onChange={onChange}
|
||||
|
@ -56,6 +56,15 @@ For high-quality text generation, such as articles, summaries, and translations,
|
||||
<Property name='user' type='string' key='user'>
|
||||
The user identifier, defined by the developer, must ensure uniqueness within the app.
|
||||
</Property>
|
||||
<Property name='files' type='array[object]' key='files'>
|
||||
Uploaded files.
|
||||
- `type` file type: currently only `image` is supported.
|
||||
- `transfer_method` transfer method:
|
||||
- `remote_url`: Image address.
|
||||
- `local_file`: upload file.
|
||||
- `upload_file_id` Upload file ID. Required when `transfer_method` is `local_file`.
|
||||
- `url` image address. Required when `transfer_method` is `remote_url`.
|
||||
</Property>
|
||||
</Properties>
|
||||
</Col>
|
||||
<Col sticky>
|
||||
@ -168,6 +177,57 @@ For high-quality text generation, such as articles, summaries, and translations,
|
||||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/files/upload'
|
||||
method='POST'
|
||||
title='Upload files'
|
||||
name='#files-upload'
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
Upload files to the server for text generation and dialogue. Uploaded files are only available to the current end user.
|
||||
|
||||
### Request Body
|
||||
The request method is `multipart/form-data`
|
||||
<Properties>
|
||||
<Property name='file' type='file' key='file'>
|
||||
The file to upload. Currently supports png, jpg, jpeg, webp, gif format files.
|
||||
</Property>
|
||||
<Property name='user' type='string' key='user'>
|
||||
User ID, rules are defined by the developer, and it is necessary to ensure that the user ID is unique within the application.
|
||||
</Property>
|
||||
</Properties>
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="POST" label="/files/upload" targetCode={`curl --location --request POST '${props.appDetail.api_base_url}/files/upload' \\\n--header 'Authorization: Bearer ENTER-YOUR-SECRET-KEY' \\\n--form 'file=@localfile;type=image/[png|jpeg|jpg|webp|gif] \\\n--form 'user=abc-123'`}>
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl --location --request POST '${props.appDetail.api_base_url}/files/upload' \
|
||||
--header 'Authorization: Bearer ENTER-YOUR-SECRET-KEY' \
|
||||
--form 'file=@"/path/to/file"'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
```json {{ title: 'Response' }}
|
||||
{
|
||||
"id": "72fa9618-8f89-4a37-9b33-7e1178a24a67",
|
||||
"name": "example.png",
|
||||
"size": 1024,
|
||||
"extension": "png",
|
||||
"mime_type": "image/png",
|
||||
"created_by": 123,
|
||||
"created_at": 1577836800,
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/parameters'
|
||||
method='GET'
|
||||
@ -176,7 +236,9 @@ For high-quality text generation, such as articles, summaries, and translations,
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
Retrieve configured Input parameters, including variable names, field names, types, and default values. Typically used for displaying these fields in a form or filling in default values after the client loads.
|
||||
Content include:
|
||||
1. Retrieve configured Input parameters, including variable names, field names, types, and default values. Typically used for displaying these fields in a form or filling in default values after the client loads.
|
||||
2. Configuration of uploading images, including whether to enable image uploading, the number of uploaded images and the uploading method. Note: This configuration is only available when using a model that supports multimodality.
|
||||
|
||||
### Query
|
||||
|
||||
@ -201,19 +263,30 @@ For high-quality text generation, such as articles, summaries, and translations,
|
||||
```json {{ title: 'Response' }}
|
||||
{
|
||||
"introduction": "nice to meet you",
|
||||
"variables": [
|
||||
"user_input_form": [
|
||||
{
|
||||
"key": "book",
|
||||
"name": "Which book?",
|
||||
"description": null,
|
||||
"type": "string",
|
||||
"default": null,
|
||||
"options": null
|
||||
},
|
||||
"text-input": {
|
||||
"label": "a",
|
||||
"variable": "a",
|
||||
"required": true,
|
||||
"max_length": 48,
|
||||
"default": ""
|
||||
}
|
||||
}
|
||||
{
|
||||
// ...
|
||||
}
|
||||
]
|
||||
],
|
||||
"file_upload": {
|
||||
"image": {
|
||||
"enabled": true,
|
||||
"number_limits": 3,
|
||||
"transfer_methods": [
|
||||
"remote_url",
|
||||
"local_file"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
@ -56,6 +56,15 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
|
||||
<Property name='user' type='string' key='user'>
|
||||
用户标识,由开发者定义规则,需保证用户标识在应用内唯一。
|
||||
</Property>
|
||||
<Property name='files' type='array[object]' key='files'>
|
||||
上传的文件。
|
||||
- `type` 文件类型:目前仅支持 `image`。
|
||||
- `transfer_method` 传递方式:
|
||||
- `remote_url`: 图片地址。
|
||||
- `local_file`: 上传文件。
|
||||
- `upload_file_id` 上传文件 ID。`transfer_method` 为 `local_file` 时必填。
|
||||
- `url` 图片地址。`transfer_method` 为 `remote_url` 时必填。
|
||||
</Property>
|
||||
</Properties>
|
||||
</Col>
|
||||
<Col sticky>
|
||||
@ -168,6 +177,57 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
|
||||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/files/upload'
|
||||
method='POST'
|
||||
title='上传文件'
|
||||
name='#files-upload'
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
上传文件到服务器,供文本生成、对话使用。上传的文件仅供当前终端用户使用。
|
||||
|
||||
### Request Body
|
||||
请求方式为 `multipart/form-data`。
|
||||
<Properties>
|
||||
<Property name='file' type='file' key='file'>
|
||||
要上传的文件。目前支持 png, jpg, jpeg, webp, gif 格式文件。
|
||||
</Property>
|
||||
<Property name='user' type='string' key='user'>
|
||||
用户标识,由开发者定义规则,需保证用户标识在应用内唯一。
|
||||
</Property>
|
||||
</Properties>
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="POST" label="/files/upload" targetCode={`curl --location --request POST '${props.appDetail.api_base_url}/files/upload' \\\n--header 'Authorization: Bearer ENTER-YOUR-SECRET-KEY' \\\n--form 'file=@localfile;type=image/[png|jpeg|jpg|webp|gif] \\\n--form 'user=abc-123'`}>
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl --location --request POST '${props.appDetail.api_base_url}/files/upload' \
|
||||
--header 'Authorization: Bearer ENTER-YOUR-SECRET-KEY' \
|
||||
--form 'file=@"/path/to/file"'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
```json {{ title: 'Response' }}
|
||||
{
|
||||
"id": "72fa9618-8f89-4a37-9b33-7e1178a24a67",
|
||||
"name": "example.png",
|
||||
"size": 1024,
|
||||
"extension": "png",
|
||||
"mime_type": "image/png",
|
||||
"created_by": 123,
|
||||
"created_at": 1577836800,
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/parameters'
|
||||
method='GET'
|
||||
@ -176,7 +236,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
获取已配置的 Input 参数,包括变量名、字段名称、类型与默认值。通常用于客户端加载后显示这些字段的表单或填入默认值。
|
||||
内容包括:
|
||||
1. 已配置的 Input 参数,包括变量名、字段名称、类型与默认值。通常用于客户端加载后显示这些字段的表单或填入默认值。
|
||||
2. 上传图片的配置,包括是否开启上传图片,上传图片的数量和上传方式。注意:这个配置只有使用支持多模态的模型时才会生效。
|
||||
|
||||
### Query
|
||||
|
||||
@ -201,19 +263,30 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
|
||||
```json {{ title: 'Response' }}
|
||||
{
|
||||
"introduction": "nice to meet you",
|
||||
"variables": [
|
||||
"user_input_form": [
|
||||
{
|
||||
"key": "book",
|
||||
"name": "Which book?",
|
||||
"description": null,
|
||||
"type": "string",
|
||||
"default": null,
|
||||
"options": null
|
||||
},
|
||||
"text-input": {
|
||||
"label": "a",
|
||||
"variable": "a",
|
||||
"required": true,
|
||||
"max_length": 48,
|
||||
"default": ""
|
||||
}
|
||||
}
|
||||
{
|
||||
// ...
|
||||
}
|
||||
]
|
||||
],
|
||||
"file_upload": {
|
||||
"image": {
|
||||
"enabled": true,
|
||||
"number_limits": 3,
|
||||
"transfer_methods": [
|
||||
"remote_url",
|
||||
"local_file"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
@ -62,6 +62,15 @@ For versatile conversational apps using a Q&A format, call the chat-messages API
|
||||
<Property name='user' type='string' key='user'>
|
||||
The user identifier, defined by the developer, must ensure uniqueness within the app.
|
||||
</Property>
|
||||
<Property name='files' type='array[object]' key='files'>
|
||||
Uploaded files.
|
||||
- `type` file type: currently only `image` is supported.
|
||||
- `transfer_method` transfer method:
|
||||
- `remote_url`: Image address.
|
||||
- `local_file`: upload file.
|
||||
- `upload_file_id` Upload file ID. Required when `transfer_method` is `local_file`.
|
||||
- `url` image address. Required when `transfer_method` is `remote_url`.
|
||||
</Property>
|
||||
</Properties>
|
||||
</Col>
|
||||
<Col sticky>
|
||||
@ -449,6 +458,57 @@ For versatile conversational apps using a Q&A format, call the chat-messages API
|
||||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/files/upload'
|
||||
method='POST'
|
||||
title='Upload files'
|
||||
name='#files-upload'
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
Upload files to the server for text generation and dialogue. Uploaded files are only available to the current end user.
|
||||
|
||||
### Request Body
|
||||
The request method is `multipart/form-data`
|
||||
<Properties>
|
||||
<Property name='file' type='file' key='file'>
|
||||
The file to upload. Currently supports png, jpg, jpeg, webp, gif format files.
|
||||
</Property>
|
||||
<Property name='user' type='string' key='user'>
|
||||
User ID, rules are defined by the developer, and it is necessary to ensure that the user ID is unique within the application.
|
||||
</Property>
|
||||
</Properties>
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="POST" label="/files/upload" targetCode={`curl --location --request POST '${props.appDetail.api_base_url}/files/upload' \\\n--header 'Authorization: Bearer ENTER-YOUR-SECRET-KEY' \\\n--form 'file=@localfile;type=image/[png|jpeg|jpg|webp|gif] \\\n--form 'user=abc-123'`}>
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl --location --request POST '${props.appDetail.api_base_url}/files/upload' \
|
||||
--header 'Authorization: Bearer ENTER-YOUR-SECRET-KEY' \
|
||||
--form 'file=@"/path/to/file"'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
```json {{ title: 'Response' }}
|
||||
{
|
||||
"id": "72fa9618-8f89-4a37-9b33-7e1178a24a67",
|
||||
"name": "example.png",
|
||||
"size": 1024,
|
||||
"extension": "png",
|
||||
"mime_type": "image/png",
|
||||
"created_by": 123,
|
||||
"created_at": 1577836800,
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/audio-to-text'
|
||||
method='POST'
|
||||
@ -500,7 +560,9 @@ For versatile conversational apps using a Q&A format, call the chat-messages API
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
Retrieve configured Input parameters, including variable names, field names, types, and default values. Typically used for displaying these fields in a form or filling in default values after the client loads.
|
||||
Content include:
|
||||
1. Retrieve configured Input parameters, including variable names, field names, types, and default values. Typically used for displaying these fields in a form or filling in default values after the client loads.
|
||||
2. Configuration of uploading images, including whether to enable image uploading, the number of uploaded images and the uploading method. Note: This configuration is only available when using a model that supports multimodality.
|
||||
|
||||
### Query
|
||||
|
||||
@ -525,19 +587,30 @@ For versatile conversational apps using a Q&A format, call the chat-messages API
|
||||
```json {{ title: 'Response' }}
|
||||
{
|
||||
"introduction": "nice to meet you",
|
||||
"variables": [
|
||||
"user_input_form": [
|
||||
{
|
||||
"key": "book",
|
||||
"name": "book",
|
||||
"description": null,
|
||||
"type": "string",
|
||||
"default": null,
|
||||
"options": null
|
||||
},
|
||||
"text-input": {
|
||||
"label": "a",
|
||||
"variable": "a",
|
||||
"required": true,
|
||||
"max_length": 48,
|
||||
"default": ""
|
||||
}
|
||||
}
|
||||
{
|
||||
// ...
|
||||
}
|
||||
]
|
||||
],
|
||||
"file_upload": {
|
||||
"image": {
|
||||
"enabled": true,
|
||||
"number_limits": 3,
|
||||
"transfer_methods": [
|
||||
"remote_url",
|
||||
"local_file"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
@ -62,6 +62,15 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
|
||||
<Property name='user' type='string' key='user'>
|
||||
用户标识,由开发者定义规则,需保证用户标识在应用内唯一。
|
||||
</Property>
|
||||
<Property name='files' type='array[object]' key='files'>
|
||||
上传的文件。
|
||||
- `type` 文件类型:目前仅支持 `image`。
|
||||
- `transfer_method` 传递方式:
|
||||
- `remote_url`: 图片地址。
|
||||
- `local_file`: 上传文件。
|
||||
- `upload_file_id` 上传文件 ID。`transfer_method` 为 `local_file` 时必填。
|
||||
- `url` 图片地址。`transfer_method` 为 `remote_url` 时必填。
|
||||
</Property>
|
||||
</Properties>
|
||||
</Col>
|
||||
<Col sticky>
|
||||
@ -448,6 +457,57 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
|
||||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/files/upload'
|
||||
method='POST'
|
||||
title='上传文件'
|
||||
name='#files-upload'
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
上传文件到服务器,供文本生成、对话使用。上传的文件仅供当前终端用户使用。
|
||||
|
||||
### Request Body
|
||||
请求方式为 `multipart/form-data`。
|
||||
<Properties>
|
||||
<Property name='file' type='file' key='file'>
|
||||
要上传的文件。目前支持 png, jpg, jpeg, webp, gif 格式文件。
|
||||
</Property>
|
||||
<Property name='user' type='string' key='user'>
|
||||
用户标识,由开发者定义规则,需保证用户标识在应用内唯一。
|
||||
</Property>
|
||||
</Properties>
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="POST" label="/files/upload" targetCode={`curl --location --request POST '${props.appDetail.api_base_url}/files/upload' \\\n--header 'Authorization: Bearer ENTER-YOUR-SECRET-KEY' \\\n--form 'file=@localfile;type=image/[png|jpeg|jpg|webp|gif] \\\n--form 'user=abc-123'`}>
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl --location --request POST '${props.appDetail.api_base_url}/files/upload' \
|
||||
--header 'Authorization: Bearer ENTER-YOUR-SECRET-KEY' \
|
||||
--form 'file=@"/path/to/file"'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
```json {{ title: 'Response' }}
|
||||
{
|
||||
"id": "72fa9618-8f89-4a37-9b33-7e1178a24a67",
|
||||
"name": "example.png",
|
||||
"size": 1024,
|
||||
"extension": "png",
|
||||
"mime_type": "image/png",
|
||||
"created_by": 123,
|
||||
"created_at": 1577836800,
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/audio-to-text'
|
||||
method='POST'
|
||||
@ -499,7 +559,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
获取已配置的 Input 参数,包括变量名、字段名称、类型与默认值。通常用于客户端加载后显示这些字段的表单或填入默认值。
|
||||
内容包括:
|
||||
1. 已配置的 Input 参数,包括变量名、字段名称、类型与默认值。通常用于客户端加载后显示这些字段的表单或填入默认值。
|
||||
2. 上传图片的配置,包括是否开启上传图片,上传图片的数量和上传方式。注意:这个配置只有使用支持多模态的模型时才会生效。
|
||||
|
||||
### Query
|
||||
|
||||
@ -524,19 +586,30 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
|
||||
```json {{ title: 'Response' }}
|
||||
{
|
||||
"introduction": "nice to meet you",
|
||||
"variables": [
|
||||
"user_input_form": [
|
||||
{
|
||||
"key": "book",
|
||||
"name": "book",
|
||||
"description": null,
|
||||
"type": "string",
|
||||
"default": null,
|
||||
"options": null
|
||||
},
|
||||
"text-input": {
|
||||
"label": "a",
|
||||
"variable": "a",
|
||||
"required": true,
|
||||
"max_length": 48,
|
||||
"default": ""
|
||||
}
|
||||
}
|
||||
{
|
||||
// ...
|
||||
}
|
||||
]
|
||||
],
|
||||
"file_upload": {
|
||||
"image": {
|
||||
"enabled": true,
|
||||
"number_limits": 3,
|
||||
"transfer_methods": [
|
||||
"remote_url",
|
||||
"local_file"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
fetchChatList,
|
||||
fetchConversations,
|
||||
fetchSuggestedQuestions,
|
||||
generationConversationName,
|
||||
pinConversation,
|
||||
sendChatMessage,
|
||||
stopChatMessageResponding,
|
||||
@ -564,7 +565,11 @@ const Main: FC<IMainProps> = () => {
|
||||
|
||||
if (getConversationIdChangeBecauseOfNew()) {
|
||||
const { data: allConversations }: any = await fetchAllConversations()
|
||||
setAllConversationList(allConversations)
|
||||
const newItem: any = await generationConversationName(allConversations[0].id)
|
||||
const newAllConversations = produce(allConversations, (draft: any) => {
|
||||
draft[0].name = newItem.name
|
||||
})
|
||||
setAllConversationList(newAllConversations as any)
|
||||
noticeUpdateList()
|
||||
}
|
||||
setConversationIdChangeBecauseOfNew(false)
|
||||
|
@ -61,7 +61,6 @@ const Item: FC<ItemProps> = ({
|
||||
onCancel={() => setShowDeleteConfirm(false)}
|
||||
title={`${t('common.operation.delete')} “${data.name}”?`}
|
||||
onConfirm={handleDeleteApiBasedExtension}
|
||||
desc={t('common.apiBasedExtension.confirm.desc') || ''}
|
||||
confirmWrapperClassName='!z-30'
|
||||
confirmText={t('common.operation.delete') || ''}
|
||||
confirmBtnClassName='!bg-[#D92D20]'
|
||||
|
@ -71,6 +71,7 @@ export enum ModelType {
|
||||
|
||||
export enum ModelFeature {
|
||||
agentThought = 'agent_thought',
|
||||
vision = 'vision',
|
||||
}
|
||||
|
||||
// backend defined model struct: /console/api/workspaces/current/models/model-type/:model_type
|
||||
|
@ -3,6 +3,7 @@
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import cn from 'classnames'
|
||||
import useSWR from 'swr'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import produce from 'immer'
|
||||
@ -22,6 +23,7 @@ import {
|
||||
fetchChatList,
|
||||
fetchConversations,
|
||||
fetchSuggestedQuestions,
|
||||
generationConversationName,
|
||||
pinConversation,
|
||||
sendChatMessage,
|
||||
stopChatMessageResponding,
|
||||
@ -39,6 +41,9 @@ import { replaceStringWithValues } from '@/app/components/app/configuration/prom
|
||||
import { userInputsFormToPromptVariables } from '@/utils/model-config'
|
||||
import type { InstalledApp } from '@/models/explore'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import type { VisionFile, VisionSettings } from '@/types/app'
|
||||
import { Resolution, TransferMethod } from '@/types/app'
|
||||
import { fetchFileUploadConfig } from '@/service/common'
|
||||
|
||||
export type IMainProps = {
|
||||
isInstalledApp?: boolean
|
||||
@ -244,6 +249,7 @@ const Main: FC<IMainProps> = ({
|
||||
id: `question-${item.id}`,
|
||||
content: item.query,
|
||||
isAnswer: false,
|
||||
message_files: item.message_files,
|
||||
})
|
||||
newChatList.push({
|
||||
id: item.id,
|
||||
@ -351,6 +357,8 @@ const Main: FC<IMainProps> = ({
|
||||
: fetchAppInfo(), fetchAllConversations(), fetchAppParams(isInstalledApp, installedAppInfo?.id)])
|
||||
}
|
||||
|
||||
const { data: fileUploadConfigResponse } = useSWR(isInstalledApp ? { url: '/files/upload' } : null, fetchFileUploadConfig)
|
||||
|
||||
// init
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@ -368,7 +376,11 @@ const Main: FC<IMainProps> = ({
|
||||
const isNotNewConversation = allConversations.some(item => item.id === _conversationId)
|
||||
setAllConversationList(allConversations)
|
||||
// fetch new conversation info
|
||||
const { user_input_form, opening_statement: introduction, suggested_questions_after_answer, speech_to_text, retriever_resource, sensitive_word_avoidance }: any = appParams
|
||||
const { user_input_form, opening_statement: introduction, suggested_questions_after_answer, speech_to_text, retriever_resource, file_upload, sensitive_word_avoidance }: any = appParams
|
||||
setVisionConfig({
|
||||
...file_upload.image,
|
||||
image_file_size_limit: appParams?.system_parameters?.image_file_size_limit,
|
||||
})
|
||||
const prompt_variables = userInputsFormToPromptVariables(user_input_form)
|
||||
if (siteInfo.default_language)
|
||||
changeLanguage(siteInfo.default_language)
|
||||
@ -448,24 +460,48 @@ const Main: FC<IMainProps> = ({
|
||||
const [messageTaskId, setMessageTaskId] = useState('')
|
||||
const [hasStopResponded, setHasStopResponded, getHasStopResponded] = useGetState(false)
|
||||
const [isResponsingConIsCurrCon, setIsResponsingConCurrCon, getIsResponsingConIsCurrCon] = useGetState(true)
|
||||
|
||||
const handleSend = async (message: string) => {
|
||||
const [visionConfig, setVisionConfig] = useState<VisionSettings>({
|
||||
enabled: false,
|
||||
number_limits: 2,
|
||||
detail: Resolution.low,
|
||||
transfer_methods: [TransferMethod.local_file],
|
||||
})
|
||||
const handleSend = async (message: string, files?: VisionFile[]) => {
|
||||
if (isResponsing) {
|
||||
notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') })
|
||||
return
|
||||
}
|
||||
const data = {
|
||||
|
||||
if (files?.find(item => item.transfer_method === TransferMethod.local_file && !item.upload_file_id)) {
|
||||
notify({ type: 'info', message: t('appDebug.errorMessage.waitForImgUpload') })
|
||||
return false
|
||||
}
|
||||
|
||||
const data: Record<string, any> = {
|
||||
inputs: currInputs,
|
||||
query: message,
|
||||
conversation_id: isNewConversation ? null : currConversationId,
|
||||
}
|
||||
|
||||
if (visionConfig.enabled && files && files?.length > 0) {
|
||||
data.files = files.map((item) => {
|
||||
if (item.transfer_method === TransferMethod.local_file) {
|
||||
return {
|
||||
...item,
|
||||
url: '',
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
// qustion
|
||||
const questionId = `question-${Date.now()}`
|
||||
const questionItem = {
|
||||
id: questionId,
|
||||
content: message,
|
||||
isAnswer: false,
|
||||
message_files: files,
|
||||
}
|
||||
|
||||
const placeholderAnswerId = `answer-placeholder-${Date.now()}`
|
||||
@ -519,13 +555,17 @@ const Main: FC<IMainProps> = ({
|
||||
setChatList(newListWithAnswer)
|
||||
},
|
||||
async onCompleted(hasError?: boolean) {
|
||||
setResponsingFalse()
|
||||
if (hasError)
|
||||
return
|
||||
|
||||
if (getConversationIdChangeBecauseOfNew()) {
|
||||
const { data: allConversations }: any = await fetchAllConversations()
|
||||
setAllConversationList(allConversations)
|
||||
const newItem: any = await generationConversationName(isInstalledApp, installedAppInfo?.id, allConversations[0].id)
|
||||
|
||||
const newAllConversations = produce(allConversations, (draft: any) => {
|
||||
draft[0].name = newItem.name
|
||||
})
|
||||
setAllConversationList(newAllConversations as any)
|
||||
noticeUpdateList()
|
||||
}
|
||||
setConversationIdChangeBecauseOfNew(false)
|
||||
@ -537,6 +577,7 @@ const Main: FC<IMainProps> = ({
|
||||
setSuggestQuestions(data)
|
||||
setIsShowSuggestion(true)
|
||||
}
|
||||
setResponsingFalse()
|
||||
},
|
||||
onMessageEnd: isInstalledApp
|
||||
? (messageEnd) => {
|
||||
@ -717,6 +758,10 @@ const Main: FC<IMainProps> = ({
|
||||
suggestionList={suggestQuestions}
|
||||
isShowSpeechToText={speechToTextConfig?.enabled}
|
||||
isShowCitation={citationConfig?.enabled && isInstalledApp}
|
||||
visionConfig={{
|
||||
...visionConfig,
|
||||
image_file_size_limit: fileUploadConfigResponse ? fileUploadConfigResponse.image_file_size_limit : visionConfig.image_file_size_limit,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>)
|
||||
|
@ -14,7 +14,7 @@ import s from './style.module.css'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import ConfigScene from '@/app/components/share/chatbot/config-scence'
|
||||
import Header from '@/app/components/share/header'
|
||||
import { fetchAppInfo, fetchAppParams, fetchChatList, fetchConversations, fetchSuggestedQuestions, sendChatMessage, stopChatMessageResponding, updateFeedback } from '@/service/share'
|
||||
import { fetchAppInfo, fetchAppParams, fetchChatList, fetchConversations, fetchSuggestedQuestions, generationConversationName, sendChatMessage, stopChatMessageResponding, updateFeedback } from '@/service/share'
|
||||
import type { ConversationItem, SiteInfo } from '@/models/share'
|
||||
import type { PromptConfig, SuggestedQuestionsAfterAnswerConfig } from '@/models/debug'
|
||||
import type { Feedbacktype, IChatItem } from '@/app/components/app/chat/type'
|
||||
@ -28,6 +28,8 @@ import type { InstalledApp } from '@/models/explore'
|
||||
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
|
||||
import LogoHeader from '@/app/components/base/logo/logo-embeded-chat-header'
|
||||
import LogoAvatar from '@/app/components/base/logo/logo-embeded-chat-avatar'
|
||||
import type { VisionFile, VisionSettings } from '@/types/app'
|
||||
import { Resolution, TransferMethod } from '@/types/app'
|
||||
|
||||
export type IMainProps = {
|
||||
isInstalledApp?: boolean
|
||||
@ -184,6 +186,7 @@ const Main: FC<IMainProps> = ({
|
||||
id: `question-${item.id}`,
|
||||
content: item.query,
|
||||
isAnswer: false,
|
||||
message_files: item.message_files,
|
||||
})
|
||||
newChatList.push({
|
||||
id: item.id,
|
||||
@ -292,7 +295,11 @@ const Main: FC<IMainProps> = ({
|
||||
const isNotNewConversation = allConversations.some(item => item.id === _conversationId)
|
||||
setAllConversationList(allConversations)
|
||||
// fetch new conversation info
|
||||
const { user_input_form, opening_statement: introduction, suggested_questions_after_answer, speech_to_text, sensitive_word_avoidance }: any = appParams
|
||||
const { user_input_form, opening_statement: introduction, suggested_questions_after_answer, speech_to_text, file_upload, sensitive_word_avoidance }: any = appParams
|
||||
setVisionConfig({
|
||||
...file_upload.image,
|
||||
image_file_size_limit: appParams?.system_parameters?.image_file_size_limit,
|
||||
})
|
||||
const prompt_variables = userInputsFormToPromptVariables(user_input_form)
|
||||
if (siteInfo.default_language)
|
||||
changeLanguage(siteInfo.default_language)
|
||||
@ -371,24 +378,48 @@ const Main: FC<IMainProps> = ({
|
||||
const [messageTaskId, setMessageTaskId] = useState('')
|
||||
const [hasStopResponded, setHasStopResponded, getHasStopResponded] = useGetState(false)
|
||||
const [shouldReload, setShouldReload] = useState(false)
|
||||
const [visionConfig, setVisionConfig] = useState<VisionSettings>({
|
||||
enabled: false,
|
||||
number_limits: 2,
|
||||
detail: Resolution.low,
|
||||
transfer_methods: [TransferMethod.local_file],
|
||||
})
|
||||
|
||||
const handleSend = async (message: string) => {
|
||||
const handleSend = async (message: string, files?: VisionFile[]) => {
|
||||
if (isResponsing) {
|
||||
notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') })
|
||||
return
|
||||
}
|
||||
const data = {
|
||||
|
||||
if (files?.find(item => item.transfer_method === TransferMethod.local_file && !item.upload_file_id)) {
|
||||
notify({ type: 'info', message: t('appDebug.errorMessage.waitForImgUpload') })
|
||||
return false
|
||||
}
|
||||
const data: Record<string, any> = {
|
||||
inputs: currInputs,
|
||||
query: message,
|
||||
conversation_id: isNewConversation ? null : currConversationId,
|
||||
}
|
||||
|
||||
if (visionConfig.enabled && files && files?.length > 0) {
|
||||
data.files = files.map((item) => {
|
||||
if (item.transfer_method === TransferMethod.local_file) {
|
||||
return {
|
||||
...item,
|
||||
url: '',
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
// qustion
|
||||
const questionId = `question-${Date.now()}`
|
||||
const questionItem = {
|
||||
id: questionId,
|
||||
content: message,
|
||||
isAnswer: false,
|
||||
message_files: files,
|
||||
}
|
||||
|
||||
const placeholderAnswerId = `answer-placeholder-${Date.now()}`
|
||||
@ -436,13 +467,16 @@ const Main: FC<IMainProps> = ({
|
||||
setChatList(newListWithAnswer)
|
||||
},
|
||||
async onCompleted(hasError?: boolean) {
|
||||
setResponsingFalse()
|
||||
if (hasError)
|
||||
return
|
||||
|
||||
if (getConversationIdChangeBecauseOfNew()) {
|
||||
const { data: allConversations }: any = await fetchAllConversations()
|
||||
setAllConversationList(allConversations)
|
||||
const newItem: any = await generationConversationName(isInstalledApp, installedAppInfo?.id, allConversations[0].id)
|
||||
const newAllConversations = produce(allConversations, (draft: any) => {
|
||||
draft[0].name = newItem.name
|
||||
})
|
||||
setAllConversationList(newAllConversations as any)
|
||||
noticeUpdateList()
|
||||
}
|
||||
setConversationIdChangeBecauseOfNew(false)
|
||||
@ -454,6 +488,7 @@ const Main: FC<IMainProps> = ({
|
||||
setSuggestQuestions(data)
|
||||
setIsShowSuggestion(true)
|
||||
}
|
||||
setResponsingFalse()
|
||||
},
|
||||
onMessageReplace: (messageReplace) => {
|
||||
setChatList(produce(
|
||||
@ -581,6 +616,7 @@ const Main: FC<IMainProps> = ({
|
||||
displayScene='web'
|
||||
isShowSpeechToText={speechToTextConfig?.enabled}
|
||||
answerIcon={<LogoAvatar className='relative shrink-0' />}
|
||||
visionConfig={visionConfig}
|
||||
/>
|
||||
</div>
|
||||
</div>)
|
||||
|
@ -26,6 +26,8 @@ import SavedItems from '@/app/components/app/text-generate/saved-items'
|
||||
import type { InstalledApp } from '@/models/explore'
|
||||
import { DEFAULT_VALUE_MAX_LEN, appDefaultIconBackground } from '@/config'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import type { VisionFile, VisionSettings } from '@/types/app'
|
||||
import { Resolution, TransferMethod } from '@/types/app'
|
||||
|
||||
const GROUP_SIZE = 5 // to avoid RPM(Request per minute) limit. The group task finished then the next group.
|
||||
enum TaskStatus {
|
||||
@ -92,6 +94,14 @@ const TextGeneration: FC<IMainProps> = ({
|
||||
// send message task
|
||||
const [controlSend, setControlSend] = useState(0)
|
||||
const [controlStopResponding, setControlStopResponding] = useState(0)
|
||||
const [visionConfig, setVisionConfig] = useState<VisionSettings>({
|
||||
enabled: false,
|
||||
number_limits: 2,
|
||||
detail: Resolution.low,
|
||||
transfer_methods: [TransferMethod.local_file],
|
||||
})
|
||||
const [completionFiles, setCompletionFiles] = useState<VisionFile[]>([])
|
||||
|
||||
const handleSend = () => {
|
||||
setIsCallBatchAPI(false)
|
||||
setControlSend(Date.now())
|
||||
@ -338,7 +348,11 @@ const TextGeneration: FC<IMainProps> = ({
|
||||
setSiteInfo(siteInfo as SiteInfo)
|
||||
changeLanguage(siteInfo.default_language)
|
||||
|
||||
const { user_input_form, more_like_this, sensitive_word_avoidance }: any = appParams
|
||||
const { user_input_form, more_like_this, file_upload, sensitive_word_avoidance }: any = appParams
|
||||
setVisionConfig({
|
||||
...file_upload.image,
|
||||
image_file_size_limit: appParams?.system_parameters?.image_file_size_limit,
|
||||
})
|
||||
const prompt_variables = userInputsFormToPromptVariables(user_input_form)
|
||||
setPromptConfig({
|
||||
prompt_template: '', // placeholder for feture
|
||||
@ -378,6 +392,8 @@ const TextGeneration: FC<IMainProps> = ({
|
||||
handleSaveMessage={handleSaveMessage}
|
||||
taskId={task?.id}
|
||||
onCompleted={handleCompleted}
|
||||
visionConfig={visionConfig}
|
||||
completionFiles={completionFiles}
|
||||
/>)
|
||||
|
||||
const renderBatchRes = () => {
|
||||
@ -512,6 +528,8 @@ const TextGeneration: FC<IMainProps> = ({
|
||||
onInputsChange={setInputs}
|
||||
promptConfig={promptConfig}
|
||||
onSend={handleSend}
|
||||
visionConfig={visionConfig}
|
||||
onVisionFilesChange={setCompletionFiles}
|
||||
/>
|
||||
</div>
|
||||
<div className={cn(isInBatchTab ? 'block' : 'hidden')}>
|
||||
|
@ -13,6 +13,7 @@ import Loading from '@/app/components/base/loading'
|
||||
import type { PromptConfig } from '@/models/debug'
|
||||
import type { InstalledApp } from '@/models/explore'
|
||||
import type { ModerationService } from '@/models/common'
|
||||
import { TransferMethod, type VisionFile, type VisionSettings } from '@/types/app'
|
||||
export type IResultProps = {
|
||||
isCallBatchAPI: boolean
|
||||
isPC: boolean
|
||||
@ -32,6 +33,8 @@ export type IResultProps = {
|
||||
onCompleted: (completionRes: string, taskId?: number, success?: boolean) => void
|
||||
enableModeration?: boolean
|
||||
moderationService?: (text: string) => ReturnType<ModerationService>
|
||||
visionConfig: VisionSettings
|
||||
completionFiles: VisionFile[]
|
||||
}
|
||||
|
||||
const Result: FC<IResultProps> = ({
|
||||
@ -51,6 +54,8 @@ const Result: FC<IResultProps> = ({
|
||||
handleSaveMessage,
|
||||
taskId,
|
||||
onCompleted,
|
||||
visionConfig,
|
||||
completionFiles,
|
||||
}) => {
|
||||
const [isResponsing, { setTrue: setResponsingTrue, setFalse: setResponsingFalse }] = useBoolean(false)
|
||||
useEffect(() => {
|
||||
@ -108,6 +113,11 @@ const Result: FC<IResultProps> = ({
|
||||
logError(t('appDebug.errorMessage.valueOfVarRequired', { key: hasEmptyInput }))
|
||||
return false
|
||||
}
|
||||
|
||||
if (completionFiles.find(item => item.transfer_method === TransferMethod.local_file && !item.upload_file_id)) {
|
||||
notify({ type: 'info', message: t('appDebug.errorMessage.waitForImgUpload') })
|
||||
return false
|
||||
}
|
||||
return !hasEmptyInput
|
||||
}
|
||||
|
||||
@ -120,9 +130,20 @@ const Result: FC<IResultProps> = ({
|
||||
if (!checkCanSend())
|
||||
return
|
||||
|
||||
const data = {
|
||||
const data: Record<string, any> = {
|
||||
inputs,
|
||||
}
|
||||
if (visionConfig.enabled && completionFiles && completionFiles?.length > 0) {
|
||||
data.files = completionFiles.map((item) => {
|
||||
if (item.transfer_method === TransferMethod.local_file) {
|
||||
return {
|
||||
...item,
|
||||
url: '',
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
setMessageId(null)
|
||||
setFeedback({
|
||||
@ -145,7 +166,6 @@ const Result: FC<IResultProps> = ({
|
||||
setResponsingFalse()
|
||||
onCompleted(getCompletionRes(), taskId, false)
|
||||
isTimeout = true
|
||||
console.log(`[#${taskId}]: timeout`)
|
||||
}
|
||||
}, 1000)
|
||||
sendCompletionMessage(data, {
|
||||
|
@ -9,6 +9,8 @@ import type { SiteInfo } from '@/models/share'
|
||||
import type { PromptConfig } from '@/models/debug'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { DEFAULT_VALUE_MAX_LEN } from '@/config'
|
||||
import TextGenerationImageUploader from '@/app/components/base/image-uploader/text-generation-image-uploader'
|
||||
import type { VisionFile, VisionSettings } from '@/types/app'
|
||||
|
||||
export type IRunOnceProps = {
|
||||
siteInfo: SiteInfo
|
||||
@ -16,12 +18,16 @@ export type IRunOnceProps = {
|
||||
inputs: Record<string, any>
|
||||
onInputsChange: (inputs: Record<string, any>) => void
|
||||
onSend: () => void
|
||||
visionConfig: VisionSettings
|
||||
onVisionFilesChange: (files: VisionFile[]) => void
|
||||
}
|
||||
const RunOnce: FC<IRunOnceProps> = ({
|
||||
promptConfig,
|
||||
inputs,
|
||||
onInputsChange,
|
||||
onSend,
|
||||
visionConfig,
|
||||
onVisionFilesChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@ -73,6 +79,24 @@ const RunOnce: FC<IRunOnceProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{
|
||||
visionConfig?.enabled && (
|
||||
<div className="w-full mt-4">
|
||||
<div className="text-gray-900 text-sm font-medium">Image Upload</div>
|
||||
<div className='mt-2'>
|
||||
<TextGenerationImageUploader
|
||||
settings={visionConfig}
|
||||
onFilesChange={files => onVisionFilesChange(files.filter(file => file.progress !== -1).map(fileItem => ({
|
||||
type: 'image',
|
||||
transfer_method: fileItem.type,
|
||||
url: fileItem.url,
|
||||
upload_file_id: fileItem.fileId,
|
||||
})))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{promptConfig.prompt_variables.length > 0 && (
|
||||
<div className='mt-4 h-[1px] bg-gray-100'></div>
|
||||
)}
|
||||
|
@ -19,7 +19,8 @@ import type {
|
||||
} from '@/models/debug'
|
||||
import type { ExternalDataTool } from '@/models/common'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import { ModelModeType } from '@/types/app'
|
||||
import type { VisionSettings } from '@/types/app'
|
||||
import { ModelModeType, Resolution, TransferMethod } from '@/types/app'
|
||||
import { DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config'
|
||||
|
||||
type IDebugConfiguration = {
|
||||
@ -80,6 +81,9 @@ type IDebugConfiguration = {
|
||||
datasetConfigs: DatasetConfigs
|
||||
setDatasetConfigs: (config: DatasetConfigs) => void
|
||||
hasSetContextVar: boolean
|
||||
isShowVisionConfig: boolean
|
||||
visionConfig: VisionSettings
|
||||
setVisionConfig: (visionConfig: VisionSettings) => void
|
||||
}
|
||||
|
||||
const DebugConfigurationContext = createContext<IDebugConfiguration>({
|
||||
@ -184,6 +188,14 @@ const DebugConfigurationContext = createContext<IDebugConfiguration>({
|
||||
},
|
||||
setDatasetConfigs: () => {},
|
||||
hasSetContextVar: false,
|
||||
isShowVisionConfig: false,
|
||||
visionConfig: {
|
||||
enabled: false,
|
||||
number_limits: 2,
|
||||
detail: Resolution.low,
|
||||
transfer_methods: [TransferMethod.remote_url],
|
||||
},
|
||||
setVisionConfig: () => {},
|
||||
})
|
||||
|
||||
export default DebugConfigurationContext
|
||||
|
@ -202,6 +202,7 @@ const translation = {
|
||||
waitForBatchResponse:
|
||||
'Please wait for the response to the batch task to complete.',
|
||||
notSelectModel: 'Please choose a model',
|
||||
waitForImgUpload: 'Please wait for the image to upload',
|
||||
},
|
||||
chatSubTitle: 'Pre Prompt',
|
||||
completionSubTitle: 'Prefix Prompt',
|
||||
@ -249,6 +250,25 @@ const translation = {
|
||||
options: 'Options',
|
||||
addOption: 'Add option',
|
||||
},
|
||||
vision: {
|
||||
name: 'Vision',
|
||||
description: 'Enable Vision will allows the model to take in images and answer questions about them. ',
|
||||
settings: 'Settings',
|
||||
visionSettings: {
|
||||
title: 'Vision Settings',
|
||||
resolution: 'Resolution',
|
||||
resolutionTooltip: `low res will allow model receive a low-res 512 x 512 version of the image, and represent the image with a budget of 65 tokens. This allows the API to return faster responses and consume fewer input tokens for use cases that do not require high detail.
|
||||
\n
|
||||
high res will first allows the model to see the low res image and then creates detailed crops of input images as 512px squares based on the input image size. Each of the detailed crops uses twice the token budget for a total of 129 tokens.`,
|
||||
high: 'High',
|
||||
low: 'Low',
|
||||
uploadMethod: 'Upload Method',
|
||||
both: 'Both',
|
||||
localUpload: 'Local Upload',
|
||||
url: 'URL',
|
||||
uploadLimit: 'Upload Limit',
|
||||
},
|
||||
},
|
||||
openingStatement: {
|
||||
title: 'Opening remarks',
|
||||
add: 'Add',
|
||||
|
@ -199,6 +199,7 @@ const translation = {
|
||||
waitForResponse: '请等待上条信息响应完成',
|
||||
waitForBatchResponse: '请等待批量任务完成',
|
||||
notSelectModel: '请选择模型',
|
||||
waitForImgUpload: '请等待图片上传完成',
|
||||
},
|
||||
chatSubTitle: '对话前提示词',
|
||||
completionSubTitle: '前缀提示词',
|
||||
@ -245,6 +246,25 @@ const translation = {
|
||||
options: '选项',
|
||||
addOption: '添加选项',
|
||||
},
|
||||
vision: {
|
||||
name: '视觉',
|
||||
description: '开启视觉功能将允许模型输入图片,并根据图像内容的理解回答用户问题',
|
||||
settings: '设置',
|
||||
visionSettings: {
|
||||
title: '视觉设置',
|
||||
resolution: '分辨率',
|
||||
resolutionTooltip: `低分辨率模式将使模型接收图像的低分辨率版本,尺寸为512 x 512,并使用65 Tokens 来表示图像。这样可以使API更快地返回响应,并在不需要高细节的用例中消耗更少的输入。
|
||||
\n
|
||||
高分辨率模式将首先允许模型查看低分辨率图像,然后根据输入图像的大小创建512像素的详细裁剪图像。每个详细裁剪图像使用两倍的预算总共为129 Tokens。`,
|
||||
high: '高',
|
||||
low: '低',
|
||||
uploadMethod: '上传方式',
|
||||
both: '两者',
|
||||
localUpload: '本地上传',
|
||||
url: 'URL',
|
||||
uploadLimit: '上传数量限制',
|
||||
},
|
||||
},
|
||||
openingStatement: {
|
||||
title: '对话开场白',
|
||||
add: '添加开场白',
|
||||
|
@ -44,6 +44,7 @@ const translation = {
|
||||
annotationPlaceholder: 'Enter the expected answer that you want AI to reply, which can be used for model fine-tuning and continuous improvement of text generation quality in the future.',
|
||||
},
|
||||
variables: 'Variables',
|
||||
uploadImages: 'Uploaded Images',
|
||||
},
|
||||
filter: {
|
||||
period: {
|
||||
|
@ -44,6 +44,7 @@ const translation = {
|
||||
annotationPlaceholder: '输入你希望 AI 回复的预期答案,这在今后可用于模型微调,持续改进文本生成质量。',
|
||||
},
|
||||
variables: '变量',
|
||||
uploadImages: '上传的图片',
|
||||
},
|
||||
filter: {
|
||||
period: {
|
||||
|
@ -328,9 +328,6 @@ const translation = {
|
||||
lengthError: 'API-key length cannot be less than 5 characters',
|
||||
},
|
||||
},
|
||||
confirm: {
|
||||
desc: 'Deleting the WebHook might cause the extension points configured for this API Extension to fail and produce errors. Please proceed with caution.',
|
||||
},
|
||||
},
|
||||
about: {
|
||||
changeLog: 'Changlog',
|
||||
@ -433,6 +430,15 @@ const translation = {
|
||||
},
|
||||
existed: 'Already exists in the prompt',
|
||||
},
|
||||
imageUploader: {
|
||||
uploadFromComputer: 'Upload from Computer',
|
||||
uploadFromComputerReadError: 'Image reading failed, please try again.',
|
||||
uploadFromComputerUploadError: 'Image upload failed, please upload again.',
|
||||
uploadFromComputerLimit: 'Upload images cannot exceed {{size}} MB',
|
||||
pasteImageLink: 'Paste image link',
|
||||
pasteImageLinkInputPlaceholder: 'Paste image link here',
|
||||
pasteImageLinkInvalid: 'Invalid image link',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
@ -109,7 +109,7 @@ const translation = {
|
||||
provider: '模型供应商',
|
||||
dataSource: '数据来源',
|
||||
plugin: '插件',
|
||||
apiBasedExtension: 'API 的扩展',
|
||||
apiBasedExtension: 'API 扩展',
|
||||
},
|
||||
account: {
|
||||
avatar: '头像',
|
||||
@ -302,18 +302,18 @@ const translation = {
|
||||
},
|
||||
},
|
||||
apiBasedExtension: {
|
||||
title: 'API 的扩展提供了一个集中式的 API 管理,在此统一添加 API 配置后,方便在 Dify 上的各类应用中直接使用。',
|
||||
link: '了解如何开发您自己的 API 的扩展。',
|
||||
title: 'API 扩展提供了一个集中式的 API 管理,在此统一添加 API 配置后,方便在 Dify 上的各类应用中直接使用。',
|
||||
link: '了解如何开发您自己的 API 扩展。',
|
||||
linkUrl: 'https://docs.dify.ai/v/zh-hans/advanced/api_based_extension',
|
||||
add: '新增 API 的扩展',
|
||||
add: '新增 API 扩展',
|
||||
selector: {
|
||||
title: 'API 的扩展',
|
||||
placeholder: '请选择 API 的扩展',
|
||||
manage: '管理 API 的扩展',
|
||||
title: 'API 扩展',
|
||||
placeholder: '请选择 API 扩展',
|
||||
manage: '管理 API 扩展',
|
||||
},
|
||||
modal: {
|
||||
title: '新增 API 的扩展',
|
||||
editTitle: '编辑 API 的扩展',
|
||||
title: '新增 API 扩展',
|
||||
editTitle: '编辑 API 扩展',
|
||||
name: {
|
||||
title: '名称',
|
||||
placeholder: '请输入名称',
|
||||
@ -328,9 +328,6 @@ const translation = {
|
||||
lengthError: 'API-key 不能少于 5 位',
|
||||
},
|
||||
},
|
||||
confirm: {
|
||||
desc: '删除 WebHook 可能会导致这个 API 的扩展配置的扩展失败并产生错误。请谨慎删除。',
|
||||
},
|
||||
},
|
||||
about: {
|
||||
changeLog: '更新日志',
|
||||
@ -433,6 +430,15 @@ const translation = {
|
||||
},
|
||||
existed: 'Prompt 中已存在',
|
||||
},
|
||||
imageUploader: {
|
||||
uploadFromComputer: '从本地上传',
|
||||
uploadFromComputerReadError: '图片读取失败,请重新选择。',
|
||||
uploadFromComputerUploadError: '图片上传失败,请重新上传。',
|
||||
uploadFromComputerLimit: '上传图片不能超过 {{size}} MB',
|
||||
pasteImageLink: '粘贴图片链接',
|
||||
pasteImageLinkInputPlaceholder: '将图像链接粘贴到此处',
|
||||
pasteImageLinkInvalid: '图片链接无效',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
@ -176,6 +176,7 @@ export type PluginProvider = {
|
||||
export type FileUploadConfigResponse = {
|
||||
file_size_limit: number
|
||||
batch_count_limit: number
|
||||
image_file_size_limit?: number | string
|
||||
}
|
||||
|
||||
export type DocumentsLimitResponse = {
|
||||
|
@ -1,3 +1,5 @@
|
||||
import type { VisionFile } from '@/types/app'
|
||||
|
||||
// Log type contains key:string conversation_id:string created_at:string quesiton:string answer:string
|
||||
export type Conversation = {
|
||||
id: string
|
||||
@ -78,6 +80,7 @@ export type MessageContent = {
|
||||
from_source?: 'admin' | 'user'
|
||||
from_end_user_id?: string
|
||||
}>
|
||||
message_files: VisionFile[]
|
||||
}
|
||||
|
||||
export type CompletionConversationGeneralDetail = {
|
||||
|
@ -50,6 +50,7 @@
|
||||
"next": "13.3.1",
|
||||
"qrcode.react": "^3.1.0",
|
||||
"qs": "^6.11.1",
|
||||
"rc-textarea": "^1.5.2",
|
||||
"react": "^18.2.0",
|
||||
"react-18-input-autosize": "^3.0.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
@ -297,12 +297,30 @@ const baseFetch = <T>(
|
||||
]) as Promise<T>
|
||||
}
|
||||
|
||||
export const upload = (options: any): Promise<any> => {
|
||||
export const upload = (options: any, isPublicAPI?: boolean): Promise<any> => {
|
||||
const urlPrefix = isPublicAPI ? PUBLIC_API_PREFIX : API_PREFIX
|
||||
let token = ''
|
||||
if (isPublicAPI) {
|
||||
const sharedToken = globalThis.location.pathname.split('/').slice(-1)[0]
|
||||
const accessToken = localStorage.getItem('token') || JSON.stringify({ [sharedToken]: '' })
|
||||
let accessTokenJson = { [sharedToken]: '' }
|
||||
try {
|
||||
accessTokenJson = JSON.parse(accessToken)
|
||||
}
|
||||
catch (e) {
|
||||
|
||||
}
|
||||
token = accessTokenJson[sharedToken]
|
||||
}
|
||||
else {
|
||||
const accessToken = localStorage.getItem('console_token') || ''
|
||||
token = accessToken
|
||||
}
|
||||
const defaultOptions = {
|
||||
method: 'POST',
|
||||
url: `${API_PREFIX}/files/upload`,
|
||||
url: `${urlPrefix}/files/upload`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('console_token') || ''}`,
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
data: {},
|
||||
}
|
||||
|
@ -80,6 +80,10 @@ export const renameConversation = async (isInstalledApp: boolean, installedAppId
|
||||
return getAction('post', isInstalledApp)(getUrl(`conversations/${id}/name`, isInstalledApp, installedAppId), { body: { name } })
|
||||
}
|
||||
|
||||
export const generationConversationName = async (isInstalledApp: boolean, installedAppId = '', id: string) => {
|
||||
return getAction('post', isInstalledApp)(getUrl(`conversations/${id}/name`, isInstalledApp, installedAppId), { body: { auto_generate: true } })
|
||||
}
|
||||
|
||||
export const fetchChatList = async (conversationId: string, isInstalledApp: boolean, installedAppId = '') => {
|
||||
return getAction('get', isInstalledApp)(getUrl('messages', isInstalledApp, installedAppId), { params: { conversation_id: conversationId, limit: 20, last_id: '' } })
|
||||
}
|
||||
|
@ -50,6 +50,10 @@ export const renameConversation = async (id: string, name: string) => {
|
||||
return post(getUrl(`conversations/${id}/name`), { body: { name } })
|
||||
}
|
||||
|
||||
export const generationConversationName = async (id: string) => {
|
||||
return post(getUrl(`conversations/${id}/name`), { body: { auto_generate: true } })
|
||||
}
|
||||
|
||||
export const fetchChatList = async (conversationId: string) => {
|
||||
return get(getUrl('messages'), { params: { conversation_id: conversationId, limit: 20, last_id: '' } })
|
||||
}
|
||||
|
@ -176,6 +176,10 @@ export type ModelConfig = {
|
||||
}
|
||||
}
|
||||
dataset_configs: DatasetConfigs
|
||||
file_upload?: {
|
||||
image: VisionSettings
|
||||
}
|
||||
files?: VisionFile[]
|
||||
}
|
||||
|
||||
export const LanguagesSupported = ['zh-Hans', 'en-US'] as const
|
||||
@ -269,3 +273,40 @@ export type AppTemplate = {
|
||||
/** Model */
|
||||
model_config: ModelConfig
|
||||
}
|
||||
|
||||
export enum Resolution {
|
||||
low = 'low',
|
||||
high = 'high',
|
||||
}
|
||||
|
||||
export enum TransferMethod {
|
||||
all = 'all',
|
||||
local_file = 'local_file',
|
||||
remote_url = 'remote_url',
|
||||
}
|
||||
|
||||
export type VisionSettings = {
|
||||
enabled: boolean
|
||||
number_limits: number
|
||||
detail: Resolution
|
||||
transfer_methods: TransferMethod[]
|
||||
image_file_size_limit?: number | string
|
||||
}
|
||||
|
||||
export type ImageFile = {
|
||||
type: TransferMethod
|
||||
_id: string
|
||||
fileId: string
|
||||
file?: File
|
||||
progress: number
|
||||
url: string
|
||||
base64Url?: string
|
||||
}
|
||||
|
||||
export type VisionFile = {
|
||||
id?: string
|
||||
type: string
|
||||
transfer_method: TransferMethod
|
||||
url: string
|
||||
upload_file_id: string
|
||||
}
|
||||
|
867
web/yarn.lock
867
web/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user