mirror of
https://github.com/langgenius/dify.git
synced 2025-12-26 09:07:28 +00:00
Compare commits
104 Commits
deploy/tri
...
feat/suppo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fb339ca4f | ||
|
|
c1871e67aa | ||
|
|
f711f9a317 | ||
|
|
9ff3310cb6 | ||
|
|
b6bdcc7052 | ||
|
|
67b0771081 | ||
|
|
9a07488da9 | ||
|
|
ef043c6906 | ||
|
|
ab814e3eac | ||
|
|
a0e1eeb3f1 | ||
|
|
b1ebeb67a7 | ||
|
|
082179f70f | ||
|
|
8786ebdbca | ||
|
|
b49a4eab62 | ||
|
|
0a7b59f500 | ||
|
|
c264d9152f | ||
|
|
3bf9d898c0 | ||
|
|
a7f2849e74 | ||
|
|
0957ece92f | ||
|
|
949bf38d3c | ||
|
|
7bafb7f959 | ||
|
|
9735f55ca4 | ||
|
|
4c1f9b949b | ||
|
|
0af0c94dde | ||
|
|
8e4f0640cc | ||
|
|
1f513e3b43 | ||
|
|
aa0841e2a8 | ||
|
|
b6a1562357 | ||
|
|
bee0797401 | ||
|
|
e085f39c13 | ||
|
|
344844d3e0 | ||
|
|
6e9f82491d | ||
|
|
372b1c3db8 | ||
|
|
58d305dbed | ||
|
|
0360a0416b | ||
|
|
72282b6e8f | ||
|
|
8391884c4e | ||
|
|
b018f2b0a0 | ||
|
|
ab56b4a818 | ||
|
|
61ebc756aa | ||
|
|
4bea38042a | ||
|
|
337abc536b | ||
|
|
cc02b78aca | ||
|
|
18f2d24f8e | ||
|
|
0c7b9a462f | ||
|
|
4dd5580854 | ||
|
|
440bd825d8 | ||
|
|
d2379c38bd | ||
|
|
cbc55c577b | ||
|
|
8e962d15d1 | ||
|
|
b07c766551 | ||
|
|
9e3dd69277 | ||
|
|
db9e5665c2 | ||
|
|
cad77ce0bf | ||
|
|
6f4518ebf7 | ||
|
|
a8f5748dee | ||
|
|
738d3001be | ||
|
|
df4e32aaa0 | ||
|
|
a25e37a96d | ||
|
|
f156b46705 | ||
|
|
3b64e118d0 | ||
|
|
566cd20849 | ||
|
|
df76527f29 | ||
|
|
53a80a5dbe | ||
|
|
1507792a0c | ||
|
|
00b9bbff75 | ||
|
|
e1f8b4b387 | ||
|
|
1539d86f7d | ||
|
|
67bb14d3ee | ||
|
|
5653309080 | ||
|
|
0f52b34b61 | ||
|
|
75e35857c1 | ||
|
|
4f81be70e3 | ||
|
|
1d4d627d05 | ||
|
|
2357234f39 | ||
|
|
a3f7d8f996 | ||
|
|
56f12e70c1 | ||
|
|
b14afda160 | ||
|
|
44b4948972 | ||
|
|
487eac3b91 | ||
|
|
84b2913cd9 | ||
|
|
176d810c8d | ||
|
|
9e66564526 | ||
|
|
781a9a56cd | ||
|
|
93be1219eb | ||
|
|
3276d6429d | ||
|
|
50072a63ae | ||
|
|
1ab7e1cba8 | ||
|
|
b0aef35c63 | ||
|
|
ac351b700c | ||
|
|
d1e5d30ea9 | ||
|
|
c73e84d992 | ||
|
|
5f0bd5119a | ||
|
|
8353352bda | ||
|
|
73845cbec5 | ||
|
|
c2f94e9e8a | ||
|
|
e54efda36f | ||
|
|
d4bd19f6d8 | ||
|
|
4decbbbf18 | ||
|
|
b15867f92e | ||
|
|
a5e5fbc6e0 | ||
|
|
1b1471b6d8 | ||
|
|
5280bffde2 | ||
|
|
db0fc94b39 |
@@ -258,9 +258,9 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
|
||||
</div>
|
||||
)}
|
||||
{hasVar && (
|
||||
<div className='mt-1 px-3 pb-3'>
|
||||
<div className={cn('mt-1 grid px-3 pb-3')}>
|
||||
<ReactSortable
|
||||
className='space-y-1'
|
||||
className={cn('grid-col-1 grid space-y-1', readonly && 'grid-cols-2 gap-1 space-y-0')}
|
||||
list={promptVariablesWithIds}
|
||||
setList={(list) => { onPromptVariablesChange?.(list.map(item => item.variable)) }}
|
||||
handle='.handle'
|
||||
|
||||
@@ -38,7 +38,7 @@ const VarItem: FC<ItemProps> = ({
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
|
||||
return (
|
||||
<div className={cn('group relative mb-1 flex h-[34px] w-full items-center rounded-lg border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg pl-2.5 pr-3 shadow-xs last-of-type:mb-0 hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-sm', isDeleting && 'border-state-destructive-border hover:bg-state-destructive-hover', readonly && 'cursor-not-allowed opacity-30', className)}>
|
||||
<div className={cn('group relative mb-1 flex h-[34px] w-full items-center rounded-lg border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg pl-2.5 pr-3 shadow-xs last-of-type:mb-0 hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-sm', isDeleting && 'border-state-destructive-border hover:bg-state-destructive-hover', readonly && 'cursor-not-allowed', className)}>
|
||||
<VarIcon className={cn('mr-1 h-4 w-4 shrink-0 text-text-accent', canDrag && 'group-hover:opacity-0')} />
|
||||
{canDrag && (
|
||||
<RiDraggable className='absolute left-3 top-3 hidden h-3 w-3 cursor-pointer text-text-tertiary group-hover:block' />
|
||||
|
||||
@@ -13,10 +13,14 @@ import ConfigContext from '@/context/debug-configuration'
|
||||
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||
import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card'
|
||||
import { Resolution } from '@/types/app'
|
||||
import { noop } from 'lodash-es'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const ConfigVision: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { isShowVisionConfig, isAllowVideoUpload } = useContext(ConfigContext)
|
||||
const { isShowVisionConfig, isAllowVideoUpload, readonly } = useContext(ConfigContext)
|
||||
const file = useFeatures(s => s.features.file)
|
||||
const featuresStore = useFeaturesStore()
|
||||
|
||||
@@ -53,7 +57,7 @@ const ConfigVision: FC = () => {
|
||||
setFeatures(newFeatures)
|
||||
}, [featuresStore, isAllowVideoUpload])
|
||||
|
||||
if (!isShowVisionConfig)
|
||||
if (!isShowVisionConfig || (readonly && !isImageEnabled))
|
||||
return null
|
||||
|
||||
return (
|
||||
@@ -74,37 +78,49 @@ const ConfigVision: FC = () => {
|
||||
/>
|
||||
</div>
|
||||
<div className='flex shrink-0 items-center'>
|
||||
{/* <div className='mr-2 flex items-center gap-0.5'>
|
||||
<div className='text-text-tertiary system-xs-medium-uppercase'>{t('appDebug.vision.visionSettings.resolution')}</div>
|
||||
<Tooltip
|
||||
popupContent={
|
||||
<div className='w-[180px]' >
|
||||
{t('appDebug.vision.visionSettings.resolutionTooltip').split('\n').map(item => (
|
||||
<div key={item}>{item}</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
{readonly ? (<>
|
||||
<div className='mr-2 flex items-center gap-0.5'>
|
||||
<div className='system-xs-medium-uppercase text-text-tertiary'>{t('appDebug.vision.visionSettings.resolution')}</div>
|
||||
<Tooltip
|
||||
popupContent={
|
||||
<div className='w-[180px]' >
|
||||
{t('appDebug.vision.visionSettings.resolutionTooltip').split('\n').map(item => (
|
||||
<div key={item}>{item}</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex items-center gap-1'>
|
||||
<OptionCard
|
||||
title={t('appDebug.vision.visionSettings.high')}
|
||||
selected={file?.image?.detail === Resolution.high}
|
||||
onSelect={noop}
|
||||
className={cn(
|
||||
'cursor-not-allowed rounded-lg px-3 hover:shadow-none',
|
||||
file?.image?.detail !== Resolution.high && 'hover:border-components-option-card-option-border',
|
||||
)}
|
||||
/>
|
||||
<OptionCard
|
||||
title={t('appDebug.vision.visionSettings.low')}
|
||||
selected={file?.image?.detail === Resolution.low}
|
||||
onSelect={noop}
|
||||
className={cn(
|
||||
'cursor-not-allowed rounded-lg px-3 hover:shadow-none',
|
||||
file?.image?.detail !== Resolution.low && 'hover:border-components-option-card-option-border',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>) : <>
|
||||
<ParamConfig />
|
||||
<div className='ml-1 mr-3 h-3.5 w-[1px] bg-divider-regular'></div>
|
||||
<Switch
|
||||
defaultValue={isImageEnabled}
|
||||
onChange={handleChange}
|
||||
size='md'
|
||||
/>
|
||||
</div> */}
|
||||
{/* <div className='flex items-center gap-1'>
|
||||
<OptionCard
|
||||
title={t('appDebug.vision.visionSettings.high')}
|
||||
selected={file?.image?.detail === Resolution.high}
|
||||
onSelect={() => handleChange(Resolution.high)}
|
||||
/>
|
||||
<OptionCard
|
||||
title={t('appDebug.vision.visionSettings.low')}
|
||||
selected={file?.image?.detail === Resolution.low}
|
||||
onSelect={() => handleChange(Resolution.low)}
|
||||
/>
|
||||
</div> */}
|
||||
<ParamConfig />
|
||||
<div className='ml-1 mr-3 h-3.5 w-[1px] bg-divider-regular'></div>
|
||||
<Switch
|
||||
defaultValue={isImageEnabled}
|
||||
onChange={handleChange}
|
||||
size='md'
|
||||
/>
|
||||
</>}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -37,7 +37,7 @@ type AgentToolWithMoreInfo = AgentTool & { icon: any; collection?: Collection }
|
||||
const AgentTools: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const [isShowChooseTool, setIsShowChooseTool] = useState(false)
|
||||
const { modelConfig, setModelConfig } = useContext(ConfigContext)
|
||||
const { readonly, modelConfig, setModelConfig } = useContext(ConfigContext)
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
@@ -119,7 +119,7 @@ const AgentTools: FC = () => {
|
||||
}
|
||||
const getProviderShowName = (item: AgentTool) => {
|
||||
const type = item.provider_type
|
||||
if(type === CollectionType.builtIn)
|
||||
if (type === CollectionType.builtIn)
|
||||
return item.provider_name.split('/').pop()
|
||||
return item.provider_name
|
||||
}
|
||||
@@ -158,7 +158,7 @@ const AgentTools: FC = () => {
|
||||
headerRight={
|
||||
<div className='flex items-center'>
|
||||
<div className='text-xs font-normal leading-[18px] text-text-tertiary'>{tools.filter(item => !!item.enabled).length}/{tools.length} {t('appDebug.agent.tools.enabled')}</div>
|
||||
{tools.length < MAX_TOOLS_NUM && (
|
||||
{tools.length < MAX_TOOLS_NUM && !readonly && (
|
||||
<>
|
||||
<div className='ml-3 mr-1 h-3.5 w-px bg-divider-regular'></div>
|
||||
<ToolPicker
|
||||
@@ -177,7 +177,7 @@ const AgentTools: FC = () => {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className='grid grid-cols-1 flex-wrap items-center justify-between gap-1 2xl:grid-cols-2'>
|
||||
<div className={cn('grid grid-cols-1 flex-wrap items-center justify-between gap-1 2xl:grid-cols-2', readonly && 'grid-cols-2')}>
|
||||
{tools.map((item: AgentTool & { icon: any; collection?: Collection }, index) => (
|
||||
<div key={index}
|
||||
className={cn(
|
||||
@@ -201,7 +201,7 @@ const AgentTools: FC = () => {
|
||||
>
|
||||
<span className='system-xs-medium pr-1.5 text-text-secondary'>{getProviderShowName(item)}</span>
|
||||
<span className='text-text-tertiary'>{item.tool_label}</span>
|
||||
{!item.isDeleted && (
|
||||
{!item.isDeleted && !readonly && (
|
||||
<Tooltip
|
||||
popupContent={
|
||||
<div className='w-[180px]'>
|
||||
@@ -212,7 +212,7 @@ const AgentTools: FC = () => {
|
||||
}
|
||||
>
|
||||
<div className='h-4 w-4'>
|
||||
<div className='ml-0.5 hidden group-hover:inline-block'>
|
||||
<div className={cn('ml-0.5 hidden group-hover:inline-block')}>
|
||||
<RiInformation2Line className='h-4 w-4 text-text-tertiary' />
|
||||
</div>
|
||||
</div>
|
||||
@@ -246,8 +246,8 @@ const AgentTools: FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!item.isDeleted && (
|
||||
<div className='mr-2 hidden items-center gap-1 group-hover:flex'>
|
||||
{!item.isDeleted && !readonly && (
|
||||
<div className={cn('mr-2 hidden items-center gap-1 group-hover:flex')}>
|
||||
{!item.notAuthor && (
|
||||
<Tooltip
|
||||
popupContent={t('tools.setBuiltInTools.infoAndSetting')}
|
||||
@@ -280,7 +280,7 @@ const AgentTools: FC = () => {
|
||||
{!item.notAuthor && (
|
||||
<Switch
|
||||
defaultValue={item.isDeleted ? false : item.enabled}
|
||||
disabled={item.isDeleted}
|
||||
disabled={item.isDeleted || readonly}
|
||||
size='md'
|
||||
onChange={(enabled) => {
|
||||
const newModelConfig = produce(modelConfig, (draft) => {
|
||||
@@ -291,10 +291,14 @@ const AgentTools: FC = () => {
|
||||
}} />
|
||||
)}
|
||||
{item.notAuthor && (
|
||||
<Button variant='secondary' size='small' onClick={() => {
|
||||
setCurrentTool(item)
|
||||
setIsShowSettingTool(true)
|
||||
}}>
|
||||
<Button
|
||||
variant='secondary'
|
||||
disabled={readonly}
|
||||
size='small' onClick={() => {
|
||||
setCurrentTool(item)
|
||||
setIsShowSettingTool(true)
|
||||
}}
|
||||
>
|
||||
{t('tools.notAuthorized')}
|
||||
<Indicator className='ml-2' color='orange' />
|
||||
</Button>
|
||||
|
||||
@@ -16,7 +16,7 @@ const ConfigAudio: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const file = useFeatures(s => s.features.file)
|
||||
const featuresStore = useFeaturesStore()
|
||||
const { isShowAudioConfig } = useContext(ConfigContext)
|
||||
const { isShowAudioConfig, readonly } = useContext(ConfigContext)
|
||||
|
||||
const isAudioEnabled = file?.allowed_file_types?.includes(SupportUploadFileTypes.audio) ?? false
|
||||
|
||||
@@ -44,7 +44,7 @@ const ConfigAudio: FC = () => {
|
||||
setFeatures(newFeatures)
|
||||
}, [featuresStore])
|
||||
|
||||
if (!isShowAudioConfig)
|
||||
if (!isShowAudioConfig || (readonly && !isAudioEnabled))
|
||||
return null
|
||||
|
||||
return (
|
||||
@@ -64,14 +64,16 @@ const ConfigAudio: FC = () => {
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex shrink-0 items-center'>
|
||||
<div className='ml-1 mr-3 h-3.5 w-[1px] bg-divider-subtle'></div>
|
||||
<Switch
|
||||
defaultValue={isAudioEnabled}
|
||||
onChange={handleChange}
|
||||
size='md'
|
||||
/>
|
||||
</div>
|
||||
{!readonly && (
|
||||
<div className='flex shrink-0 items-center'>
|
||||
<div className='ml-1 mr-3 h-3.5 w-[1px] bg-divider-subtle'></div>
|
||||
<Switch
|
||||
defaultValue={isAudioEnabled}
|
||||
onChange={handleChange}
|
||||
size='md'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ const ConfigDocument: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const file = useFeatures(s => s.features.file)
|
||||
const featuresStore = useFeaturesStore()
|
||||
const { isShowDocumentConfig } = useContext(ConfigContext)
|
||||
const { isShowDocumentConfig, readonly } = useContext(ConfigContext)
|
||||
|
||||
const isDocumentEnabled = file?.allowed_file_types?.includes(SupportUploadFileTypes.document) ?? false
|
||||
|
||||
@@ -44,7 +44,7 @@ const ConfigDocument: FC = () => {
|
||||
setFeatures(newFeatures)
|
||||
}, [featuresStore])
|
||||
|
||||
if (!isShowDocumentConfig)
|
||||
if (!isShowDocumentConfig || (readonly && !isDocumentEnabled))
|
||||
return null
|
||||
|
||||
return (
|
||||
@@ -64,14 +64,16 @@ const ConfigDocument: FC = () => {
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex shrink-0 items-center'>
|
||||
<div className='ml-1 mr-3 h-3.5 w-[1px] bg-divider-subtle'></div>
|
||||
<Switch
|
||||
defaultValue={isDocumentEnabled}
|
||||
onChange={handleChange}
|
||||
size='md'
|
||||
/>
|
||||
</div>
|
||||
{!readonly && (
|
||||
<div className='flex shrink-0 items-center'>
|
||||
<div className='ml-1 mr-3 h-3.5 w-[1px] bg-divider-subtle'></div>
|
||||
<Switch
|
||||
defaultValue={isDocumentEnabled}
|
||||
onChange={handleChange}
|
||||
size='md'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { AppModeEnum, ModelModeType } from '@/types/app'
|
||||
|
||||
const Config: FC = () => {
|
||||
const {
|
||||
readonly,
|
||||
mode,
|
||||
isAdvancedMode,
|
||||
modelModeType,
|
||||
@@ -27,6 +28,7 @@ const Config: FC = () => {
|
||||
modelConfig,
|
||||
setModelConfig,
|
||||
setPrevPromptConfig,
|
||||
dataSets,
|
||||
} = useContext(ConfigContext)
|
||||
const isChatApp = [AppModeEnum.ADVANCED_CHAT, AppModeEnum.AGENT_CHAT, AppModeEnum.CHAT].includes(mode)
|
||||
const formattingChangedDispatcher = useFormattingChangedDispatcher()
|
||||
@@ -65,19 +67,28 @@ const Config: FC = () => {
|
||||
promptTemplate={promptTemplate}
|
||||
promptVariables={promptVariables}
|
||||
onChange={handlePromptChange}
|
||||
readonly={readonly}
|
||||
/>
|
||||
|
||||
{/* Variables */}
|
||||
<ConfigVar
|
||||
promptVariables={promptVariables}
|
||||
onPromptVariablesChange={handlePromptVariablesNameChange}
|
||||
/>
|
||||
{!(readonly && promptVariables.length === 0) && (
|
||||
<ConfigVar
|
||||
promptVariables={promptVariables}
|
||||
onPromptVariablesChange={handlePromptVariablesNameChange}
|
||||
readonly={readonly}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Dataset */}
|
||||
<DatasetConfig />
|
||||
|
||||
{!(readonly && dataSets.length === 0) && (
|
||||
<DatasetConfig
|
||||
readonly={readonly}
|
||||
hideMetadataFilter={readonly}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{/* Tools */}
|
||||
{isAgent && (
|
||||
{isAgent && !(readonly && modelConfig.agentConfig.tools.length === 0) && (
|
||||
<AgentTools />
|
||||
)}
|
||||
|
||||
@@ -88,7 +99,7 @@ const Config: FC = () => {
|
||||
<ConfigAudio />
|
||||
|
||||
{/* Chat History */}
|
||||
{isAdvancedMode && isChatApp && modelModeType === ModelModeType.completion && (
|
||||
{!readonly && isAdvancedMode && isChatApp && modelModeType === ModelModeType.completion && (
|
||||
<HistoryPanel
|
||||
showWarning={!hasSetBlockStatus.history}
|
||||
onShowEditModal={showHistoryModal}
|
||||
|
||||
@@ -29,6 +29,7 @@ const Item: FC<ItemProps> = ({
|
||||
config,
|
||||
onSave,
|
||||
onRemove,
|
||||
readonly = false,
|
||||
editable = true,
|
||||
}) => {
|
||||
const media = useBreakpoints()
|
||||
@@ -55,6 +56,7 @@ const Item: FC<ItemProps> = ({
|
||||
<div className={cn(
|
||||
'group relative mb-1 flex h-10 w-full cursor-pointer items-center justify-between rounded-lg border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg px-2 last-of-type:mb-0 hover:bg-components-panel-on-panel-item-bg-hover',
|
||||
isDeleting && 'border-state-destructive-border hover:bg-state-destructive-hover',
|
||||
readonly && 'cursor-not-allowed',
|
||||
)}>
|
||||
<div className='flex w-0 grow items-center space-x-1.5'>
|
||||
<AppIcon
|
||||
@@ -68,7 +70,7 @@ const Item: FC<ItemProps> = ({
|
||||
</div>
|
||||
<div className='ml-2 hidden shrink-0 items-center space-x-1 group-hover:flex'>
|
||||
{
|
||||
editable && <ActionButton
|
||||
editable && !readonly && <ActionButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setShowSettingsModal(true)
|
||||
@@ -77,14 +79,18 @@ const Item: FC<ItemProps> = ({
|
||||
<RiEditLine className='h-4 w-4 shrink-0 text-text-tertiary' />
|
||||
</ActionButton>
|
||||
}
|
||||
<ActionButton
|
||||
onClick={() => onRemove(config.id)}
|
||||
state={isDeleting ? ActionButtonState.Destructive : ActionButtonState.Default}
|
||||
onMouseEnter={() => setIsDeleting(true)}
|
||||
onMouseLeave={() => setIsDeleting(false)}
|
||||
>
|
||||
<RiDeleteBinLine className={cn('h-4 w-4 shrink-0 text-text-tertiary', isDeleting && 'text-text-destructive')} />
|
||||
</ActionButton>
|
||||
{
|
||||
!readonly && (
|
||||
<ActionButton
|
||||
onClick={() => onRemove(config.id)}
|
||||
state={isDeleting ? ActionButtonState.Destructive : ActionButtonState.Default}
|
||||
onMouseEnter={() => setIsDeleting(true)}
|
||||
onMouseLeave={() => setIsDeleting(false)}
|
||||
>
|
||||
<RiDeleteBinLine className={cn('h-4 w-4 shrink-0 text-text-tertiary', isDeleting && 'text-text-destructive')} />
|
||||
</ActionButton>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
config.indexing_technique && <Badge
|
||||
@@ -98,13 +104,15 @@ const Item: FC<ItemProps> = ({
|
||||
text={t('dataset.externalTag') as string}
|
||||
/>
|
||||
}
|
||||
<Drawer isOpen={showSettingsModal} onClose={() => setShowSettingsModal(false)} footer={null} mask={isMobile} panelClassName='mt-16 mx-2 sm:mr-2 mb-3 !p-0 !max-w-[640px] rounded-xl'>
|
||||
<SettingsModal
|
||||
currentDataset={config}
|
||||
onCancel={() => setShowSettingsModal(false)}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</Drawer>
|
||||
{showSettingsModal && (
|
||||
<Drawer isOpen={showSettingsModal} onClose={() => setShowSettingsModal(false)} footer={null} mask={isMobile} panelClassName='mt-16 mx-2 sm:mr-2 mb-3 !p-0 !max-w-[640px] rounded-xl'>
|
||||
<SettingsModal
|
||||
currentDataset={config}
|
||||
onCancel={() => setShowSettingsModal(false)}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</Drawer>
|
||||
)}
|
||||
</div >
|
||||
)
|
||||
}
|
||||
|
||||
@@ -36,8 +36,13 @@ import {
|
||||
LogicalOperator,
|
||||
MetadataFilteringVariableType,
|
||||
} from '@/app/components/workflow/nodes/knowledge-retrieval/types'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const DatasetConfig: FC = () => {
|
||||
type Props = {
|
||||
readonly?: boolean
|
||||
hideMetadataFilter?: boolean
|
||||
}
|
||||
const DatasetConfig: FC<Props> = ({ readonly, hideMetadataFilter }) => {
|
||||
const { t } = useTranslation()
|
||||
const userProfile = useAppContextSelector(s => s.userProfile)
|
||||
const {
|
||||
@@ -254,24 +259,25 @@ const DatasetConfig: FC = () => {
|
||||
className='mt-2'
|
||||
title={t('appDebug.feature.dataSet.title')}
|
||||
headerRight={
|
||||
<div className='flex items-center gap-1'>
|
||||
!readonly && (<div className='flex items-center gap-1'>
|
||||
{!isAgent && <ParamsConfig disabled={!hasData} selectedDatasets={dataSet} />}
|
||||
<OperationBtn type="add" onClick={showSelectDataSet} />
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
hasHeaderBottomBorder={!hasData}
|
||||
noBodySpacing
|
||||
>
|
||||
{hasData
|
||||
? (
|
||||
<div className='mt-1 flex flex-wrap justify-between px-3 pb-3'>
|
||||
<div className={cn('mt-1 grid grid-cols-1 px-3 pb-3', readonly && 'grid-cols-2 gap-1')}>
|
||||
{formattedDataset.map(item => (
|
||||
<CardItem
|
||||
key={item.id}
|
||||
config={item}
|
||||
onRemove={onRemove}
|
||||
onSave={handleSave}
|
||||
editable={item.editable}
|
||||
editable={item.editable && !readonly}
|
||||
readonly={readonly}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -282,27 +288,29 @@ const DatasetConfig: FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='border-t border-t-divider-subtle py-2'>
|
||||
<MetadataFilter
|
||||
metadataList={metadataList}
|
||||
selectedDatasetsLoaded
|
||||
metadataFilterMode={datasetConfigs.metadata_filtering_mode}
|
||||
metadataFilteringConditions={datasetConfigs.metadata_filtering_conditions}
|
||||
handleAddCondition={handleAddCondition}
|
||||
handleMetadataFilterModeChange={handleMetadataFilterModeChange}
|
||||
handleRemoveCondition={handleRemoveCondition}
|
||||
handleToggleConditionLogicalOperator={handleToggleConditionLogicalOperator}
|
||||
handleUpdateCondition={handleUpdateCondition}
|
||||
metadataModelConfig={datasetConfigs.metadata_model_config}
|
||||
handleMetadataModelChange={handleMetadataModelChange}
|
||||
handleMetadataCompletionParamsChange={handleMetadataCompletionParamsChange}
|
||||
isCommonVariable
|
||||
availableCommonStringVars={promptVariablesToSelect.filter(item => item.type === MetadataFilteringVariableType.string || item.type === MetadataFilteringVariableType.select)}
|
||||
availableCommonNumberVars={promptVariablesToSelect.filter(item => item.type === MetadataFilteringVariableType.number)}
|
||||
/>
|
||||
</div>
|
||||
{!hideMetadataFilter && (
|
||||
<div className='border-t border-t-divider-subtle py-2'>
|
||||
<MetadataFilter
|
||||
metadataList={metadataList}
|
||||
selectedDatasetsLoaded
|
||||
metadataFilterMode={datasetConfigs.metadata_filtering_mode}
|
||||
metadataFilteringConditions={datasetConfigs.metadata_filtering_conditions}
|
||||
handleAddCondition={handleAddCondition}
|
||||
handleMetadataFilterModeChange={handleMetadataFilterModeChange}
|
||||
handleRemoveCondition={handleRemoveCondition}
|
||||
handleToggleConditionLogicalOperator={handleToggleConditionLogicalOperator}
|
||||
handleUpdateCondition={handleUpdateCondition}
|
||||
metadataModelConfig={datasetConfigs.metadata_model_config}
|
||||
handleMetadataModelChange={handleMetadataModelChange}
|
||||
handleMetadataCompletionParamsChange={handleMetadataCompletionParamsChange}
|
||||
isCommonVariable
|
||||
availableCommonStringVars={promptVariablesToSelect.filter(item => item.type === MetadataFilteringVariableType.string || item.type === MetadataFilteringVariableType.select)}
|
||||
availableCommonNumberVars={promptVariablesToSelect.filter(item => item.type === MetadataFilteringVariableType.number)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode === AppModeEnum.COMPLETION && dataSet.length > 0 && (
|
||||
{!readonly && mode === AppModeEnum.COMPLETION && dataSet.length > 0 && (
|
||||
<ContextVar
|
||||
value={selectedContextVar?.key}
|
||||
options={promptVariablesToSelect}
|
||||
|
||||
@@ -18,7 +18,7 @@ const ChatUserInput = ({
|
||||
inputs,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { modelConfig, setInputs } = useContext(ConfigContext)
|
||||
const { modelConfig, setInputs, readonly } = useContext(ConfigContext)
|
||||
|
||||
const promptVariables = modelConfig.configs.prompt_variables.filter(({ key, name }) => {
|
||||
return key && key?.trim() && name && name?.trim()
|
||||
@@ -70,6 +70,7 @@ const ChatUserInput = ({
|
||||
placeholder={name}
|
||||
autoFocus={index === 0}
|
||||
maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
)}
|
||||
{type === 'paragraph' && (
|
||||
@@ -78,6 +79,7 @@ const ChatUserInput = ({
|
||||
placeholder={name}
|
||||
value={inputs[key] ? `${inputs[key]}` : ''}
|
||||
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
)}
|
||||
{type === 'select' && (
|
||||
@@ -87,6 +89,7 @@ const ChatUserInput = ({
|
||||
onSelect={(i) => { handleInputValueChange(key, i.value as string) }}
|
||||
items={(options || []).map(i => ({ name: i, value: i }))}
|
||||
allowSearch={false}
|
||||
disabled={readonly}
|
||||
/>
|
||||
)}
|
||||
{type === 'number' && (
|
||||
@@ -97,6 +100,7 @@ const ChatUserInput = ({
|
||||
placeholder={name}
|
||||
autoFocus={index === 0}
|
||||
maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
)}
|
||||
{type === 'checkbox' && (
|
||||
@@ -105,6 +109,7 @@ const ChatUserInput = ({
|
||||
value={!!inputs[key]}
|
||||
required={required}
|
||||
onChange={(value) => { handleInputValueChange(key, value) }}
|
||||
readonly={readonly}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useProviderContext } from '@/context/provider-context'
|
||||
import { useFeatures } from '@/app/components/base/features/hooks'
|
||||
import { cloneDeep, noop } from 'lodash-es'
|
||||
import { DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config'
|
||||
import { AppSourceType } from '@/service/share'
|
||||
|
||||
type TextGenerationItemProps = {
|
||||
modelAndParameter: ModelAndParameter
|
||||
@@ -129,11 +130,11 @@ const TextGenerationItem: FC<TextGenerationItemProps> = ({
|
||||
|
||||
return (
|
||||
<TextGeneration
|
||||
appSourceType={AppSourceType.webApp}
|
||||
className='flex h-full flex-col overflow-y-auto border-none'
|
||||
content={completion}
|
||||
isLoading={!completion && isResponding}
|
||||
isResponding={isResponding}
|
||||
isInstalledApp={false}
|
||||
siteInfo={null}
|
||||
messageId={messageId}
|
||||
isError={false}
|
||||
|
||||
@@ -39,6 +39,7 @@ const DebugWithSingleModel = (
|
||||
) => {
|
||||
const { userProfile } = useAppContext()
|
||||
const {
|
||||
readonly,
|
||||
modelConfig,
|
||||
appId,
|
||||
inputs,
|
||||
@@ -154,6 +155,7 @@ const DebugWithSingleModel = (
|
||||
|
||||
return (
|
||||
<Chat
|
||||
readonly={readonly}
|
||||
config={config}
|
||||
chatList={chatList}
|
||||
isResponding={isResponding}
|
||||
|
||||
@@ -49,6 +49,7 @@ import PromptLogModal from '@/app/components/base/prompt-log-modal'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
import { noop } from 'lodash-es'
|
||||
import { AppSourceType } from '@/service/share'
|
||||
|
||||
type IDebug = {
|
||||
isAPIKeySet: boolean
|
||||
@@ -71,6 +72,7 @@ const Debug: FC<IDebug> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
readonly,
|
||||
appId,
|
||||
mode,
|
||||
modelModeType,
|
||||
@@ -412,19 +414,23 @@ const Debug: FC<IDebug> = ({
|
||||
}
|
||||
{mode !== AppModeEnum.COMPLETION && (
|
||||
<>
|
||||
<TooltipPlus
|
||||
popupContent={t('common.operation.refresh')}
|
||||
>
|
||||
<ActionButton onClick={clearConversation}>
|
||||
<RefreshCcw01 className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
</TooltipPlus>
|
||||
{!readonly && (
|
||||
<TooltipPlus
|
||||
popupContent={t('common.operation.refresh')}
|
||||
>
|
||||
<ActionButton onClick={clearConversation}>
|
||||
<RefreshCcw01 className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
|
||||
</TooltipPlus>
|
||||
)}
|
||||
|
||||
{varList.length > 0 && (
|
||||
<div className='relative ml-1 mr-2'>
|
||||
<TooltipPlus
|
||||
popupContent={t('workflow.panel.userInputField')}
|
||||
>
|
||||
<ActionButton state={expanded ? ActionButtonState.Active : undefined} onClick={() => setExpanded(!expanded)}>
|
||||
<ActionButton state={expanded ? ActionButtonState.Active : undefined} onClick={() => !readonly && setExpanded(!expanded)}>
|
||||
<RiEqualizer2Line className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
</TooltipPlus>
|
||||
@@ -506,12 +512,12 @@ const Debug: FC<IDebug> = ({
|
||||
<div className='mx-4 mt-3'><GroupName name={t('appDebug.result')} /></div>
|
||||
<div className='mx-3 mb-8'>
|
||||
<TextGeneration
|
||||
appSourceType={AppSourceType.webApp}
|
||||
className="mt-2"
|
||||
content={completionRes}
|
||||
isLoading={!completionRes && isResponding}
|
||||
isShowTextToSpeech={textToSpeechConfig.enabled && !!text2speechDefaultModel}
|
||||
isResponding={isResponding}
|
||||
isInstalledApp={false}
|
||||
messageId={messageId}
|
||||
isError={false}
|
||||
onRetry={noop}
|
||||
@@ -552,7 +558,7 @@ const Debug: FC<IDebug> = ({
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
)}
|
||||
{!isAPIKeySet && (<HasNotSetAPIKEY isTrailFinished={!IS_CE_EDITION} onSetting={onSetting} />)}
|
||||
{!isAPIKeySet && !readonly && (<HasNotSetAPIKEY isTrailFinished={!IS_CE_EDITION} onSetting={onSetting} />)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
onVisionFilesChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { modelModeType, modelConfig, setInputs, mode, isAdvancedMode, completionPromptConfig, chatPromptConfig } = useContext(ConfigContext)
|
||||
const { readonly, modelModeType, modelConfig, setInputs, mode, isAdvancedMode, completionPromptConfig, chatPromptConfig } = useContext(ConfigContext)
|
||||
const [userInputFieldCollapse, setUserInputFieldCollapse] = useState(false)
|
||||
const promptVariables = modelConfig.configs.prompt_variables.filter(({ key, name }) => {
|
||||
return key && key?.trim() && name && name?.trim()
|
||||
@@ -60,12 +60,12 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
|
||||
if (isAdvancedMode) {
|
||||
if (modelModeType === ModelModeType.chat)
|
||||
return chatPromptConfig.prompt.every(({ text }) => !text)
|
||||
return chatPromptConfig?.prompt.every(({ text }) => !text)
|
||||
return !completionPromptConfig.prompt?.text
|
||||
}
|
||||
|
||||
else { return !modelConfig.configs.prompt_template }
|
||||
}, [chatPromptConfig.prompt, completionPromptConfig.prompt?.text, isAdvancedMode, mode, modelConfig.configs.prompt_template, modelModeType])
|
||||
}, [chatPromptConfig?.prompt, completionPromptConfig.prompt?.text, isAdvancedMode, mode, modelConfig.configs.prompt_template, modelModeType])
|
||||
|
||||
const handleInputValueChange = (key: string, value: string | boolean) => {
|
||||
if (!(key in promptVariableObj))
|
||||
@@ -95,8 +95,8 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
<div className={cn('px-4 pt-3', userInputFieldCollapse ? 'pb-3' : 'pb-1')}>
|
||||
<div className='flex cursor-pointer items-center gap-0.5 py-0.5' onClick={() => setUserInputFieldCollapse(!userInputFieldCollapse)}>
|
||||
<div className='system-md-semibold-uppercase text-text-secondary'>{t('appDebug.inputs.userInputField')}</div>
|
||||
{userInputFieldCollapse && <RiArrowRightSLine className='h-4 w-4 text-text-secondary'/>}
|
||||
{!userInputFieldCollapse && <RiArrowDownSLine className='h-4 w-4 text-text-secondary'/>}
|
||||
{userInputFieldCollapse && <RiArrowRightSLine className='h-4 w-4 text-text-secondary' />}
|
||||
{!userInputFieldCollapse && <RiArrowDownSLine className='h-4 w-4 text-text-secondary' />}
|
||||
</div>
|
||||
{!userInputFieldCollapse && (
|
||||
<div className='system-xs-regular mt-1 text-text-tertiary'>{t('appDebug.inputs.completionVarTip')}</div>
|
||||
@@ -124,6 +124,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
placeholder={name}
|
||||
autoFocus={index === 0}
|
||||
maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
)}
|
||||
{type === 'paragraph' && (
|
||||
@@ -132,6 +133,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
placeholder={name}
|
||||
value={inputs[key] ? `${inputs[key]}` : ''}
|
||||
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
)}
|
||||
{type === 'select' && (
|
||||
@@ -142,6 +144,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
items={(options || []).map(i => ({ name: i, value: i }))}
|
||||
allowSearch={false}
|
||||
bgClassName='bg-gray-50'
|
||||
disabled={readonly}
|
||||
/>
|
||||
)}
|
||||
{type === 'number' && (
|
||||
@@ -152,6 +155,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
placeholder={name}
|
||||
autoFocus={index === 0}
|
||||
maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
)}
|
||||
{type === 'checkbox' && (
|
||||
@@ -160,6 +164,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
value={!!inputs[key]}
|
||||
required={required}
|
||||
onChange={(value) => { handleInputValueChange(key, value) }}
|
||||
readonly={readonly}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -178,6 +183,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
url: fileItem.url,
|
||||
upload_file_id: fileItem.fileId,
|
||||
})))}
|
||||
disabled={readonly}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -186,12 +192,12 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
)}
|
||||
{!userInputFieldCollapse && (
|
||||
<div className='flex justify-between border-t border-divider-subtle p-4 pt-3'>
|
||||
<Button className='w-[72px]' onClick={onClear}>{t('common.operation.clear')}</Button>
|
||||
<Button className='w-[72px]' disabled={readonly} onClick={onClear}>{t('common.operation.clear')}</Button>
|
||||
{canNotRun && (
|
||||
<Tooltip popupContent={t('appDebug.otherError.promptNoBeEmpty')}>
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={canNotRun}
|
||||
disabled={canNotRun || readonly}
|
||||
onClick={() => onSend?.()}
|
||||
className="w-[96px]">
|
||||
<RiPlayLargeFill className="mr-0.5 h-4 w-4 shrink-0" aria-hidden="true" />
|
||||
@@ -202,7 +208,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
{!canNotRun && (
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={canNotRun}
|
||||
disabled={canNotRun || readonly}
|
||||
onClick={() => onSend?.()}
|
||||
className="w-[96px]">
|
||||
<RiPlayLargeFill className="mr-0.5 h-4 w-4 shrink-0" aria-hidden="true" />
|
||||
@@ -216,7 +222,10 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
<FeatureBar
|
||||
showFileUpload={false}
|
||||
isChatMode={appType !== AppModeEnum.COMPLETION}
|
||||
onFeatureBarClick={setShowAppConfigureFeaturesModal} />
|
||||
onFeatureBarClick={setShowAppConfigureFeaturesModal}
|
||||
disabled={readonly}
|
||||
hideEditEntrance={readonly}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -6,6 +6,11 @@ import Button from '@/app/components/base/button'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { App } from '@/models/explore'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { RiInformation2Line } from '@remixicon/react'
|
||||
import { useCallback } from 'react'
|
||||
import AppListContext from '@/context/app-list-context'
|
||||
import { useContextSelector } from 'use-context-selector'
|
||||
|
||||
export type AppCardProps = {
|
||||
app: App
|
||||
@@ -19,6 +24,14 @@ const AppCard = ({
|
||||
}: AppCardProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { app: appBasicInfo } = app
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
const isTrialApp = app.can_trial && systemFeatures.enable_trial_app
|
||||
const setShowTryAppPanel = useContextSelector(AppListContext, ctx => ctx.setShowTryAppPanel)
|
||||
const showTryAPPPanel = useCallback((appId: string) => {
|
||||
return () => {
|
||||
setShowTryAppPanel?.(true, { appId, app })
|
||||
}
|
||||
}, [setShowTryAppPanel, app.category])
|
||||
return (
|
||||
<div className={cn('group relative flex h-[132px] cursor-pointer flex-col overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg p-4 shadow-xs hover:shadow-lg')}>
|
||||
<div className='flex shrink-0 grow-0 items-center gap-3 pb-2'>
|
||||
@@ -46,11 +59,17 @@ const AppCard = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn('absolute bottom-0 left-0 right-0 hidden bg-gradient-to-t from-components-panel-gradient-2 from-[60.27%] to-transparent p-4 pt-8 group-hover:flex')}>
|
||||
<div className={cn('flex h-8 w-full items-center space-x-2')}>
|
||||
<Button variant='primary' className='grow' onClick={() => onCreate()}>
|
||||
<div className={cn('w-full', isTrialApp && 'grid grid-cols-2 gap-2')}>
|
||||
<Button variant='primary' className='w-full' onClick={() => onCreate()}>
|
||||
<PlusIcon className='mr-1 h-4 w-4' />
|
||||
<span className='text-xs'>{t('app.newApp.useTemplate')}</span>
|
||||
</Button>
|
||||
{isTrialApp && (
|
||||
<Button className='w-full' onClick={showTryAPPPanel(app.app_id)}>
|
||||
<RiInformation2Line className='mr-1 size-4' />
|
||||
<span>{t('explore.appCard.try')}</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -43,6 +43,7 @@ import cn from '@/utils/classnames'
|
||||
import { noop } from 'lodash-es'
|
||||
import PromptLogModal from '../../base/prompt-log-modal'
|
||||
import { WorkflowContextProvider } from '@/app/components/workflow/context'
|
||||
import { AppSourceType } from '@/service/share'
|
||||
|
||||
type AppStoreState = ReturnType<typeof useAppStore.getState>
|
||||
type ConversationListItem = ChatConversationGeneralDetail | CompletionConversationGeneralDetail
|
||||
@@ -689,12 +690,12 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
}}></div>
|
||||
</div>
|
||||
<TextGeneration
|
||||
appSourceType={AppSourceType.webApp}
|
||||
className='mt-2'
|
||||
content={detail.message.answer}
|
||||
messageId={detail.message.id}
|
||||
isError={false}
|
||||
onRetry={noop}
|
||||
isInstalledApp={false}
|
||||
supportFeedback
|
||||
feedback={detail.message.feedbacks.find((item: any) => item.from_source === 'admin')}
|
||||
onFeedback={feedback => onFeedback(detail.message.id, feedback)}
|
||||
|
||||
@@ -21,7 +21,7 @@ import { Markdown } from '@/app/components/base/markdown'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
|
||||
import { fetchMoreLikeThis, updateFeedback } from '@/service/share'
|
||||
import { AppSourceType, fetchMoreLikeThis, updateFeedback } from '@/service/share'
|
||||
import { fetchTextGenerationMessage } from '@/service/debug'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import WorkflowProcessItem from '@/app/components/base/chat/chat/answer/workflow-process'
|
||||
@@ -52,7 +52,7 @@ export type IGenerationItemProps = {
|
||||
onFeedback?: (feedback: FeedbackType) => void
|
||||
onSave?: (messageId: string) => void
|
||||
isMobile?: boolean
|
||||
isInstalledApp: boolean
|
||||
appSourceType: AppSourceType
|
||||
installedAppId?: string
|
||||
taskId?: string
|
||||
controlClearMoreLikeThis?: number
|
||||
@@ -86,7 +86,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
onSave,
|
||||
depth = 1,
|
||||
isMobile,
|
||||
isInstalledApp,
|
||||
appSourceType,
|
||||
installedAppId,
|
||||
taskId,
|
||||
controlClearMoreLikeThis,
|
||||
@@ -99,6 +99,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
const { t } = useTranslation()
|
||||
const params = useParams()
|
||||
const isTop = depth === 1
|
||||
const isTryApp = appSourceType === AppSourceType.tryApp
|
||||
const [completionRes, setCompletionRes] = useState('')
|
||||
const [childMessageId, setChildMessageId] = useState<string | null>(null)
|
||||
const [childFeedback, setChildFeedback] = useState<FeedbackType>({
|
||||
@@ -112,7 +113,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
const setShowPromptLogModal = useAppStore(s => s.setShowPromptLogModal)
|
||||
|
||||
const handleFeedback = async (childFeedback: FeedbackType) => {
|
||||
await updateFeedback({ url: `/messages/${childMessageId}/feedbacks`, body: { rating: childFeedback.rating } }, isInstalledApp, installedAppId)
|
||||
await updateFeedback({ url: `/messages/${childMessageId}/feedbacks`, body: { rating: childFeedback.rating } }, appSourceType, installedAppId)
|
||||
setChildFeedback(childFeedback)
|
||||
}
|
||||
|
||||
@@ -130,7 +131,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
onSave,
|
||||
isShowTextToSpeech,
|
||||
isMobile,
|
||||
isInstalledApp,
|
||||
appSourceType,
|
||||
installedAppId,
|
||||
controlClearMoreLikeThis,
|
||||
isWorkflow,
|
||||
@@ -144,7 +145,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
return
|
||||
}
|
||||
startQuerying()
|
||||
const res: any = await fetchMoreLikeThis(messageId as string, isInstalledApp, installedAppId)
|
||||
const res: any = await fetchMoreLikeThis(messageId as string, appSourceType, installedAppId)
|
||||
setCompletionRes(res.answer)
|
||||
setChildFeedback({
|
||||
rating: null,
|
||||
@@ -292,7 +293,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
{!isWorkflow && <span>{content?.length} {t('common.unit.char')}</span>}
|
||||
{/* action buttons */}
|
||||
<div className='absolute bottom-1 right-2 flex items-center'>
|
||||
{!isInWebApp && !isInstalledApp && !isResponding && (
|
||||
{!isInWebApp && (appSourceType !== AppSourceType.installedApp) && !isResponding && (
|
||||
<div className='ml-1 flex items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm'>
|
||||
<ActionButton disabled={isError || !messageId} onClick={handleOpenLogModal}>
|
||||
<RiFileList3Line className='h-4 w-4' />
|
||||
@@ -301,12 +302,12 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
</div>
|
||||
)}
|
||||
<div className='ml-1 flex items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm'>
|
||||
{moreLikeThis && (
|
||||
{moreLikeThis && !isTryApp && (
|
||||
<ActionButton state={depth === MAX_DEPTH ? ActionButtonState.Disabled : ActionButtonState.Default} disabled={depth === MAX_DEPTH} onClick={handleMoreLikeThis}>
|
||||
<RiSparklingLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
)}
|
||||
{isShowTextToSpeech && (
|
||||
{isShowTextToSpeech && !isTryApp && (
|
||||
<NewAudioButton
|
||||
id={messageId!}
|
||||
voice={config?.text_to_speech?.voice}
|
||||
@@ -329,13 +330,13 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
<RiReplay15Line className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
)}
|
||||
{isInWebApp && !isWorkflow && (
|
||||
{isInWebApp && !isWorkflow && !isTryApp && (
|
||||
<ActionButton disabled={isError || !messageId} onClick={() => { onSave?.(messageId as string) }}>
|
||||
<RiBookmark3Line className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
{(supportFeedback || isInWebApp) && !isWorkflow && !isError && messageId && (
|
||||
{(supportFeedback || isInWebApp) && !isWorkflow && !isTryApp && !isError && messageId && (
|
||||
<div className='ml-1 flex items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm'>
|
||||
{!feedback?.rating && (
|
||||
<>
|
||||
|
||||
@@ -3,6 +3,16 @@ import { useEducationInit } from '@/app/education-apply/hooks'
|
||||
import List from './list'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AppListContext from '@/context/app-list-context'
|
||||
import { useCallback, useState } from 'react'
|
||||
import type { CurrentTryAppParams } from '@/context/explore-context'
|
||||
import TryApp from '../explore/try-app'
|
||||
import type { CreateAppModalProps } from '../explore/create-app-modal'
|
||||
import CreateAppModal from '../explore/create-app-modal'
|
||||
import { fetchAppDetail } from '@/service/explore'
|
||||
import { DSLImportMode } from '@/models/app'
|
||||
import { useImportDSL } from '@/hooks/use-import-dsl'
|
||||
import DSLConfirmModal from '../app/create-from-dsl-modal/dsl-confirm-modal'
|
||||
|
||||
const Apps = () => {
|
||||
const { t } = useTranslation()
|
||||
@@ -10,10 +20,122 @@ const Apps = () => {
|
||||
useDocumentTitle(t('common.menus.apps'))
|
||||
useEducationInit()
|
||||
|
||||
const [currentTryAppParams, setCurrentTryAppParams] = useState<CurrentTryAppParams | undefined>(undefined)
|
||||
const currApp = currentTryAppParams?.app
|
||||
const [isShowTryAppPanel, setIsShowTryAppPanel] = useState(false)
|
||||
const hideTryAppPanel = useCallback(() => {
|
||||
setIsShowTryAppPanel(false)
|
||||
}, [])
|
||||
const setShowTryAppPanel = (showTryAppPanel: boolean, params?: CurrentTryAppParams) => {
|
||||
if (showTryAppPanel)
|
||||
setCurrentTryAppParams(params)
|
||||
else
|
||||
setCurrentTryAppParams(undefined)
|
||||
setIsShowTryAppPanel(showTryAppPanel)
|
||||
}
|
||||
const [isShowCreateModal, setIsShowCreateModal] = useState(false)
|
||||
|
||||
const handleShowFromTryApp = useCallback(() => {
|
||||
setIsShowCreateModal(true)
|
||||
}, [])
|
||||
|
||||
const [controlRefreshList, setControlRefreshList] = useState(0)
|
||||
const [controlHideCreateFromTemplatePanel, setControlHideCreateFromTemplatePanel] = useState(0)
|
||||
const onSuccess = useCallback(() => {
|
||||
setControlRefreshList(prev => prev + 1)
|
||||
setControlHideCreateFromTemplatePanel(prev => prev + 1)
|
||||
}, [])
|
||||
|
||||
const [showDSLConfirmModal, setShowDSLConfirmModal] = useState(false)
|
||||
|
||||
const {
|
||||
handleImportDSL,
|
||||
handleImportDSLConfirm,
|
||||
versions,
|
||||
isFetching,
|
||||
} = useImportDSL()
|
||||
|
||||
const onConfirmDSL = useCallback(async () => {
|
||||
await handleImportDSLConfirm({
|
||||
onSuccess,
|
||||
})
|
||||
}, [handleImportDSLConfirm, onSuccess])
|
||||
|
||||
const onCreate: CreateAppModalProps['onConfirm'] = async ({
|
||||
name,
|
||||
icon_type,
|
||||
icon,
|
||||
icon_background,
|
||||
description,
|
||||
}) => {
|
||||
hideTryAppPanel()
|
||||
|
||||
const { export_data } = await fetchAppDetail(
|
||||
currApp?.app.id as string,
|
||||
)
|
||||
const payload = {
|
||||
mode: DSLImportMode.YAML_CONTENT,
|
||||
yaml_content: export_data,
|
||||
name,
|
||||
icon_type,
|
||||
icon,
|
||||
icon_background,
|
||||
description,
|
||||
}
|
||||
await handleImportDSL(payload, {
|
||||
onSuccess: () => {
|
||||
setIsShowCreateModal(false)
|
||||
},
|
||||
onPending: () => {
|
||||
setShowDSLConfirmModal(true)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='relative flex h-0 shrink-0 grow flex-col overflow-y-auto bg-background-body'>
|
||||
<List />
|
||||
</div >
|
||||
<AppListContext.Provider value={{
|
||||
currentApp: currentTryAppParams,
|
||||
isShowTryAppPanel,
|
||||
setShowTryAppPanel,
|
||||
controlHideCreateFromTemplatePanel,
|
||||
}}>
|
||||
<div className='relative flex h-0 shrink-0 grow flex-col overflow-y-auto bg-background-body'>
|
||||
<List controlRefreshList={controlRefreshList} />
|
||||
{isShowTryAppPanel && (
|
||||
<TryApp appId={currentTryAppParams?.appId || ''}
|
||||
category={currentTryAppParams?.app?.category}
|
||||
onClose={hideTryAppPanel}
|
||||
onCreate={handleShowFromTryApp}
|
||||
/>
|
||||
)}
|
||||
|
||||
{
|
||||
showDSLConfirmModal && (
|
||||
<DSLConfirmModal
|
||||
versions={versions}
|
||||
onCancel={() => setShowDSLConfirmModal(false)}
|
||||
onConfirm={onConfirmDSL}
|
||||
confirmDisabled={isFetching}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
{isShowCreateModal && (
|
||||
<CreateAppModal
|
||||
appIconType={currApp?.app.icon_type || 'emoji'}
|
||||
appIcon={currApp?.app.icon || ''}
|
||||
appIconBackground={currApp?.app.icon_background || ''}
|
||||
appIconUrl={currApp?.app.icon_url}
|
||||
appName={currApp?.app.name || ''}
|
||||
appDescription={currApp?.app.description || ''}
|
||||
show
|
||||
onConfirm={onCreate}
|
||||
confirmDisabled={isFetching}
|
||||
onHide={() => setIsShowCreateModal(false)}
|
||||
/>
|
||||
)}
|
||||
</div >
|
||||
</AppListContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import type { FC } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
useRouter,
|
||||
@@ -67,7 +68,12 @@ const getKey = (
|
||||
return null
|
||||
}
|
||||
|
||||
const List = () => {
|
||||
type Props = {
|
||||
controlRefreshList: number
|
||||
}
|
||||
const List: FC<Props> = ({
|
||||
controlRefreshList,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
const router = useRouter()
|
||||
@@ -113,6 +119,11 @@ const List = () => {
|
||||
},
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (controlRefreshList > 0)
|
||||
mutate()
|
||||
}, [controlRefreshList])
|
||||
|
||||
const anchorRef = useRef<HTMLDivElement>(null)
|
||||
const options = [
|
||||
{ value: 'all', text: t('app.types.all'), icon: <RiApps2Line className='mr-1 h-[14px] w-[14px]' /> },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
useRouter,
|
||||
useSearchParams,
|
||||
@@ -11,6 +11,8 @@ import { useProviderContext } from '@/context/provider-context'
|
||||
import { FileArrow01, FilePlus01, FilePlus02 } from '@/app/components/base/icons/src/vender/line/files'
|
||||
import cn from '@/utils/classnames'
|
||||
import dynamic from 'next/dynamic'
|
||||
import AppListContext from '@/context/app-list-context'
|
||||
import { useContextSelector } from 'use-context-selector'
|
||||
|
||||
const CreateAppModal = dynamic(() => import('@/app/components/app/create-app-modal'), {
|
||||
ssr: false,
|
||||
@@ -52,6 +54,12 @@ const CreateAppCard = ({
|
||||
return undefined
|
||||
}, [dslUrl])
|
||||
|
||||
const controlHideCreateFromTemplatePanel = useContextSelector(AppListContext, ctx => ctx.controlHideCreateFromTemplatePanel)
|
||||
useEffect(() => {
|
||||
if (controlHideCreateFromTemplatePanel > 0)
|
||||
setShowNewAppTemplateDialog(false)
|
||||
}, [controlHideCreateFromTemplatePanel])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
|
||||
@@ -50,13 +50,14 @@ function getActionButtonState(state: ActionButtonState) {
|
||||
}
|
||||
}
|
||||
|
||||
const ActionButton = ({ className, size, state = ActionButtonState.Default, styleCss, children, ref, ...props }: ActionButtonProps) => {
|
||||
const ActionButton = ({ className, size, state = ActionButtonState.Default, styleCss, children, ref, disabled, ...props }: ActionButtonProps) => {
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
className={classNames(
|
||||
actionButtonVariants({ className, size }),
|
||||
getActionButtonState(state),
|
||||
disabled && 'cursor-not-allowed text-text-disabled hover:bg-transparent hover:text-text-disabled',
|
||||
)}
|
||||
ref={ref}
|
||||
style={styleCss}
|
||||
|
||||
59
web/app/components/base/alert.tsx
Normal file
59
web/app/components/base/alert.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
memo,
|
||||
} from 'react'
|
||||
import {
|
||||
RiCloseLine,
|
||||
RiInformation2Fill,
|
||||
} from '@remixicon/react'
|
||||
import { cva } from 'class-variance-authority'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
type?: 'info'
|
||||
message: string
|
||||
onHide: () => void
|
||||
className?: string
|
||||
}
|
||||
const bgVariants = cva(
|
||||
'',
|
||||
{
|
||||
variants: {
|
||||
type: {
|
||||
info: 'from-components-badge-status-light-normal-halo to-background-gradient-mask-transparent',
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
const Alert: React.FC<Props> = ({
|
||||
type = 'info',
|
||||
message,
|
||||
onHide,
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn('pointer-events-none w-full', className)}>
|
||||
<div
|
||||
className='relative flex space-x-1 overflow-hidden rounded-xl border border-components-panel-border bg-components-panel-bg-blur p-3 shadow-lg'
|
||||
>
|
||||
<div className={cn('pointer-events-none absolute inset-0 bg-gradient-to-r opacity-[0.4]', bgVariants({ type }))}>
|
||||
</div>
|
||||
<div className='flex h-6 w-6 items-center justify-center'>
|
||||
<RiInformation2Fill className='text-text-accent' />
|
||||
</div>
|
||||
<div className='p-1'>
|
||||
<div className='system-xs-regular text-text-secondary'>
|
||||
{message}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className='pointer-events-auto flex h-6 w-6 cursor-pointer items-center justify-center'
|
||||
onClick={onHide}
|
||||
>
|
||||
<RiCloseLine className='h-4 w-4 text-text-tertiary' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Alert)
|
||||
@@ -1,5 +1,5 @@
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { textToAudioStream } from '@/service/share'
|
||||
import { AppSourceType, textToAudioStream } from '@/service/share'
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line ts/consistent-type-definitions
|
||||
@@ -100,7 +100,7 @@ export default class AudioPlayer {
|
||||
|
||||
private async loadAudio() {
|
||||
try {
|
||||
const audioResponse: any = await textToAudioStream(this.url, this.isPublic, { content_type: 'audio/mpeg' }, {
|
||||
const audioResponse: any = await textToAudioStream(this.url, this.isPublic ? AppSourceType.webApp : AppSourceType.installedApp, { content_type: 'audio/mpeg' }, {
|
||||
message_id: this.msgId,
|
||||
streaming: true,
|
||||
voice: this.voice,
|
||||
|
||||
225
web/app/components/base/carousel/index.tsx
Normal file
225
web/app/components/base/carousel/index.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
import cn from '@/utils/classnames'
|
||||
import Autoplay from 'embla-carousel-autoplay'
|
||||
import useEmblaCarousel, { type UseEmblaCarouselType } from 'embla-carousel-react'
|
||||
import * as React from 'react'
|
||||
|
||||
type CarouselApi = UseEmblaCarouselType[1]
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
|
||||
type CarouselOptions = UseCarouselParameters[0]
|
||||
type CarouselPlugin = UseCarouselParameters[1]
|
||||
|
||||
type CarouselProps = {
|
||||
opts?: CarouselOptions;
|
||||
plugins?: CarouselPlugin;
|
||||
orientation?: 'horizontal' | 'vertical';
|
||||
}
|
||||
|
||||
type CarouselContextValue = {
|
||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
||||
api: ReturnType<typeof useEmblaCarousel>[1];
|
||||
scrollPrev: () => void;
|
||||
scrollNext: () => void;
|
||||
selectedIndex: number;
|
||||
canScrollPrev: boolean;
|
||||
canScrollNext: boolean;
|
||||
} & CarouselProps
|
||||
|
||||
const CarouselContext = React.createContext<CarouselContextValue | null>(null)
|
||||
|
||||
function useCarousel() {
|
||||
const context = React.useContext(CarouselContext)
|
||||
|
||||
if (!context)
|
||||
throw new Error('useCarousel must be used within a <Carousel />')
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
type TCarousel = {
|
||||
Content: typeof CarouselContent;
|
||||
Item: typeof CarouselItem;
|
||||
Previous: typeof CarouselPrevious;
|
||||
Next: typeof CarouselNext;
|
||||
Dot: typeof CarouselDot;
|
||||
Plugin: typeof CarouselPlugins;
|
||||
} & React.ForwardRefExoticComponent<
|
||||
React.HTMLAttributes<HTMLDivElement> & CarouselProps & React.RefAttributes<CarouselContextValue>
|
||||
>
|
||||
|
||||
const Carousel: TCarousel = React.forwardRef(
|
||||
({ orientation = 'horizontal', opts, plugins, className, children, ...props }, ref) => {
|
||||
const [carouselRef, api] = useEmblaCarousel(
|
||||
{ ...opts, axis: orientation === 'horizontal' ? 'x' : 'y' },
|
||||
plugins,
|
||||
)
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
||||
const [selectedIndex, setSelectedIndex] = React.useState(0)
|
||||
|
||||
const scrollPrev = React.useCallback(() => {
|
||||
api?.scrollPrev()
|
||||
}, [api])
|
||||
|
||||
const scrollNext = React.useCallback(() => {
|
||||
api?.scrollNext()
|
||||
}, [api])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api)
|
||||
return
|
||||
|
||||
const onSelect = (api: CarouselApi) => {
|
||||
if (!api)
|
||||
return
|
||||
|
||||
setSelectedIndex(api.selectedScrollSnap())
|
||||
setCanScrollPrev(api.canScrollPrev())
|
||||
setCanScrollNext(api.canScrollNext())
|
||||
}
|
||||
|
||||
onSelect(api)
|
||||
api.on('reInit', onSelect)
|
||||
api.on('select', onSelect)
|
||||
|
||||
return () => {
|
||||
api?.off('select', onSelect)
|
||||
}
|
||||
}, [api])
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
carouselRef,
|
||||
api,
|
||||
opts,
|
||||
orientation,
|
||||
scrollPrev,
|
||||
scrollNext,
|
||||
selectedIndex,
|
||||
canScrollPrev,
|
||||
canScrollNext,
|
||||
}))
|
||||
|
||||
return (
|
||||
<CarouselContext.Provider
|
||||
value={{
|
||||
carouselRef,
|
||||
api,
|
||||
opts,
|
||||
orientation,
|
||||
scrollPrev,
|
||||
scrollNext,
|
||||
selectedIndex,
|
||||
canScrollPrev,
|
||||
canScrollNext,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={carouselRef}
|
||||
// onKeyDownCapture={handleKeyDown}
|
||||
className={cn('relative overflow-hidden', className)}
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</CarouselContext.Provider>
|
||||
)
|
||||
},
|
||||
) as TCarousel
|
||||
Carousel.displayName = 'Carousel'
|
||||
|
||||
const CarouselContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => {
|
||||
const { orientation } = useCarousel()
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex', orientation === 'vertical' && 'flex-col', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
CarouselContent.displayName = 'CarouselContent'
|
||||
|
||||
const CarouselItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
className={cn('min-w-0 shrink-0 grow-0 basis-full', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
CarouselItem.displayName = 'CarouselItem'
|
||||
|
||||
type CarouselActionProps = {
|
||||
children?: React.ReactNode;
|
||||
} & Omit<React.HTMLAttributes<HTMLButtonElement>, 'disabled' | 'onClick'>
|
||||
|
||||
const CarouselPrevious = React.forwardRef<HTMLButtonElement, CarouselActionProps>(
|
||||
({ children, ...props }, ref) => {
|
||||
const { scrollPrev, canScrollPrev } = useCarousel()
|
||||
|
||||
return (
|
||||
<button ref={ref} {...props} disabled={!canScrollPrev} onClick={scrollPrev}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
)
|
||||
CarouselPrevious.displayName = 'CarouselPrevious'
|
||||
|
||||
const CarouselNext = React.forwardRef<HTMLButtonElement, CarouselActionProps>(
|
||||
({ children, ...props }, ref) => {
|
||||
const { scrollNext, canScrollNext } = useCarousel()
|
||||
|
||||
return (
|
||||
<button ref={ref} {...props} disabled={!canScrollNext} onClick={scrollNext}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
)
|
||||
CarouselNext.displayName = 'CarouselNext'
|
||||
|
||||
const CarouselDot = React.forwardRef<HTMLButtonElement, CarouselActionProps>(
|
||||
({ children, ...props }, ref) => {
|
||||
const { api, selectedIndex } = useCarousel()
|
||||
|
||||
return api?.slideNodes().map((_, index) => {
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
ref={ref}
|
||||
{...props}
|
||||
data-state={index === selectedIndex ? 'active' : 'inactive'}
|
||||
onClick={() => {
|
||||
api.scrollTo(index)
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
})
|
||||
},
|
||||
)
|
||||
CarouselDot.displayName = 'CarouselDot'
|
||||
|
||||
const CarouselPlugins = {
|
||||
Autoplay,
|
||||
}
|
||||
|
||||
Carousel.Content = CarouselContent
|
||||
Carousel.Item = CarouselItem
|
||||
Carousel.Previous = CarouselPrevious
|
||||
Carousel.Next = CarouselNext
|
||||
Carousel.Dot = CarouselDot
|
||||
Carousel.Plugin = CarouselPlugins
|
||||
|
||||
export { Carousel, useCarousel }
|
||||
@@ -12,6 +12,7 @@ import { InputVarType } from '@/app/components/workflow/types'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import InputsForm from '@/app/components/base/chat/chat-with-history/inputs-form'
|
||||
import {
|
||||
AppSourceType,
|
||||
fetchSuggestedQuestions,
|
||||
getUrl,
|
||||
stopChatMessageResponding,
|
||||
@@ -52,6 +53,11 @@ const ChatWrapper = () => {
|
||||
initUserVariables,
|
||||
} = useChatWithHistoryContext()
|
||||
|
||||
const appSourceType = isInstalledApp ? AppSourceType.installedApp : AppSourceType.webApp
|
||||
|
||||
// Semantic variable for better code readability
|
||||
const isHistoryConversation = !!currentConversationId
|
||||
|
||||
const appConfig = useMemo(() => {
|
||||
const config = appParams || {}
|
||||
|
||||
@@ -79,7 +85,7 @@ const ChatWrapper = () => {
|
||||
inputsForm: inputsForms,
|
||||
},
|
||||
appPrevChatTree,
|
||||
taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId),
|
||||
taskId => stopChatMessageResponding('', taskId, appSourceType, appId),
|
||||
clearChatList,
|
||||
setClearChatList,
|
||||
)
|
||||
@@ -138,11 +144,11 @@ const ChatWrapper = () => {
|
||||
}
|
||||
|
||||
handleSend(
|
||||
getUrl('chat-messages', isInstalledApp, appId || ''),
|
||||
getUrl('chat-messages', appSourceType, appId || ''),
|
||||
data,
|
||||
{
|
||||
onGetSuggestedQuestions: responseItemId => fetchSuggestedQuestions(responseItemId, isInstalledApp, appId),
|
||||
onConversationComplete: currentConversationId ? undefined : handleNewConversationCompleted,
|
||||
onGetSuggestedQuestions: responseItemId => fetchSuggestedQuestions(responseItemId, appSourceType, appId),
|
||||
onConversationComplete: isHistoryConversation ? undefined : handleNewConversationCompleted,
|
||||
isPublicAPI: !isInstalledApp,
|
||||
},
|
||||
)
|
||||
@@ -178,13 +184,13 @@ const ChatWrapper = () => {
|
||||
else {
|
||||
return <InputsForm collapsed={collapsed} setCollapsed={setCollapsed} />
|
||||
}
|
||||
},
|
||||
[
|
||||
}, [
|
||||
inputsForms.length,
|
||||
isMobile,
|
||||
currentConversationId,
|
||||
collapsed, allInputsHidden,
|
||||
])
|
||||
],
|
||||
)
|
||||
|
||||
const welcome = useMemo(() => {
|
||||
const welcomeMessage = chatList.find(item => item.isOpeningStatement)
|
||||
@@ -231,8 +237,7 @@ const ChatWrapper = () => {
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
[
|
||||
}, [
|
||||
appData?.site.icon,
|
||||
appData?.site.icon_background,
|
||||
appData?.site.icon_type,
|
||||
|
||||
@@ -20,6 +20,7 @@ import { buildChatItemTree, getProcessedSystemVariablesFromUrlParams, getRawInpu
|
||||
import { addFileInfos, sortAgentSorts } from '../../../tools/utils'
|
||||
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
|
||||
import {
|
||||
AppSourceType,
|
||||
delConversation,
|
||||
fetchChatList,
|
||||
fetchConversations,
|
||||
@@ -70,6 +71,7 @@ function getFormattedChatList(messages: any[]) {
|
||||
|
||||
export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo])
|
||||
const appSourceType = isInstalledApp ? AppSourceType.installedApp : AppSourceType.webApp
|
||||
const appInfo = useWebAppStore(s => s.appInfo)
|
||||
const appParams = useWebAppStore(s => s.appParams)
|
||||
const appMeta = useWebAppStore(s => s.appMeta)
|
||||
@@ -176,17 +178,17 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
|
||||
const { data: appPinnedConversationData, mutate: mutateAppPinnedConversationData } = useSWR(
|
||||
appId ? ['appConversationData', isInstalledApp, appId, true] : null,
|
||||
() => fetchConversations(isInstalledApp, appId, undefined, true, 100),
|
||||
() => fetchConversations(appSourceType, appId, undefined, true, 100),
|
||||
{ revalidateOnFocus: false, revalidateOnReconnect: false },
|
||||
)
|
||||
const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(
|
||||
appId ? ['appConversationData', isInstalledApp, appId, false] : null,
|
||||
() => fetchConversations(isInstalledApp, appId, undefined, false, 100),
|
||||
() => fetchConversations(appSourceType, appId, undefined, false, 100),
|
||||
{ revalidateOnFocus: false, revalidateOnReconnect: false },
|
||||
)
|
||||
const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(
|
||||
chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null,
|
||||
() => fetchChatList(chatShouldReloadKey, isInstalledApp, appId),
|
||||
chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, appSourceType, appId] : null,
|
||||
() => fetchChatList(chatShouldReloadKey, appSourceType, appId),
|
||||
{ revalidateOnFocus: false, revalidateOnReconnect: false },
|
||||
)
|
||||
|
||||
@@ -309,7 +311,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
handleNewConversationInputsChange(conversationInputs)
|
||||
}, [handleNewConversationInputsChange, inputsForms])
|
||||
|
||||
const { data: newConversation } = useSWR(newConversationId ? [isInstalledApp, appId, newConversationId] : null, () => generationConversationName(isInstalledApp, appId, newConversationId), { revalidateOnFocus: false })
|
||||
const { data: newConversation } = useSWR(newConversationId ? [isInstalledApp, appId, newConversationId] : null, () => generationConversationName(appSourceType, appId, newConversationId), { revalidateOnFocus: false })
|
||||
const [originConversationList, setOriginConversationList] = useState<ConversationItem[]>([])
|
||||
useEffect(() => {
|
||||
if (appConversationData?.data && !appConversationDataLoading)
|
||||
@@ -434,16 +436,16 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
}, [mutateAppConversationData, mutateAppPinnedConversationData])
|
||||
|
||||
const handlePinConversation = useCallback(async (conversationId: string) => {
|
||||
await pinConversation(isInstalledApp, appId, conversationId)
|
||||
await pinConversation(appSourceType, appId, conversationId)
|
||||
notify({ type: 'success', message: t('common.api.success') })
|
||||
handleUpdateConversationList()
|
||||
}, [isInstalledApp, appId, notify, t, handleUpdateConversationList])
|
||||
}, [appSourceType, appId, notify, t, handleUpdateConversationList])
|
||||
|
||||
const handleUnpinConversation = useCallback(async (conversationId: string) => {
|
||||
await unpinConversation(isInstalledApp, appId, conversationId)
|
||||
await unpinConversation(appSourceType, appId, conversationId)
|
||||
notify({ type: 'success', message: t('common.api.success') })
|
||||
handleUpdateConversationList()
|
||||
}, [isInstalledApp, appId, notify, t, handleUpdateConversationList])
|
||||
}, [appSourceType, appId, notify, t, handleUpdateConversationList])
|
||||
|
||||
const [conversationDeleting, setConversationDeleting] = useState(false)
|
||||
const handleDeleteConversation = useCallback(async (
|
||||
@@ -457,7 +459,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
|
||||
try {
|
||||
setConversationDeleting(true)
|
||||
await delConversation(isInstalledApp, appId, conversationId)
|
||||
await delConversation(appSourceType, appId, conversationId)
|
||||
notify({ type: 'success', message: t('common.api.success') })
|
||||
onSuccess()
|
||||
}
|
||||
@@ -492,7 +494,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
|
||||
setConversationRenaming(true)
|
||||
try {
|
||||
await renameConversation(isInstalledApp, appId, conversationId, newName)
|
||||
await renameConversation(appSourceType, appId, conversationId, newName)
|
||||
|
||||
notify({
|
||||
type: 'success',
|
||||
@@ -522,9 +524,9 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
}, [mutateAppConversationData, handleConversationIdInfoChange])
|
||||
|
||||
const handleFeedback = useCallback(async (messageId: string, feedback: Feedback) => {
|
||||
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, isInstalledApp, appId)
|
||||
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, appSourceType, appId)
|
||||
notify({ type: 'success', message: t('common.api.success') })
|
||||
}, [isInstalledApp, appId, t, notify])
|
||||
}, [appSourceType, appId, t, notify])
|
||||
|
||||
return {
|
||||
isInstalledApp,
|
||||
|
||||
@@ -150,7 +150,7 @@ const Answer: FC<AnswerProps> = ({
|
||||
data={workflowProcess}
|
||||
item={item}
|
||||
hideProcessDetail={hideProcessDetail}
|
||||
readonly={hideProcessDetail && appData ? !appData.site.show_workflow_steps : undefined}
|
||||
readonly={hideProcessDetail && appData ? !appData.site?.show_workflow_steps : undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ const Operation: FC<OperationProps> = ({
|
||||
onAnnotationAdded,
|
||||
onAnnotationEdited,
|
||||
onAnnotationRemoved,
|
||||
disableFeedback,
|
||||
onFeedback,
|
||||
onRegenerate,
|
||||
} = useChatContext()
|
||||
@@ -166,7 +167,7 @@ const Operation: FC<OperationProps> = ({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!isOpeningStatement && config?.supportFeedback && !localFeedback?.rating && onFeedback && (
|
||||
{!disableFeedback && !isOpeningStatement && config?.supportFeedback && !localFeedback?.rating && onFeedback && (
|
||||
<div className='ml-1 hidden items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm group-hover:flex'>
|
||||
{!localFeedback?.rating && (
|
||||
<>
|
||||
@@ -180,7 +181,7 @@ const Operation: FC<OperationProps> = ({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!isOpeningStatement && config?.supportFeedback && localFeedback?.rating && onFeedback && (
|
||||
{!disableFeedback && !isOpeningStatement && config?.supportFeedback && localFeedback?.rating && onFeedback && (
|
||||
<div className='ml-1 flex items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm'>
|
||||
{localFeedback?.rating === 'like' && (
|
||||
<ActionButton state={ActionButtonState.Active} onClick={() => handleFeedback(null)}>
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { FC } from 'react'
|
||||
import { memo } from 'react'
|
||||
import type { ChatItem } from '../../types'
|
||||
import { useChatContext } from '../context'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type SuggestedQuestionsProps = {
|
||||
item: ChatItem
|
||||
@@ -9,7 +10,7 @@ type SuggestedQuestionsProps = {
|
||||
const SuggestedQuestions: FC<SuggestedQuestionsProps> = ({
|
||||
item,
|
||||
}) => {
|
||||
const { onSend } = useChatContext()
|
||||
const { onSend, readonly } = useChatContext()
|
||||
|
||||
const {
|
||||
isOpeningStatement,
|
||||
@@ -24,8 +25,11 @@ const SuggestedQuestions: FC<SuggestedQuestionsProps> = ({
|
||||
{suggestedQuestions.filter(q => !!q && q.trim()).map((question, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className='system-sm-medium mr-1 mt-1 inline-flex max-w-full shrink-0 cursor-pointer flex-wrap rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-3.5 py-2 text-components-button-secondary-accent-text shadow-xs last:mr-0 hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover'
|
||||
onClick={() => onSend?.(question)}
|
||||
className={cn(
|
||||
'system-sm-medium mr-1 mt-1 inline-flex max-w-full shrink-0 cursor-pointer flex-wrap rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-3.5 py-2 text-components-button-secondary-accent-text shadow-xs last:mr-0 hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover',
|
||||
readonly && 'pointer-events-none opacity-50',
|
||||
)}
|
||||
onClick={() => !readonly && onSend?.(question)}
|
||||
>
|
||||
{question}
|
||||
</div>),
|
||||
|
||||
@@ -27,8 +27,10 @@ import { useToastContext } from '@/app/components/base/toast'
|
||||
import FeatureBar from '@/app/components/base/features/new-feature-panel/feature-bar'
|
||||
import type { FileUpload } from '@/app/components/base/features/types'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { noop } from 'lodash-es'
|
||||
|
||||
type ChatInputAreaProps = {
|
||||
readonly?: boolean
|
||||
botName?: string
|
||||
showFeatureBar?: boolean
|
||||
showFileUpload?: boolean
|
||||
@@ -44,6 +46,7 @@ type ChatInputAreaProps = {
|
||||
disabled?: boolean
|
||||
}
|
||||
const ChatInputArea = ({
|
||||
readonly,
|
||||
botName,
|
||||
showFeatureBar,
|
||||
showFileUpload,
|
||||
@@ -168,6 +171,7 @@ const ChatInputArea = ({
|
||||
const operation = (
|
||||
<Operation
|
||||
ref={holdSpaceRef}
|
||||
readonly={readonly}
|
||||
fileConfig={visionConfig}
|
||||
speechToTextConfig={speechToTextConfig}
|
||||
onShowVoiceInput={handleShowVoiceInput}
|
||||
@@ -203,7 +207,7 @@ const ChatInputArea = ({
|
||||
className={cn(
|
||||
'body-lg-regular w-full resize-none bg-transparent p-1 leading-6 text-text-primary outline-none',
|
||||
)}
|
||||
placeholder={t('common.chat.inputPlaceholder', { botName }) || ''}
|
||||
placeholder={readonly ? t('common.chat.inputDisabledPlaceholder') : t('common.chat.inputPlaceholder', { botName }) || ''}
|
||||
autoFocus
|
||||
minRows={1}
|
||||
value={query}
|
||||
@@ -216,6 +220,7 @@ const ChatInputArea = ({
|
||||
onDragLeave={handleDragFileLeave}
|
||||
onDragOver={handleDragFileOver}
|
||||
onDrop={handleDropFile}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
@@ -237,7 +242,12 @@ const ChatInputArea = ({
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{showFeatureBar && <FeatureBar showFileUpload={showFileUpload} disabled={featureBarDisabled} onFeatureBarClick={onFeatureBarClick} />}
|
||||
{showFeatureBar && <FeatureBar
|
||||
showFileUpload={showFileUpload}
|
||||
disabled={featureBarDisabled}
|
||||
onFeatureBarClick={readonly ? noop : onFeatureBarClick}
|
||||
hideEditEntrance={readonly}
|
||||
/>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,8 +13,10 @@ import ActionButton from '@/app/components/base/action-button'
|
||||
import { FileUploaderInChatInput } from '@/app/components/base/file-uploader'
|
||||
import type { FileUpload } from '@/app/components/base/features/types'
|
||||
import cn from '@/utils/classnames'
|
||||
import { noop } from 'lodash-es'
|
||||
|
||||
type OperationProps = {
|
||||
readonly?: boolean
|
||||
fileConfig?: FileUpload
|
||||
speechToTextConfig?: EnableType
|
||||
onShowVoiceInput?: () => void
|
||||
@@ -23,6 +25,7 @@ type OperationProps = {
|
||||
ref?: Ref<HTMLDivElement>;
|
||||
}
|
||||
const Operation: FC<OperationProps> = ({
|
||||
readonly,
|
||||
ref,
|
||||
fileConfig,
|
||||
speechToTextConfig,
|
||||
@@ -41,12 +44,13 @@ const Operation: FC<OperationProps> = ({
|
||||
ref={ref}
|
||||
>
|
||||
<div className='flex items-center space-x-1'>
|
||||
{fileConfig?.enabled && <FileUploaderInChatInput fileConfig={fileConfig} />}
|
||||
{fileConfig?.enabled && <FileUploaderInChatInput readonly={readonly} fileConfig={fileConfig} />}
|
||||
{
|
||||
speechToTextConfig?.enabled && (
|
||||
<ActionButton
|
||||
size='l'
|
||||
onClick={onShowVoiceInput}
|
||||
disabled={readonly}
|
||||
onClick={readonly ? noop : onShowVoiceInput}
|
||||
>
|
||||
<RiMicLine className='h-5 w-5' />
|
||||
</ActionButton>
|
||||
@@ -56,7 +60,8 @@ const Operation: FC<OperationProps> = ({
|
||||
<Button
|
||||
className='ml-3 w-8 px-0'
|
||||
variant='primary'
|
||||
onClick={onSend}
|
||||
onClick={readonly ? noop : onSend}
|
||||
disabled={readonly}
|
||||
style={
|
||||
theme
|
||||
? {
|
||||
|
||||
@@ -15,11 +15,15 @@ export type ChatContextValue = Pick<ChatProps, 'config'
|
||||
| 'onAnnotationEdited'
|
||||
| 'onAnnotationAdded'
|
||||
| 'onAnnotationRemoved'
|
||||
| 'disableFeedback'
|
||||
| 'onFeedback'
|
||||
>
|
||||
> & {
|
||||
readonly?: boolean
|
||||
}
|
||||
|
||||
const ChatContext = createContext<ChatContextValue>({
|
||||
chatList: [],
|
||||
readonly: false,
|
||||
})
|
||||
|
||||
type ChatContextProviderProps = {
|
||||
@@ -28,6 +32,7 @@ type ChatContextProviderProps = {
|
||||
|
||||
export const ChatContextProvider = ({
|
||||
children,
|
||||
readonly = false,
|
||||
config,
|
||||
isResponding,
|
||||
chatList,
|
||||
@@ -39,11 +44,13 @@ export const ChatContextProvider = ({
|
||||
onAnnotationEdited,
|
||||
onAnnotationAdded,
|
||||
onAnnotationRemoved,
|
||||
disableFeedback,
|
||||
onFeedback,
|
||||
}: ChatContextProviderProps) => {
|
||||
return (
|
||||
<ChatContext.Provider value={{
|
||||
config,
|
||||
readonly,
|
||||
isResponding,
|
||||
chatList: chatList || [],
|
||||
showPromptLog,
|
||||
@@ -54,6 +61,7 @@ export const ChatContextProvider = ({
|
||||
onAnnotationEdited,
|
||||
onAnnotationAdded,
|
||||
onAnnotationRemoved,
|
||||
disableFeedback,
|
||||
onFeedback,
|
||||
}}>
|
||||
{children}
|
||||
|
||||
@@ -36,6 +36,8 @@ import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import type { AppData } from '@/models/share'
|
||||
|
||||
export type ChatProps = {
|
||||
isTryApp?: boolean
|
||||
readonly?: boolean
|
||||
appData?: AppData
|
||||
chatList: ChatItem[]
|
||||
config?: ChatConfig
|
||||
@@ -60,6 +62,7 @@ export type ChatProps = {
|
||||
onAnnotationAdded?: (annotationId: string, authorName: string, question: string, answer: string, index: number) => void
|
||||
onAnnotationRemoved?: (index: number) => void
|
||||
chatNode?: ReactNode
|
||||
disableFeedback?: boolean
|
||||
onFeedback?: (messageId: string, feedback: Feedback) => void
|
||||
chatAnswerContainerInner?: string
|
||||
hideProcessDetail?: boolean
|
||||
@@ -76,6 +79,8 @@ export type ChatProps = {
|
||||
}
|
||||
|
||||
const Chat: FC<ChatProps> = ({
|
||||
isTryApp,
|
||||
readonly = false,
|
||||
appData,
|
||||
config,
|
||||
onSend,
|
||||
@@ -99,6 +104,7 @@ const Chat: FC<ChatProps> = ({
|
||||
onAnnotationEdited,
|
||||
onAnnotationRemoved,
|
||||
chatNode,
|
||||
disableFeedback,
|
||||
onFeedback,
|
||||
chatAnswerContainerInner,
|
||||
hideProcessDetail,
|
||||
@@ -219,6 +225,7 @@ const Chat: FC<ChatProps> = ({
|
||||
|
||||
return (
|
||||
<ChatContextProvider
|
||||
readonly={readonly}
|
||||
config={config}
|
||||
chatList={chatList}
|
||||
isResponding={isResponding}
|
||||
@@ -230,17 +237,18 @@ const Chat: FC<ChatProps> = ({
|
||||
onAnnotationAdded={onAnnotationAdded}
|
||||
onAnnotationEdited={onAnnotationEdited}
|
||||
onAnnotationRemoved={onAnnotationRemoved}
|
||||
disableFeedback={disableFeedback}
|
||||
onFeedback={onFeedback}
|
||||
>
|
||||
<div className='relative h-full'>
|
||||
<div className={cn('relative h-full', isTryApp && 'flex flex-col')}>
|
||||
<div
|
||||
ref={chatContainerRef}
|
||||
className={cn('relative h-full overflow-y-auto overflow-x-hidden', chatContainerClassName)}
|
||||
className={cn('relative h-full overflow-y-auto overflow-x-hidden', isTryApp && 'h-0 grow', chatContainerClassName)}
|
||||
>
|
||||
{chatNode}
|
||||
<div
|
||||
ref={chatContainerInnerRef}
|
||||
className={cn('w-full', !noSpacing && 'px-8', chatContainerInnerClassName)}
|
||||
className={cn('w-full', !noSpacing && 'px-8', chatContainerInnerClassName, isTryApp && 'px-0')}
|
||||
>
|
||||
{
|
||||
chatList.map((item, index) => {
|
||||
@@ -284,7 +292,7 @@ const Chat: FC<ChatProps> = ({
|
||||
>
|
||||
<div
|
||||
ref={chatFooterInnerRef}
|
||||
className={cn('relative', chatFooterInnerClassName)}
|
||||
className={cn('relative', chatFooterInnerClassName, isTryApp && 'px-0')}
|
||||
>
|
||||
{
|
||||
!noStopResponding && isResponding && (
|
||||
@@ -308,7 +316,7 @@ const Chat: FC<ChatProps> = ({
|
||||
{
|
||||
!noChatInput && (
|
||||
<ChatInputArea
|
||||
botName={appData?.site.title || 'Bot'}
|
||||
botName={appData?.site?.title || 'Bot'}
|
||||
disabled={inputDisabled}
|
||||
showFeatureBar={showFeatureBar}
|
||||
showFileUpload={showFileUpload}
|
||||
@@ -321,6 +329,7 @@ const Chat: FC<ChatProps> = ({
|
||||
inputsForm={inputsForm}
|
||||
theme={themeBuilder?.theme}
|
||||
isResponding={isResponding}
|
||||
readonly={readonly}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { InputVarType } from '@/app/components/workflow/types'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import InputsForm from '@/app/components/base/chat/embedded-chatbot/inputs-form'
|
||||
import {
|
||||
AppSourceType,
|
||||
fetchSuggestedQuestions,
|
||||
getUrl,
|
||||
stopChatMessageResponding,
|
||||
@@ -42,6 +43,7 @@ const ChatWrapper = () => {
|
||||
isInstalledApp,
|
||||
appId,
|
||||
appMeta,
|
||||
disableFeedback,
|
||||
handleFeedback,
|
||||
currentChatInstanceRef,
|
||||
themeBuilder,
|
||||
@@ -50,7 +52,9 @@ const ChatWrapper = () => {
|
||||
setIsResponding,
|
||||
allInputsHidden,
|
||||
initUserVariables,
|
||||
appSourceType,
|
||||
} = useEmbeddedChatbotContext()
|
||||
|
||||
const appConfig = useMemo(() => {
|
||||
const config = appParams || {}
|
||||
|
||||
@@ -78,7 +82,7 @@ const ChatWrapper = () => {
|
||||
inputsForm: inputsForms,
|
||||
},
|
||||
appPrevChatList,
|
||||
taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId),
|
||||
taskId => stopChatMessageResponding('', taskId, appSourceType, appId),
|
||||
clearChatList,
|
||||
setClearChatList,
|
||||
)
|
||||
@@ -134,14 +138,13 @@ const ChatWrapper = () => {
|
||||
conversation_id: currentConversationId,
|
||||
parent_message_id: (isRegenerate ? parentAnswer?.id : getLastAnswer(chatList)?.id) || null,
|
||||
}
|
||||
|
||||
handleSend(
|
||||
getUrl('chat-messages', isInstalledApp, appId || ''),
|
||||
getUrl('chat-messages', appSourceType, appId || ''),
|
||||
data,
|
||||
{
|
||||
onGetSuggestedQuestions: responseItemId => fetchSuggestedQuestions(responseItemId, isInstalledApp, appId),
|
||||
onGetSuggestedQuestions: responseItemId => fetchSuggestedQuestions(responseItemId, appSourceType, appId),
|
||||
onConversationComplete: currentConversationId ? undefined : handleNewConversationCompleted,
|
||||
isPublicAPI: !isInstalledApp,
|
||||
isPublicAPI: appSourceType === AppSourceType.webApp,
|
||||
},
|
||||
)
|
||||
}, [currentConversationId, currentConversationInputs, newConversationInputs, chatList, handleSend, isInstalledApp, appId, handleNewConversationCompleted])
|
||||
@@ -163,7 +166,8 @@ const ChatWrapper = () => {
|
||||
return chatList.filter(item => !item.isOpeningStatement)
|
||||
}, [chatList, currentConversationId])
|
||||
|
||||
const [collapsed, setCollapsed] = useState(!!currentConversationId)
|
||||
const isTryApp = appSourceType === AppSourceType.tryApp
|
||||
const [collapsed, setCollapsed] = useState(!!currentConversationId && !isTryApp) // try app always use the new chat
|
||||
|
||||
const chatNode = useMemo(() => {
|
||||
if (allInputsHidden || !inputsForms.length)
|
||||
@@ -188,6 +192,8 @@ const ChatWrapper = () => {
|
||||
return null
|
||||
if (!collapsed && inputsForms.length > 0 && !allInputsHidden)
|
||||
return null
|
||||
if (!appData?.site)
|
||||
return null
|
||||
if (welcomeMessage.suggestedQuestions && welcomeMessage.suggestedQuestions?.length > 0) {
|
||||
return (
|
||||
<div className={cn('flex items-center justify-center px-4 py-12', isMobile ? 'min-h-[30vh] py-0' : 'h-[50vh]')}>
|
||||
@@ -221,7 +227,7 @@ const ChatWrapper = () => {
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}, [appData?.site.icon, appData?.site.icon_background, appData?.site.icon_type, appData?.site.icon_url, chatList, collapsed, currentConversationId, inputsForms.length, respondingState, allInputsHidden])
|
||||
}, [appData?.site, chatList, collapsed, currentConversationId, inputsForms.length, respondingState, allInputsHidden])
|
||||
|
||||
const answerIcon = isDify()
|
||||
? <LogoAvatar className='relative shrink-0' />
|
||||
@@ -236,6 +242,7 @@ const ChatWrapper = () => {
|
||||
|
||||
return (
|
||||
<Chat
|
||||
isTryApp={isTryApp}
|
||||
appData={appData || undefined}
|
||||
config={appConfig}
|
||||
chatList={messageList}
|
||||
@@ -255,6 +262,7 @@ const ChatWrapper = () => {
|
||||
</>
|
||||
}
|
||||
allToolIcons={appMeta?.tool_icons || {}}
|
||||
disableFeedback={disableFeedback}
|
||||
onFeedback={handleFeedback}
|
||||
suggestedQuestions={suggestedQuestions}
|
||||
answerIcon={answerIcon}
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
ConversationItem,
|
||||
} from '@/models/share'
|
||||
import { noop } from 'lodash-es'
|
||||
import { AppSourceType } from '@/service/share'
|
||||
|
||||
export type EmbeddedChatbotContextValue = {
|
||||
appMeta: AppMeta | null
|
||||
@@ -37,8 +38,10 @@ export type EmbeddedChatbotContextValue = {
|
||||
chatShouldReloadKey: string
|
||||
isMobile: boolean
|
||||
isInstalledApp: boolean
|
||||
appSourceType: AppSourceType
|
||||
allowResetChat: boolean
|
||||
appId?: string
|
||||
disableFeedback?: boolean
|
||||
handleFeedback: (messageId: string, feedback: Feedback) => void
|
||||
currentChatInstanceRef: RefObject<{ handleStop: () => void }>
|
||||
themeBuilder?: ThemeBuilder
|
||||
@@ -74,6 +77,7 @@ export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>
|
||||
handleNewConversationCompleted: noop,
|
||||
chatShouldReloadKey: '',
|
||||
isMobile: false,
|
||||
appSourceType: AppSourceType.webApp,
|
||||
isInstalledApp: false,
|
||||
allowResetChat: true,
|
||||
handleFeedback: noop,
|
||||
|
||||
@@ -18,12 +18,15 @@ import { CONVERSATION_ID_INFO } from '../constants'
|
||||
import { buildChatItemTree, getProcessedInputsFromUrlParams, getProcessedSystemVariablesFromUrlParams, getProcessedUserVariablesFromUrlParams } from '../utils'
|
||||
import { getProcessedFilesFromResponse } from '../../file-uploader/utils'
|
||||
import {
|
||||
AppSourceType,
|
||||
fetchChatList,
|
||||
fetchConversations,
|
||||
generationConversationName,
|
||||
updateFeedback,
|
||||
} from '@/service/share'
|
||||
|
||||
import type {
|
||||
AppData,
|
||||
// AppData,
|
||||
ConversationItem,
|
||||
} from '@/models/share'
|
||||
@@ -34,6 +37,7 @@ import { TransferMethod } from '@/types/app'
|
||||
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
|
||||
import { noop } from 'lodash-es'
|
||||
import { useWebAppStore } from '@/context/web-app-context'
|
||||
import { useGetTryAppInfo, useGetTryAppParams } from '@/service/use-try-app'
|
||||
|
||||
function getFormattedChatList(messages: any[]) {
|
||||
const newChatList: ChatItem[] = []
|
||||
@@ -61,18 +65,35 @@ function getFormattedChatList(messages: any[]) {
|
||||
return newChatList
|
||||
}
|
||||
|
||||
export const useEmbeddedChatbot = () => {
|
||||
const isInstalledApp = false
|
||||
const appInfo = useWebAppStore(s => s.appInfo)
|
||||
export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: string) => {
|
||||
const isInstalledApp = false // just can be webapp and try app
|
||||
const isTryApp = appSourceType === AppSourceType.tryApp
|
||||
const { data: tryAppInfo } = useGetTryAppInfo(isTryApp ? tryAppId! : '')
|
||||
const webAppInfo = useWebAppStore(s => s.appInfo)
|
||||
const appInfo = isTryApp ? tryAppInfo : webAppInfo
|
||||
const appMeta = useWebAppStore(s => s.appMeta)
|
||||
const appParams = useWebAppStore(s => s.appParams)
|
||||
const { data: tryAppParams } = useGetTryAppParams(isTryApp ? tryAppId! : '')
|
||||
const webAppParams = useWebAppStore(s => s.appParams)
|
||||
const appParams = isTryApp ? tryAppParams : webAppParams
|
||||
|
||||
const appId = useMemo(() => {
|
||||
return isTryApp ? tryAppId : (appInfo as any)?.app_id
|
||||
}, [appInfo])
|
||||
|
||||
const embeddedConversationId = useWebAppStore(s => s.embeddedConversationId)
|
||||
const embeddedUserId = useWebAppStore(s => s.embeddedUserId)
|
||||
const appId = useMemo(() => appInfo?.app_id, [appInfo])
|
||||
|
||||
const [userId, setUserId] = useState<string>()
|
||||
const [conversationId, setConversationId] = useState<string>()
|
||||
|
||||
useEffect(() => {
|
||||
if (isTryApp) return
|
||||
getProcessedSystemVariablesFromUrlParams().then(({ user_id, conversation_id }) => {
|
||||
setUserId(user_id)
|
||||
setConversationId(conversation_id)
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setUserId(embeddedUserId || undefined)
|
||||
}, [embeddedUserId])
|
||||
@@ -82,6 +103,7 @@ export const useEmbeddedChatbot = () => {
|
||||
}, [embeddedConversationId])
|
||||
|
||||
useEffect(() => {
|
||||
if (isTryApp) return
|
||||
const setLanguageFromParams = async () => {
|
||||
// Check URL parameters for language override
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
@@ -99,9 +121,9 @@ export const useEmbeddedChatbot = () => {
|
||||
// If locale is set as a system variable, use that
|
||||
await changeLanguage(localeFromSysVar)
|
||||
}
|
||||
else if (appInfo?.site.default_language) {
|
||||
else if ((appInfo as unknown as AppData)?.site?.default_language) {
|
||||
// Otherwise use the default from app config
|
||||
await changeLanguage(appInfo.site.default_language)
|
||||
await changeLanguage((appInfo as unknown as AppData).site?.default_language)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +133,13 @@ export const useEmbeddedChatbot = () => {
|
||||
const [conversationIdInfo, setConversationIdInfo] = useLocalStorageState<Record<string, Record<string, string>>>(CONVERSATION_ID_INFO, {
|
||||
defaultValue: {},
|
||||
})
|
||||
const removeConversationIdInfo = useCallback((appId: string) => {
|
||||
setConversationIdInfo((prev) => {
|
||||
const newInfo = { ...prev }
|
||||
delete newInfo[appId]
|
||||
return newInfo
|
||||
})
|
||||
}, [setConversationIdInfo])
|
||||
const allowResetChat = !conversationId
|
||||
const currentConversationId = useMemo(() => conversationIdInfo?.[appId || '']?.[userId || 'DEFAULT'] || conversationId || '',
|
||||
[appId, conversationIdInfo, userId, conversationId])
|
||||
@@ -136,10 +165,12 @@ export const useEmbeddedChatbot = () => {
|
||||
|
||||
return currentConversationId
|
||||
}, [currentConversationId, newConversationId])
|
||||
|
||||
const { data: appPinnedConversationData } = useSWR(['appConversationData', isInstalledApp, appId, true], () => fetchConversations(isInstalledApp, appId, undefined, true, 100))
|
||||
const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(['appConversationData', isInstalledApp, appId, false], () => fetchConversations(isInstalledApp, appId, undefined, false, 100))
|
||||
const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId))
|
||||
// todo app params
|
||||
// const { data: appParams } = useSWR(['appParams', appSourceType, appId], () => fetchAppParams(appSourceType, appId))
|
||||
// const { data: appMeta } = useSWR(isTryApp ? null : ['appMeta', appSourceType, appId], () => fetchAppMeta(appSourceType, appId))
|
||||
const { data: appPinnedConversationData } = useSWR(isTryApp ? null : ['appConversationData', appSourceType, appId, true], () => fetchConversations(appSourceType, appId, undefined, true, 100))
|
||||
const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(isTryApp ? null : ['appConversationData', appSourceType, appId, false], () => fetchConversations(appSourceType, appId, undefined, false, 100))
|
||||
const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR((chatShouldReloadKey && !isTryApp) ? ['appChatList', chatShouldReloadKey, appSourceType, appId] : null, () => fetchChatList(chatShouldReloadKey, appSourceType, appId))
|
||||
|
||||
const [clearChatList, setClearChatList] = useState(false)
|
||||
const [isResponding, setIsResponding] = useState(false)
|
||||
@@ -244,6 +275,8 @@ export const useEmbeddedChatbot = () => {
|
||||
useEffect(() => {
|
||||
// init inputs from url params
|
||||
(async () => {
|
||||
if (isTryApp)
|
||||
return
|
||||
const inputs = await getProcessedInputsFromUrlParams()
|
||||
const userVariables = await getProcessedUserVariablesFromUrlParams()
|
||||
setInitInputs(inputs)
|
||||
@@ -259,7 +292,7 @@ export const useEmbeddedChatbot = () => {
|
||||
handleNewConversationInputsChange(conversationInputs)
|
||||
}, [handleNewConversationInputsChange, inputsForms])
|
||||
|
||||
const { data: newConversation } = useSWR(newConversationId ? [isInstalledApp, appId, newConversationId] : null, () => generationConversationName(isInstalledApp, appId, newConversationId), { revalidateOnFocus: false })
|
||||
const { data: newConversation } = useSWR((!isTryApp && newConversationId) ? [isInstalledApp, appId, newConversationId] : null, () => generationConversationName(appSourceType, appId, newConversationId), { revalidateOnFocus: false })
|
||||
const [originConversationList, setOriginConversationList] = useState<ConversationItem[]>([])
|
||||
useEffect(() => {
|
||||
if (appConversationData?.data && !appConversationDataLoading)
|
||||
@@ -308,7 +341,7 @@ export const useEmbeddedChatbot = () => {
|
||||
}, [appChatListData, currentConversationId])
|
||||
const [currentConversationInputs, setCurrentConversationInputs] = useState<Record<string, any>>(currentConversationLatestInputs || {})
|
||||
useEffect(() => {
|
||||
if (currentConversationItem)
|
||||
if (currentConversationItem && !isTryApp)
|
||||
setCurrentConversationInputs(currentConversationLatestInputs || {})
|
||||
}, [currentConversationItem, currentConversationLatestInputs])
|
||||
|
||||
@@ -368,12 +401,17 @@ export const useEmbeddedChatbot = () => {
|
||||
setClearChatList(false)
|
||||
}, [handleConversationIdInfoChange, setClearChatList])
|
||||
const handleNewConversation = useCallback(async () => {
|
||||
if (isTryApp) {
|
||||
setClearChatList(true)
|
||||
return
|
||||
}
|
||||
|
||||
currentChatInstanceRef.current.handleStop()
|
||||
setShowNewConversationItemInList(true)
|
||||
handleChangeConversation('')
|
||||
handleNewConversationInputsChange(await getProcessedInputsFromUrlParams())
|
||||
setClearChatList(true)
|
||||
}, [handleChangeConversation, setShowNewConversationItemInList, handleNewConversationInputsChange, setClearChatList])
|
||||
}, [isTryApp, setShowNewConversationItemInList, handleNewConversationInputsChange, setClearChatList])
|
||||
|
||||
const handleNewConversationCompleted = useCallback((newConversationId: string) => {
|
||||
setNewConversationId(newConversationId)
|
||||
@@ -383,16 +421,18 @@ export const useEmbeddedChatbot = () => {
|
||||
}, [mutateAppConversationData, handleConversationIdInfoChange])
|
||||
|
||||
const handleFeedback = useCallback(async (messageId: string, feedback: Feedback) => {
|
||||
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, isInstalledApp, appId)
|
||||
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, appSourceType, appId)
|
||||
notify({ type: 'success', message: t('common.api.success') })
|
||||
}, [isInstalledApp, appId, t, notify])
|
||||
}, [appSourceType, appId, t, notify])
|
||||
|
||||
return {
|
||||
appSourceType,
|
||||
isInstalledApp,
|
||||
allowResetChat,
|
||||
appId,
|
||||
currentConversationId,
|
||||
currentConversationItem,
|
||||
removeConversationIdInfo,
|
||||
handleConversationIdInfoChange,
|
||||
appData: appInfo,
|
||||
appParams: appParams || {} as ChatConfig,
|
||||
|
||||
@@ -20,6 +20,8 @@ import DifyLogo from '@/app/components/base/logo/dify-logo'
|
||||
import cn from '@/utils/classnames'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { AppSourceType } from '@/service/share'
|
||||
import type { AppData } from '@/models/share'
|
||||
|
||||
const Chatbot = () => {
|
||||
const {
|
||||
@@ -131,10 +133,11 @@ const EmbeddedChatbotWrapper = () => {
|
||||
setCurrentConversationInputs,
|
||||
allInputsHidden,
|
||||
initUserVariables,
|
||||
} = useEmbeddedChatbot()
|
||||
} = useEmbeddedChatbot(AppSourceType.webApp)
|
||||
|
||||
return <EmbeddedChatbotContext.Provider value={{
|
||||
appData,
|
||||
appSourceType: AppSourceType.webApp,
|
||||
appData: appData as AppData,
|
||||
appParams,
|
||||
appMeta,
|
||||
appChatListDataLoading,
|
||||
|
||||
@@ -6,6 +6,7 @@ import Divider from '@/app/components/base/divider'
|
||||
import InputsFormContent from '@/app/components/base/chat/embedded-chatbot/inputs-form/content'
|
||||
import { useEmbeddedChatbotContext } from '../context'
|
||||
import cn from '@/utils/classnames'
|
||||
import { AppSourceType } from '@/service/share'
|
||||
|
||||
type Props = {
|
||||
collapsed: boolean
|
||||
@@ -18,6 +19,7 @@ const InputsFormNode = ({
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
appSourceType,
|
||||
isMobile,
|
||||
currentConversationId,
|
||||
themeBuilder,
|
||||
@@ -25,15 +27,17 @@ const InputsFormNode = ({
|
||||
allInputsHidden,
|
||||
inputsForms,
|
||||
} = useEmbeddedChatbotContext()
|
||||
const isTryApp = appSourceType === AppSourceType.tryApp
|
||||
|
||||
if (allInputsHidden || inputsForms.length === 0)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className={cn('mb-6 flex flex-col items-center px-4 pt-6', isMobile && 'mb-4 pt-4')}>
|
||||
<div className={cn('mb-6 flex flex-col items-center px-4 pt-6', isMobile && 'mb-4 pt-4', isTryApp && 'mb-0 px-0')}>
|
||||
<div className={cn(
|
||||
'w-full max-w-[672px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-md',
|
||||
collapsed && 'border border-components-card-border bg-components-card-bg shadow-none',
|
||||
isTryApp && 'max-w-[auto]',
|
||||
)}>
|
||||
<div className={cn(
|
||||
'flex items-center gap-3 rounded-t-2xl px-6 py-4',
|
||||
|
||||
@@ -33,7 +33,7 @@ const ViewFormDropdown = ({ iconColor }: Props) => {
|
||||
<RiChatSettingsLine className={cn('h-[18px] w-[18px]', iconColor)} />
|
||||
</ActionButton>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className="z-50">
|
||||
<PortalToFollowElemContent className="z-[99]">
|
||||
<div className='w-[400px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg backdrop-blur-sm'>
|
||||
<div className='flex items-center gap-3 rounded-t-2xl border-b border-divider-subtle px-6 py-4'>
|
||||
<Message3Fill className='h-6 w-6 shrink-0' />
|
||||
|
||||
@@ -13,6 +13,7 @@ type Props = {
|
||||
showFileUpload?: boolean
|
||||
disabled?: boolean
|
||||
onFeatureBarClick?: (state: boolean) => void
|
||||
hideEditEntrance?: boolean
|
||||
}
|
||||
|
||||
const FeatureBar = ({
|
||||
@@ -20,6 +21,7 @@ const FeatureBar = ({
|
||||
showFileUpload = true,
|
||||
disabled,
|
||||
onFeatureBarClick,
|
||||
hideEditEntrance = false,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const features = useFeatures(s => s.features)
|
||||
@@ -132,10 +134,14 @@ const FeatureBar = ({
|
||||
)}
|
||||
</div>
|
||||
<div className='body-xs-regular grow text-text-tertiary'>{t('appDebug.feature.bar.enableText')}</div>
|
||||
<Button className='shrink-0' variant='ghost-accent' size='small' onClick={() => onFeatureBarClick?.(true)}>
|
||||
<div className='mx-1'>{t('appDebug.feature.bar.manage')}</div>
|
||||
<RiArrowRightLine className='h-3.5 w-3.5 text-text-accent' />
|
||||
</Button>
|
||||
{
|
||||
!hideEditEntrance && (
|
||||
<Button className='shrink-0' variant='ghost-accent' size='small' onClick={() => onFeatureBarClick?.(true)}>
|
||||
<div className='mx-1'>{t('appDebug.feature.bar.manage')}</div>
|
||||
<RiArrowRightLine className='h-3.5 w-3.5 text-text-accent' />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -13,21 +13,27 @@ import { TransferMethod } from '@/types/app'
|
||||
|
||||
type FileUploaderInChatInputProps = {
|
||||
fileConfig: FileUpload
|
||||
readonly?: boolean
|
||||
}
|
||||
const FileUploaderInChatInput = ({
|
||||
fileConfig,
|
||||
readonly,
|
||||
}: FileUploaderInChatInputProps) => {
|
||||
const renderTrigger = useCallback((open: boolean) => {
|
||||
return (
|
||||
<ActionButton
|
||||
size='l'
|
||||
className={cn(open && 'bg-state-base-hover')}
|
||||
disabled={readonly}
|
||||
>
|
||||
<RiAttachmentLine className='h-5 w-5' />
|
||||
</ActionButton>
|
||||
)
|
||||
}, [])
|
||||
|
||||
if(readonly)
|
||||
return renderTrigger(false)
|
||||
|
||||
return (
|
||||
<FileFromLinkOrLocal
|
||||
trigger={renderTrigger}
|
||||
|
||||
@@ -69,10 +69,12 @@ const PasteImageLinkButton: FC<PasteImageLinkButtonProps> = ({
|
||||
type TextGenerationImageUploaderProps = {
|
||||
settings: VisionSettings
|
||||
onFilesChange: (files: ImageFile[]) => void
|
||||
disabled?: boolean
|
||||
}
|
||||
const TextGenerationImageUploader: FC<TextGenerationImageUploaderProps> = ({
|
||||
settings,
|
||||
onFilesChange,
|
||||
disabled,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -92,7 +94,7 @@ const TextGenerationImageUploader: FC<TextGenerationImageUploaderProps> = ({
|
||||
const localUpload = (
|
||||
<Uploader
|
||||
onUpload={onUpload}
|
||||
disabled={files.length >= settings.number_limits}
|
||||
disabled={files.length >= settings.number_limits || disabled}
|
||||
limit={+settings.image_file_size_limit!}
|
||||
>
|
||||
{
|
||||
@@ -113,7 +115,7 @@ const TextGenerationImageUploader: FC<TextGenerationImageUploaderProps> = ({
|
||||
const urlUpload = (
|
||||
<PasteImageLinkButton
|
||||
onUpload={onUpload}
|
||||
disabled={files.length >= settings.number_limits}
|
||||
disabled={files.length >= settings.number_limits || disabled}
|
||||
/>
|
||||
)
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ export type ITabHeaderProps = {
|
||||
items: Item[]
|
||||
value: string
|
||||
itemClassName?: string
|
||||
itemWrapClassName?: string
|
||||
activeItemClassName?: string
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
@@ -23,6 +25,8 @@ const TabHeader: FC<ITabHeaderProps> = ({
|
||||
items,
|
||||
value,
|
||||
itemClassName,
|
||||
itemWrapClassName,
|
||||
activeItemClassName,
|
||||
onChange,
|
||||
}) => {
|
||||
const renderItem = ({ id, name, icon, extra, disabled }: Item) => (
|
||||
@@ -30,8 +34,9 @@ const TabHeader: FC<ITabHeaderProps> = ({
|
||||
key={id}
|
||||
className={cn(
|
||||
'system-md-semibold relative flex cursor-pointer items-center border-b-2 border-transparent pb-2 pt-2.5',
|
||||
id === value ? 'border-components-tab-active text-text-primary' : 'text-text-tertiary',
|
||||
id === value ? cn('border-components-tab-active text-text-primary', activeItemClassName) : 'text-text-tertiary',
|
||||
disabled && 'cursor-not-allowed opacity-30',
|
||||
itemWrapClassName,
|
||||
)}
|
||||
onClick={() => !disabled && onChange(id)}
|
||||
>
|
||||
|
||||
@@ -11,7 +11,7 @@ import { convertToMp3 } from './utils'
|
||||
import s from './index.module.css'
|
||||
import cn from '@/utils/classnames'
|
||||
import { StopCircle } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
|
||||
import { audioToText } from '@/service/share'
|
||||
import { AppSourceType, audioToText } from '@/service/share'
|
||||
|
||||
type VoiceInputTypes = {
|
||||
onConverted: (text: string) => void
|
||||
@@ -108,7 +108,7 @@ const VoiceInput = ({
|
||||
}
|
||||
|
||||
try {
|
||||
const audioResponse = await audioToText(url, isPublic, formData)
|
||||
const audioResponse = await audioToText(url, isPublic ? AppSourceType.webApp : AppSourceType.installedApp, formData)
|
||||
onConverted(audioResponse.text)
|
||||
onCancel()
|
||||
}
|
||||
|
||||
@@ -6,7 +6,13 @@ import cn from '@/utils/classnames'
|
||||
import type { App } from '@/models/explore'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { AppTypeIcon } from '../../app/type-selector'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { RiInformation2Line } from '@remixicon/react'
|
||||
import { useCallback } from 'react'
|
||||
import ExploreContext from '@/context/explore-context'
|
||||
import { useContextSelector } from 'use-context-selector'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
|
||||
export type AppCardProps = {
|
||||
app: App
|
||||
canCreate: boolean
|
||||
@@ -22,8 +28,17 @@ const AppCard = ({
|
||||
}: AppCardProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { app: appBasicInfo } = app
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
const isTrialApp = app.can_trial && systemFeatures.enable_trial_app
|
||||
const setShowTryAppPanel = useContextSelector(ExploreContext, ctx => ctx.setShowTryAppPanel)
|
||||
const showTryAPPPanel = useCallback((appId: string) => {
|
||||
return () => {
|
||||
setShowTryAppPanel?.(true, { appId, app })
|
||||
}
|
||||
}, [setShowTryAppPanel, app.category])
|
||||
|
||||
return (
|
||||
<div className={cn('group relative col-span-1 flex cursor-pointer flex-col overflow-hidden rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg pb-2 shadow-sm transition-all duration-200 ease-in-out hover:shadow-lg')}>
|
||||
<div className={cn('group relative col-span-1 flex cursor-pointer flex-col overflow-hidden rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg pb-2 shadow-sm transition-all duration-200 ease-in-out hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-lg')}>
|
||||
<div className='flex h-[66px] shrink-0 grow-0 items-center gap-3 px-[14px] pb-3 pt-[14px]'>
|
||||
<div className='relative shrink-0'>
|
||||
<AppIcon
|
||||
@@ -54,14 +69,23 @@ const AppCard = ({
|
||||
{app.description}
|
||||
</div>
|
||||
</div>
|
||||
{isExplore && canCreate && (
|
||||
<div className={cn('absolute bottom-0 left-0 right-0 hidden bg-gradient-to-t from-components-panel-gradient-2 from-[60.27%] to-transparent p-4 pt-8 group-hover:flex')}>
|
||||
<div className={cn('flex h-8 w-full items-center space-x-2')}>
|
||||
<Button variant='primary' className='h-7 grow' onClick={() => onCreate()}>
|
||||
{isExplore && (canCreate || isTrialApp) && (
|
||||
<div className={cn(
|
||||
'absolute bottom-0 left-0 right-0 hidden bg-gradient-to-t from-components-panel-gradient-2 from-[60.27%] to-transparent p-4 pt-8',
|
||||
(canCreate && isTrialApp) && 'grid-cols-2 gap-2 group-hover:grid',
|
||||
)}>
|
||||
{canCreate && (
|
||||
<Button variant='primary' className='h-7' onClick={() => onCreate()}>
|
||||
<PlusIcon className='mr-1 h-4 w-4' />
|
||||
<span className='text-xs'>{t('explore.appCard.addToWorkspace')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{isTrialApp && (
|
||||
<Button className='w-full' onClick={showTryAPPPanel(app.app_id)}>
|
||||
<RiInformation2Line className='mr-1 size-4' />
|
||||
<span>{t('explore.appCard.try')}</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,11 @@ import {
|
||||
} from '@/models/app'
|
||||
import { useImportDSL } from '@/hooks/use-import-dsl'
|
||||
import DSLConfirmModal from '@/app/components/app/create-from-dsl-modal/dsl-confirm-modal'
|
||||
import Banner from '@/app/components/explore/banner/banner'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { useContextSelector } from 'use-context-selector'
|
||||
import TryApp from '../try-app'
|
||||
|
||||
type AppsProps = {
|
||||
onSuccess?: () => void
|
||||
@@ -36,12 +41,19 @@ const Apps = ({
|
||||
onSuccess,
|
||||
}: AppsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
const { hasEditPermission } = useContext(ExploreContext)
|
||||
const allCategoriesEn = t('explore.apps.allCategories', { lng: 'en' })
|
||||
|
||||
const [keywords, setKeywords] = useState('')
|
||||
const [searchKeywords, setSearchKeywords] = useState('')
|
||||
|
||||
const hasFilterCondition = !!keywords
|
||||
const handleResetFilter = useCallback(() => {
|
||||
setKeywords('')
|
||||
setSearchKeywords('')
|
||||
}, [])
|
||||
|
||||
const { run: handleSearch } = useDebounceFn(() => {
|
||||
setSearchKeywords(keywords)
|
||||
}, { wait: 500 })
|
||||
@@ -96,6 +108,18 @@ const Apps = ({
|
||||
isFetching,
|
||||
} = useImportDSL()
|
||||
const [showDSLConfirmModal, setShowDSLConfirmModal] = useState(false)
|
||||
|
||||
const isShowTryAppPanel = useContextSelector(ExploreContext, ctx => ctx.isShowTryAppPanel)
|
||||
const setShowTryAppPanel = useContextSelector(ExploreContext, ctx => ctx.setShowTryAppPanel)
|
||||
const hideTryAppPanel = useCallback(() => {
|
||||
setShowTryAppPanel(false)
|
||||
}, [setShowTryAppPanel])
|
||||
const appParams = useContextSelector(ExploreContext, ctx => ctx.currentApp)
|
||||
const handleShowFromTryApp = useCallback(() => {
|
||||
setCurrApp(appParams?.app || null)
|
||||
setIsShowCreateModal(true)
|
||||
}, [appParams?.app])
|
||||
|
||||
const onCreate: CreateAppModalProps['onConfirm'] = async ({
|
||||
name,
|
||||
icon_type,
|
||||
@@ -103,6 +127,8 @@ const Apps = ({
|
||||
icon_background,
|
||||
description,
|
||||
}) => {
|
||||
hideTryAppPanel()
|
||||
|
||||
const { export_data } = await fetchAppDetail(
|
||||
currApp?.app.id as string,
|
||||
)
|
||||
@@ -141,23 +167,25 @@ const Apps = ({
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'flex h-full flex-col border-l-[0.5px] border-divider-regular',
|
||||
'flex h-full flex-col',
|
||||
)}>
|
||||
|
||||
<div className='shrink-0 px-12 pt-6'>
|
||||
<div className={`mb-1 ${s.textGradient} text-xl font-semibold`}>{t('explore.apps.title')}</div>
|
||||
<div className='text-sm text-text-tertiary'>{t('explore.apps.description')}</div>
|
||||
</div>
|
||||
|
||||
{systemFeatures.enable_explore_banner && (
|
||||
<div className='mt-4 px-12'>
|
||||
<Banner />
|
||||
</div>
|
||||
)}
|
||||
<div className={cn(
|
||||
'mt-6 flex items-center justify-between px-12',
|
||||
)}>
|
||||
<Category
|
||||
list={categories}
|
||||
value={currCategory}
|
||||
onChange={setCurrCategory}
|
||||
allCategoriesEn={allCategoriesEn}
|
||||
/>
|
||||
<div className='flex items-center'>
|
||||
<div className={'system-xl-semibold grow truncate text-text-primary'}>{!hasFilterCondition ? t('explore.apps.title') : t('explore.apps.resultNum', { num: searchFilteredList.length })}</div>
|
||||
{hasFilterCondition && (
|
||||
<>
|
||||
<div className='mx-3 h-4 w-px bg-divider-regular'></div>
|
||||
<Button size='medium' onClick={handleResetFilter}>{t('explore.apps.resetFilter')}</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Input
|
||||
showLeftIcon
|
||||
showClearIcon
|
||||
@@ -168,6 +196,15 @@ const Apps = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-2 px-12'>
|
||||
<Category
|
||||
list={categories}
|
||||
value={currCategory}
|
||||
onChange={setCurrCategory}
|
||||
allCategoriesEn={allCategoriesEn}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={cn(
|
||||
'relative mt-4 flex flex-1 shrink-0 grow flex-col overflow-auto pb-6',
|
||||
)}>
|
||||
@@ -214,6 +251,14 @@ const Apps = ({
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
{isShowTryAppPanel && (
|
||||
<TryApp appId={appParams?.appId || ''}
|
||||
category={appParams?.app?.category}
|
||||
onClose={hideTryAppPanel}
|
||||
onCreate={handleShowFromTryApp}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
196
web/app/components/explore/banner/banner-item.tsx
Normal file
196
web/app/components/explore/banner/banner-item.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
import type { FC } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { RiArrowRightLine } from '@remixicon/react'
|
||||
import { useCarousel } from '@/app/components/base/carousel'
|
||||
import { IndicatorButton } from './indicator-button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
export type BannerData = {
|
||||
id: string
|
||||
content: {
|
||||
'category': string
|
||||
'title': string
|
||||
'description': string
|
||||
'img-src': string
|
||||
}
|
||||
status: 'enabled' | 'disabled'
|
||||
link: string
|
||||
created_at: number
|
||||
}
|
||||
|
||||
type BannerItemProps = {
|
||||
banner: BannerData
|
||||
autoplayDelay: number
|
||||
isPaused?: boolean
|
||||
}
|
||||
|
||||
const RESPONSIVE_BREAKPOINT = 1200
|
||||
const MAX_RESPONSIVE_WIDTH = 600
|
||||
const INDICATOR_WIDTH = 20
|
||||
const INDICATOR_GAP = 8
|
||||
const MIN_VIEW_MORE_WIDTH = 480
|
||||
|
||||
export const BannerItem: FC<BannerItemProps> = ({ banner, autoplayDelay, isPaused = false }) => {
|
||||
const { t } = useTranslation()
|
||||
const { api, selectedIndex } = useCarousel()
|
||||
const { category, title, description, 'img-src': imgSrc } = banner.content
|
||||
|
||||
const [resetKey, setResetKey] = useState(0)
|
||||
const textAreaRef = useRef<HTMLDivElement>(null)
|
||||
const [maxWidth, setMaxWidth] = useState<number | undefined>(undefined)
|
||||
|
||||
const slideInfo = useMemo(() => {
|
||||
const slides = api?.slideNodes() ?? []
|
||||
const totalSlides = slides.length
|
||||
const nextIndex = totalSlides > 0 ? (selectedIndex + 1) % totalSlides : 0
|
||||
return { slides, totalSlides, nextIndex }
|
||||
}, [api, selectedIndex])
|
||||
|
||||
const indicatorsWidth = useMemo(() => {
|
||||
const count = slideInfo.totalSlides
|
||||
if (count === 0) return 0
|
||||
// Calculate: indicator buttons + gaps + extra spacing (3 * 20px for divider and padding)
|
||||
return (count + 2) * INDICATOR_WIDTH + (count - 1) * INDICATOR_GAP
|
||||
}, [slideInfo.totalSlides])
|
||||
|
||||
const viewMoreStyle = useMemo(() => {
|
||||
if (!maxWidth) return undefined
|
||||
return {
|
||||
maxWidth: `${maxWidth}px`,
|
||||
minWidth: indicatorsWidth ? `${Math.min(maxWidth - indicatorsWidth, MIN_VIEW_MORE_WIDTH)}px` : undefined,
|
||||
}
|
||||
}, [maxWidth, indicatorsWidth])
|
||||
|
||||
const responsiveStyle = useMemo(
|
||||
() => (maxWidth !== undefined ? { maxWidth: `${maxWidth}px` } : undefined),
|
||||
[maxWidth],
|
||||
)
|
||||
|
||||
const incrementResetKey = useCallback(() => setResetKey(prev => prev + 1), [])
|
||||
|
||||
useEffect(() => {
|
||||
const updateMaxWidth = () => {
|
||||
if (window.innerWidth < RESPONSIVE_BREAKPOINT && textAreaRef.current) {
|
||||
const textAreaWidth = textAreaRef.current.offsetWidth
|
||||
setMaxWidth(Math.min(textAreaWidth, MAX_RESPONSIVE_WIDTH))
|
||||
}
|
||||
else {
|
||||
setMaxWidth(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
updateMaxWidth()
|
||||
|
||||
const resizeObserver = new ResizeObserver(updateMaxWidth)
|
||||
if (textAreaRef.current)
|
||||
resizeObserver.observe(textAreaRef.current)
|
||||
|
||||
window.addEventListener('resize', updateMaxWidth)
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect()
|
||||
window.removeEventListener('resize', updateMaxWidth)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
incrementResetKey()
|
||||
}, [selectedIndex, incrementResetKey])
|
||||
|
||||
const handleBannerClick = useCallback(() => {
|
||||
incrementResetKey()
|
||||
if (banner.link)
|
||||
window.open(banner.link, '_blank', 'noopener,noreferrer')
|
||||
}, [banner.link, incrementResetKey])
|
||||
|
||||
const handleIndicatorClick = useCallback((index: number) => {
|
||||
incrementResetKey()
|
||||
api?.scrollTo(index)
|
||||
}, [api, incrementResetKey])
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative flex w-full min-w-[784px] cursor-pointer overflow-hidden rounded-2xl bg-components-panel-on-panel-item-bg pr-[288px] transition-shadow hover:shadow-md"
|
||||
onClick={handleBannerClick}
|
||||
>
|
||||
{/* Left content area */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex h-full flex-col gap-3 py-6 pl-8 pr-0">
|
||||
{/* Text section */}
|
||||
<div className="flex min-h-24 flex-wrap items-end gap-1 py-1">
|
||||
{/* Title area */}
|
||||
<div
|
||||
ref={textAreaRef}
|
||||
className="flex min-w-[480px] max-w-[680px] flex-[1_0_0] flex-col pr-4"
|
||||
style={responsiveStyle}
|
||||
>
|
||||
<p className="title-4xl-semi-bold line-clamp-1 text-dify-logo-dify-logo-blue">
|
||||
{category}
|
||||
</p>
|
||||
<p className="title-4xl-semi-bold line-clamp-2 text-dify-logo-dify-logo-black">
|
||||
{title}
|
||||
</p>
|
||||
</div>
|
||||
{/* Description area */}
|
||||
<div
|
||||
className="min-w-60 max-w-[600px] flex-[1_0_0] self-end overflow-hidden py-1 pr-4"
|
||||
style={responsiveStyle}
|
||||
>
|
||||
<p className="body-sm-regular line-clamp-4 overflow-hidden text-text-tertiary">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions section */}
|
||||
<div className="flex items-center gap-1">
|
||||
{/* View more button */}
|
||||
<div
|
||||
className="flex min-w-[480px] max-w-[680px] flex-[1_0_0] items-center gap-[6px] py-1 pr-8"
|
||||
style={viewMoreStyle}
|
||||
>
|
||||
<div className="flex h-4 w-4 items-center justify-center rounded-full bg-text-accent p-[2px]">
|
||||
<RiArrowRightLine className="h-3 w-3 text-text-primary-on-surface" />
|
||||
</div>
|
||||
<span className="system-sm-semibold-uppercase text-text-accent">
|
||||
{t('explore.banner.viewMore')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn('flex max-w-[600px] flex-[1_0_0] items-center gap-2 py-1 pr-10', maxWidth ? '' : 'min-w-60')}
|
||||
style={responsiveStyle}
|
||||
>
|
||||
{/* Slide navigation indicators */}
|
||||
<div className="flex items-center gap-2">
|
||||
{slideInfo.slides.map((_: unknown, index: number) => (
|
||||
<IndicatorButton
|
||||
key={index}
|
||||
index={index}
|
||||
selectedIndex={selectedIndex}
|
||||
isNextSlide={index === slideInfo.nextIndex}
|
||||
autoplayDelay={autoplayDelay}
|
||||
resetKey={resetKey}
|
||||
isPaused={isPaused}
|
||||
onClick={() => handleIndicatorClick(index)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="hidden h-[1px] flex-1 bg-divider-regular min-[1380px]:block" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right image area */}
|
||||
<div className="absolute right-0 top-0 flex h-full items-center p-2">
|
||||
<img
|
||||
src={imgSrc}
|
||||
alt={title}
|
||||
className="aspect-[4/3] h-full max-w-[296px] rounded-xl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
93
web/app/components/explore/banner/banner.tsx
Normal file
93
web/app/components/explore/banner/banner.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Carousel } from '@/app/components/base/carousel'
|
||||
import { useGetBanners } from '@/service/use-explore'
|
||||
import Loading from '../../base/loading'
|
||||
import { type BannerData, BannerItem } from './banner-item'
|
||||
import { useI18N } from '@/context/i18n'
|
||||
|
||||
const AUTOPLAY_DELAY = 5000
|
||||
const MIN_LOADING_HEIGHT = 168
|
||||
const RESIZE_DEBOUNCE_DELAY = 50
|
||||
|
||||
const LoadingState: FC = () => (
|
||||
<div
|
||||
className="flex items-center justify-center rounded-2xl bg-components-panel-on-panel-item-bg shadow-md"
|
||||
style={{ minHeight: MIN_LOADING_HEIGHT }}
|
||||
>
|
||||
<Loading />
|
||||
</div>
|
||||
)
|
||||
|
||||
const Banner: FC = () => {
|
||||
const { locale } = useI18N()
|
||||
const { data: banners, isLoading, isError } = useGetBanners(locale)
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const [isResizing, setIsResizing] = useState(false)
|
||||
const resizeTimerRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
const enabledBanners = useMemo(
|
||||
() => banners?.filter((banner: BannerData) => banner.status === 'enabled') ?? [],
|
||||
[banners],
|
||||
)
|
||||
|
||||
const isPaused = isHovered || isResizing
|
||||
|
||||
// Handle window resize to pause animation
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setIsResizing(true)
|
||||
|
||||
if (resizeTimerRef.current)
|
||||
clearTimeout(resizeTimerRef.current)
|
||||
|
||||
resizeTimerRef.current = setTimeout(() => {
|
||||
setIsResizing(false)
|
||||
}, RESIZE_DEBOUNCE_DELAY)
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
if (resizeTimerRef.current)
|
||||
clearTimeout(resizeTimerRef.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (isLoading)
|
||||
return <LoadingState />
|
||||
|
||||
if (isError || enabledBanners.length === 0)
|
||||
return null
|
||||
|
||||
return (
|
||||
<Carousel
|
||||
opts={{ loop: true }}
|
||||
plugins={[
|
||||
Carousel.Plugin.Autoplay({
|
||||
delay: AUTOPLAY_DELAY,
|
||||
stopOnInteraction: false,
|
||||
stopOnMouseEnter: true,
|
||||
}),
|
||||
]}
|
||||
className="rounded-2xl"
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<Carousel.Content>
|
||||
{enabledBanners.map((banner: BannerData) => (
|
||||
<Carousel.Item key={banner.id}>
|
||||
<BannerItem
|
||||
banner={banner}
|
||||
autoplayDelay={AUTOPLAY_DELAY}
|
||||
isPaused={isPaused}
|
||||
/>
|
||||
</Carousel.Item>
|
||||
))}
|
||||
</Carousel.Content>
|
||||
</Carousel>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Banner)
|
||||
111
web/app/components/explore/banner/indicator-button.tsx
Normal file
111
web/app/components/explore/banner/indicator-button.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import type { FC } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type IndicatorButtonProps = {
|
||||
index: number
|
||||
selectedIndex: number
|
||||
isNextSlide: boolean
|
||||
autoplayDelay: number
|
||||
resetKey: number
|
||||
isPaused?: boolean
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
const PROGRESS_MAX = 100
|
||||
const DEGREES_PER_PERCENT = 3.6
|
||||
|
||||
export const IndicatorButton: FC<IndicatorButtonProps> = ({
|
||||
index,
|
||||
selectedIndex,
|
||||
isNextSlide,
|
||||
autoplayDelay,
|
||||
resetKey,
|
||||
isPaused = false,
|
||||
onClick,
|
||||
}) => {
|
||||
const [progress, setProgress] = useState(0)
|
||||
const frameIdRef = useRef<number | undefined>(undefined)
|
||||
const startTimeRef = useRef(0)
|
||||
|
||||
const isActive = index === selectedIndex
|
||||
const shouldAnimate = !document.hidden && !isPaused
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNextSlide) {
|
||||
setProgress(0)
|
||||
if (frameIdRef.current)
|
||||
cancelAnimationFrame(frameIdRef.current)
|
||||
return
|
||||
}
|
||||
|
||||
setProgress(0)
|
||||
startTimeRef.current = Date.now()
|
||||
|
||||
const animate = () => {
|
||||
if (!document.hidden && !isPaused) {
|
||||
const elapsed = Date.now() - startTimeRef.current
|
||||
const newProgress = Math.min((elapsed / autoplayDelay) * PROGRESS_MAX, PROGRESS_MAX)
|
||||
setProgress(newProgress)
|
||||
|
||||
if (newProgress < PROGRESS_MAX)
|
||||
frameIdRef.current = requestAnimationFrame(animate)
|
||||
}
|
||||
else {
|
||||
frameIdRef.current = requestAnimationFrame(animate)
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldAnimate)
|
||||
frameIdRef.current = requestAnimationFrame(animate)
|
||||
|
||||
return () => {
|
||||
if (frameIdRef.current)
|
||||
cancelAnimationFrame(frameIdRef.current)
|
||||
}
|
||||
}, [isNextSlide, autoplayDelay, resetKey, isPaused])
|
||||
|
||||
const handleClick = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
onClick()
|
||||
}, [onClick])
|
||||
|
||||
const progressDegrees = progress * DEGREES_PER_PERCENT
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className={cn(
|
||||
'system-2xs-semibold-uppercase relative flex h-[18px] w-[20px] items-center justify-center rounded-[7px] border border-divider-subtle p-[2px] text-center transition-colors',
|
||||
isActive
|
||||
? 'bg-text-primary text-components-panel-on-panel-item-bg'
|
||||
: 'bg-components-panel-on-panel-item-bg text-text-tertiary hover:text-text-secondary',
|
||||
)}
|
||||
>
|
||||
{/* progress border for next slide */}
|
||||
{isNextSlide && !isActive && (
|
||||
<span
|
||||
key={resetKey}
|
||||
className="absolute inset-[-1px] rounded-[7px]"
|
||||
style={{
|
||||
background: `conic-gradient(
|
||||
from 0deg,
|
||||
var(--color-text-primary) ${progressDegrees}deg,
|
||||
transparent ${progressDegrees}deg
|
||||
)`,
|
||||
WebkitMask: 'linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)',
|
||||
WebkitMaskComposite: 'xor',
|
||||
mask: 'linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)',
|
||||
maskComposite: 'exclude',
|
||||
padding: '1px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* number content */}
|
||||
<span className="relative z-10">
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import { useTranslation } from 'react-i18next'
|
||||
import cn from '@/utils/classnames'
|
||||
import exploreI18n from '@/i18n/en-US/explore'
|
||||
import type { AppCategory } from '@/models/explore'
|
||||
import { ThumbsUp } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
|
||||
|
||||
const categoryI18n = exploreI18n.category
|
||||
|
||||
@@ -31,7 +30,7 @@ const Category: FC<ICategoryProps> = ({
|
||||
const isAllCategories = !list.includes(value as AppCategory) || value === allCategoriesEn
|
||||
|
||||
const itemClassName = (isSelected: boolean) => cn(
|
||||
'flex h-[32px] cursor-pointer items-center rounded-lg border-[0.5px] border-transparent px-3 py-[7px] font-medium leading-[18px] text-text-tertiary hover:bg-components-main-nav-nav-button-bg-active',
|
||||
'system-sm-medium flex h-7 cursor-pointer items-center rounded-lg border border-transparent px-3 text-text-tertiary hover:bg-components-main-nav-nav-button-bg-active',
|
||||
isSelected && 'border-components-main-nav-nav-button-border bg-components-main-nav-nav-button-bg-active text-components-main-nav-nav-button-text-active shadow-xs',
|
||||
)
|
||||
|
||||
@@ -41,7 +40,6 @@ const Category: FC<ICategoryProps> = ({
|
||||
className={itemClassName(isAllCategories)}
|
||||
onClick={() => onChange(allCategoriesEn)}
|
||||
>
|
||||
<ThumbsUp className='mr-1 h-3.5 w-3.5' />
|
||||
{t('explore.apps.allCategories')}
|
||||
</div>
|
||||
{list.filter(name => name !== allCategoriesEn).map(name => (
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import type { CurrentTryAppParams } from '@/context/explore-context'
|
||||
import ExploreContext from '@/context/explore-context'
|
||||
import Sidebar from '@/app/components/explore/sidebar'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
@@ -42,6 +43,16 @@ const Explore: FC<IExploreProps> = ({
|
||||
return router.replace('/datasets')
|
||||
}, [isCurrentWorkspaceDatasetOperator])
|
||||
|
||||
const [currentTryAppParams, setCurrentTryAppParams] = useState<CurrentTryAppParams | undefined>(undefined)
|
||||
const [isShowTryAppPanel, setIsShowTryAppPanel] = useState(false)
|
||||
const setShowTryAppPanel = (showTryAppPanel: boolean, params?: CurrentTryAppParams) => {
|
||||
if (showTryAppPanel)
|
||||
setCurrentTryAppParams(params)
|
||||
else
|
||||
setCurrentTryAppParams(undefined)
|
||||
setIsShowTryAppPanel(showTryAppPanel)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex h-full overflow-hidden border-t border-divider-regular bg-background-body'>
|
||||
<ExploreContext.Provider
|
||||
@@ -54,6 +65,9 @@ const Explore: FC<IExploreProps> = ({
|
||||
setInstalledApps,
|
||||
isFetchingInstalledApps,
|
||||
setIsFetchingInstalledApps,
|
||||
currentApp: currentTryAppParams,
|
||||
isShowTryAppPanel,
|
||||
setShowTryAppPanel,
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
@@ -12,6 +12,7 @@ import AppUnavailable from '../../base/app-unavailable'
|
||||
import { useGetUserCanAccessApp } from '@/service/access-control'
|
||||
import { useGetInstalledAppAccessModeByAppId, useGetInstalledAppMeta, useGetInstalledAppParams } from '@/service/use-explore'
|
||||
import type { AppData } from '@/models/share'
|
||||
import type { AccessMode } from '@/models/access-control'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
|
||||
export type IInstalledAppProps = {
|
||||
@@ -62,8 +63,8 @@ const InstalledApp: FC<IInstalledAppProps> = ({
|
||||
if (appMeta)
|
||||
updateWebAppMeta(appMeta)
|
||||
if (webAppAccessMode)
|
||||
updateWebAppAccessMode(webAppAccessMode.accessMode)
|
||||
updateUserCanAccessApp(Boolean(userCanAccessApp && userCanAccessApp?.result))
|
||||
updateWebAppAccessMode((webAppAccessMode as { accessMode: AccessMode }).accessMode)
|
||||
updateUserCanAccessApp(Boolean(userCanAccessApp && (userCanAccessApp as { result: boolean })?.result))
|
||||
}, [installedApp, appMeta, appParams, updateAppInfo, updateAppParams, updateUserCanAccessApp, updateWebAppMeta, userCanAccessApp, webAppAccessMode, updateWebAppAccessMode])
|
||||
|
||||
if (appParamsError) {
|
||||
|
||||
@@ -52,7 +52,7 @@ const ItemOperation: FC<IItemOperationProps> = ({
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={() => setOpen(v => !v)}
|
||||
>
|
||||
<div className={cn(className, s.btn, 'h-6 w-6 rounded-md border-none py-1', (isItemHovering || open) && `${s.open} !bg-components-actionbar-bg !shadow-none`)}></div>
|
||||
<div className={cn(className, s.btn, 'h-6 w-6 rounded-md border-none py-1', (isItemHovering || open) && `${s.open}`)}></div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent
|
||||
className="z-50"
|
||||
|
||||
@@ -57,7 +57,7 @@ export default function AppNavItem({
|
||||
<>
|
||||
<div className='flex w-0 grow items-center space-x-2'>
|
||||
<AppIcon size='tiny' iconType={icon_type} icon={icon} background={icon_background} imageUrl={icon_url} />
|
||||
<div className='overflow-hidden text-ellipsis whitespace-nowrap' title={name}>{name}</div>
|
||||
<div className='system-sm-regular truncate text-components-menu-item-text' title={name}>{name}</div>
|
||||
</div>
|
||||
<div className='h-6 shrink-0' onClick={e => e.stopPropagation()}>
|
||||
<ItemOperation
|
||||
|
||||
@@ -13,18 +13,9 @@ import Confirm from '@/app/components/base/confirm'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import { useGetInstalledApps, useUninstallApp, useUpdateAppPinStatus } from '@/service/use-explore'
|
||||
|
||||
const SelectedDiscoveryIcon = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="current" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M13.4135 1.11725C13.5091 1.09983 13.6483 1.08355 13.8078 1.11745C14.0143 1.16136 14.2017 1.26953 14.343 1.42647C14.4521 1.54766 14.5076 1.67634 14.5403 1.76781C14.5685 1.84673 14.593 1.93833 14.6136 2.01504L15.5533 5.5222C15.5739 5.5989 15.5985 5.69049 15.6135 5.77296C15.6309 5.86852 15.6472 6.00771 15.6133 6.16722C15.5694 6.37378 15.4612 6.56114 15.3043 6.70245C15.1831 6.81157 15.0544 6.86706 14.9629 6.89975C14.884 6.92796 14.7924 6.95247 14.7157 6.97299L14.676 6.98364C14.3365 7.07461 14.0437 7.15309 13.7972 7.19802C13.537 7.24543 13.2715 7.26736 12.9946 7.20849C12.7513 7.15677 12.5213 7.06047 12.3156 6.92591L9.63273 7.64477C9.86399 7.97104 9.99992 8.36965 9.99992 8.80001C9.99992 9.2424 9.85628 9.65124 9.6131 9.98245L12.5508 14.291C12.7582 14.5952 12.6797 15.01 12.3755 15.2174C12.0713 15.4248 11.6566 15.3464 11.4492 15.0422L8.51171 10.7339C8.34835 10.777 8.17682 10.8 7.99992 10.8C7.82305 10.8 7.65155 10.777 7.48823 10.734L4.5508 15.0422C4.34338 15.3464 3.92863 15.4248 3.62442 15.2174C3.32021 15.01 3.24175 14.5952 3.44916 14.291L6.3868 9.98254C6.14358 9.65132 5.99992 9.24244 5.99992 8.80001C5.99992 8.73795 6.00274 8.67655 6.00827 8.61594L4.59643 8.99424C4.51973 9.01483 4.42813 9.03941 4.34567 9.05444C4.25011 9.07185 4.11092 9.08814 3.95141 9.05423C3.74485 9.01033 3.55748 8.90215 3.41618 8.74522C3.38535 8.71097 3.3588 8.67614 3.33583 8.64171L2.49206 8.8678C2.41536 8.88838 2.32376 8.91296 2.2413 8.92799C2.14574 8.94541 2.00655 8.96169 1.84704 8.92779C1.64048 8.88388 1.45311 8.77571 1.31181 8.61877C1.20269 8.49759 1.1472 8.3689 1.1145 8.27744C1.08629 8.1985 1.06177 8.10689 1.04125 8.03018L0.791701 7.09885C0.771119 7.02215 0.746538 6.93055 0.731508 6.84809C0.714092 6.75253 0.697808 6.61334 0.731712 6.45383C0.775619 6.24726 0.883793 6.0599 1.04073 5.9186C1.16191 5.80948 1.2906 5.75399 1.38206 5.72129C1.461 5.69307 1.55261 5.66856 1.62932 5.64804L2.47318 5.42193C2.47586 5.38071 2.48143 5.33735 2.49099 5.29237C2.5349 5.08581 2.64307 4.89844 2.80001 4.75714C2.92119 4.64802 3.04988 4.59253 3.14134 4.55983C3.22027 4.53162 3.31189 4.50711 3.3886 4.48658L11.1078 2.41824C11.2186 2.19888 11.3697 2.00049 11.5545 1.83406C11.7649 1.64462 12.0058 1.53085 12.2548 1.44183C12.4907 1.35749 12.7836 1.27904 13.123 1.18809L13.1628 1.17744C13.2395 1.15686 13.3311 1.13228 13.4135 1.11725ZM13.3642 2.5039C13.0648 2.58443 12.8606 2.64126 12.7036 2.69735C12.5325 2.75852 12.4742 2.80016 12.4467 2.82492C12.3421 2.91912 12.2699 3.04403 12.2407 3.18174C12.233 3.21793 12.2261 3.28928 12.2587 3.46805C12.2927 3.6545 12.3564 3.89436 12.4559 4.26563L12.5594 4.652C12.6589 5.02328 12.7236 5.26287 12.7874 5.44133C12.8486 5.61244 12.8902 5.67079 12.915 5.69829C13.0092 5.80291 13.1341 5.87503 13.2718 5.9043C13.308 5.91199 13.3793 5.91887 13.5581 5.88629C13.7221 5.85641 13.9273 5.80352 14.2269 5.72356L13.3642 2.5039Z" fill="currentColor" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const DiscoveryIcon = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="current" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.74786 9.89676L12.0003 14.6669M7.25269 9.89676L4.00027 14.6669M9.3336 8.80031C9.3336 9.53669 8.73665 10.1336 8.00027 10.1336C7.26389 10.1336 6.66694 9.53669 6.66694 8.80031C6.66694 8.06393 7.26389 7.46698 8.00027 7.46698C8.73665 7.46698 9.3336 8.06393 9.3336 8.80031ZM11.4326 3.02182L3.57641 5.12689C3.39609 5.1752 3.30593 5.19936 3.24646 5.25291C3.19415 5.30001 3.15809 5.36247 3.14345 5.43132C3.12681 5.5096 3.15097 5.59976 3.19929 5.78008L3.78595 7.96951C3.83426 8.14984 3.85842 8.24 3.91197 8.29947C3.95907 8.35178 4.02153 8.38784 4.09038 8.40248C4.16866 8.41911 4.25882 8.39496 4.43914 8.34664L12.2953 6.24158L11.4326 3.02182ZM14.5285 6.33338C13.8072 6.52665 13.4466 6.62328 13.1335 6.55673C12.8581 6.49819 12.6082 6.35396 12.4198 6.14471C12.2056 5.90682 12.109 5.54618 11.9157 4.82489L11.8122 4.43852C11.6189 3.71722 11.5223 3.35658 11.5889 3.04347C11.6474 2.76805 11.7916 2.51823 12.0009 2.32982C12.2388 2.11563 12.5994 2.019 13.3207 1.82573C13.501 1.77741 13.5912 1.75325 13.6695 1.76989C13.7383 1.78452 13.8008 1.82058 13.8479 1.87289C13.9014 1.93237 13.9256 2.02253 13.9739 2.20285L14.9057 5.68018C14.954 5.86051 14.9781 5.95067 14.9615 6.02894C14.9469 6.0978 14.9108 6.16025 14.8585 6.20736C14.799 6.2609 14.7088 6.28506 14.5285 6.33338ZM2.33475 8.22033L3.23628 7.97876C3.4166 7.93044 3.50676 7.90628 3.56623 7.85274C3.61854 7.80563 3.6546 7.74318 3.66924 7.67433C3.68588 7.59605 3.66172 7.50589 3.6134 7.32556L3.37184 6.42403C3.32352 6.24371 3.29936 6.15355 3.24581 6.09408C3.19871 6.04176 3.13626 6.00571 3.0674 5.99107C2.98912 5.97443 2.89896 5.99859 2.71864 6.04691L1.81711 6.28847C1.63678 6.33679 1.54662 6.36095 1.48715 6.4145C1.43484 6.4616 1.39878 6.52405 1.38415 6.59291C1.36751 6.67119 1.39167 6.76135 1.43998 6.94167L1.68155 7.8432C1.72987 8.02352 1.75402 8.11369 1.80757 8.17316C1.85467 8.22547 1.91713 8.26153 1.98598 8.27616C2.06426 8.2928 2.15442 8.26864 2.33475 8.22033Z" stroke="currentColor" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
import { RiAppsFill, RiExpandRightLine, RiLayoutLeft2Line } from '@remixicon/react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import NoApps from './no-apps'
|
||||
|
||||
export type IExploreSideBarProps = {
|
||||
controlUpdateInstalledApps: number
|
||||
@@ -44,6 +35,9 @@ const SideBar: FC<IExploreSideBarProps> = ({
|
||||
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
const [isFold, {
|
||||
toggle: toggleIsFold,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const [showConfirm, setShowConfirm] = useState(false)
|
||||
const [currId, setCurrId] = useState('')
|
||||
@@ -83,22 +77,28 @@ const SideBar: FC<IExploreSideBarProps> = ({
|
||||
|
||||
const pinnedAppsCount = installedApps.filter(({ is_pinned }) => is_pinned).length
|
||||
return (
|
||||
<div className='w-fit shrink-0 cursor-pointer border-r border-divider-burn px-4 pt-6 sm:w-[216px]'>
|
||||
<div className={cn(isDiscoverySelected ? 'text-text-accent' : 'text-text-tertiary')}>
|
||||
<Link
|
||||
href='/explore/apps'
|
||||
className={cn(isDiscoverySelected ? ' bg-components-main-nav-nav-button-bg-active' : 'font-medium hover:bg-state-base-hover',
|
||||
'flex h-9 items-center gap-2 rounded-lg px-3 mobile:w-fit mobile:justify-center mobile:px-2 pc:w-full pc:justify-start')}
|
||||
style={isDiscoverySelected ? { boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)' } : {}}
|
||||
>
|
||||
{isDiscoverySelected ? <SelectedDiscoveryIcon /> : <DiscoveryIcon />}
|
||||
{!isMobile && <div className='text-sm'>{t('explore.sidebar.discovery')}</div>}
|
||||
</Link>
|
||||
</div>
|
||||
<div className={cn('relative w-fit shrink-0 cursor-pointer px-3 pt-6 sm:w-[240px]', isFold && 'sm:w-[56px]')}>
|
||||
<Link
|
||||
href='/explore/apps'
|
||||
className={cn(isDiscoverySelected ? 'bg-state-base-active' : 'hover:bg-state-base-hover',
|
||||
'flex h-8 items-center gap-2 rounded-lg px-1 mobile:w-fit mobile:justify-center pc:w-full pc:justify-start')}
|
||||
>
|
||||
<div className='flex size-6 shrink-0 items-center justify-center rounded-md bg-components-icon-bg-blue-solid'>
|
||||
<RiAppsFill className='size-3.5 text-components-avatar-shape-fill-stop-100' />
|
||||
</div>
|
||||
{!isMobile && !isFold && <div className={cn('truncate', isDiscoverySelected ? 'system-sm-semibold text-components-menu-item-text-active' : 'system-sm-regular text-components-menu-item-text')}>{t('explore.sidebar.title')}</div>}
|
||||
</Link>
|
||||
|
||||
{installedApps.length === 0 && !isMobile && !isFold
|
||||
&& <div className='mt-5'>
|
||||
<NoApps />
|
||||
</div>
|
||||
}
|
||||
|
||||
{installedApps.length > 0 && (
|
||||
<div className='mt-10'>
|
||||
<p className='break-all pl-2 text-xs font-medium uppercase text-text-tertiary mobile:px-0'>{t('explore.sidebar.workspace')}</p>
|
||||
<div className='mt-3 space-y-1 overflow-y-auto overflow-x-hidden'
|
||||
<div className='mt-5'>
|
||||
{!isMobile && !isFold && <p className='system-xs-medium-uppercase mb-1.5 break-all pl-2 uppercase text-text-tertiary mobile:px-0'>{t('explore.sidebar.webApps')}</p>}
|
||||
<div className='space-y-0.5 overflow-y-auto overflow-x-hidden'
|
||||
style={{
|
||||
height: 'calc(100vh - 250px)',
|
||||
}}
|
||||
@@ -106,7 +106,7 @@ const SideBar: FC<IExploreSideBarProps> = ({
|
||||
{installedApps.map(({ id, is_pinned, uninstallable, app: { name, icon_type, icon, icon_url, icon_background } }, index) => (
|
||||
<React.Fragment key={id}>
|
||||
<Item
|
||||
isMobile={isMobile}
|
||||
isMobile={isMobile || isFold}
|
||||
name={name}
|
||||
icon_type={icon_type}
|
||||
icon={icon}
|
||||
@@ -128,6 +128,15 @@ const SideBar: FC<IExploreSideBarProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isMobile && (
|
||||
<div className='absolute bottom-3 left-3 flex size-8 cursor-pointer items-center justify-center text-text-tertiary' onClick={toggleIsFold}>
|
||||
{isFold ? <RiExpandRightLine className='size-4.5' /> : (
|
||||
<RiLayoutLeft2Line className='size-4.5' />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showConfirm && (
|
||||
<Confirm
|
||||
title={t('explore.sidebar.delete.title')}
|
||||
|
||||
24
web/app/components/explore/sidebar/no-apps/index.tsx
Normal file
24
web/app/components/explore/sidebar/no-apps/index.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import s from './style.module.css'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import cn from '@/utils/classnames'
|
||||
import { Theme } from '@/types/app'
|
||||
|
||||
const i18nPrefix = 'explore.sidebar.noApps'
|
||||
|
||||
const NoApps: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
return (
|
||||
<div className='rounded-xl bg-background-default-subtle p-4'>
|
||||
<div className={cn('h-[35px] w-[86px] bg-contain bg-center bg-no-repeat', theme === Theme.dark ? s.dark : s.light)}></div>
|
||||
<div className='system-sm-semibold mt-2 text-text-secondary'>{t(`${i18nPrefix}.title`)}</div>
|
||||
<div className='system-xs-regular my-1 text-text-tertiary'>{t(`${i18nPrefix}.description`)}</div>
|
||||
<a className='system-xs-regular text-text-accent' target='_blank' rel='noopener noreferrer' href='https://docs.dify.ai/en/guides/application-publishing/launch-your-webapp-quickly/README'>{t(`${i18nPrefix}.learnMore`)}</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(NoApps)
|
||||
BIN
web/app/components/explore/sidebar/no-apps/no-web-apps-dark.png
Normal file
BIN
web/app/components/explore/sidebar/no-apps/no-web-apps-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
web/app/components/explore/sidebar/no-apps/no-web-apps-light.png
Normal file
BIN
web/app/components/explore/sidebar/no-apps/no-web-apps-light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@@ -0,0 +1,7 @@
|
||||
.light {
|
||||
background-image: url('./no-web-apps-light.png');
|
||||
}
|
||||
|
||||
.dark {
|
||||
background-image: url('./no-web-apps-dark.png');
|
||||
}
|
||||
92
web/app/components/explore/try-app/app-info/index.tsx
Normal file
92
web/app/components/explore/try-app/app-info/index.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { AppTypeIcon } from '@/app/components/app/type-selector'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { TryAppInfo } from '@/service/try-app'
|
||||
import cn from '@/utils/classnames'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { RiAddLine } from '@remixicon/react'
|
||||
import useGetRequirements from './use-get-requirements'
|
||||
|
||||
type Props = {
|
||||
appId: string
|
||||
appDetail: TryAppInfo
|
||||
category?: string
|
||||
className?: string
|
||||
onCreate: () => void
|
||||
}
|
||||
|
||||
const headerClassName = 'system-sm-semibold-uppercase text-text-secondary mb-3'
|
||||
|
||||
const AppInfo: FC<Props> = ({
|
||||
appId,
|
||||
className,
|
||||
category,
|
||||
appDetail,
|
||||
onCreate,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const mode = appDetail?.mode
|
||||
const { requirements } = useGetRequirements({ appDetail, appId })
|
||||
return (
|
||||
<div className={cn('flex h-full flex-col px-4 pt-2', className)}>
|
||||
{/* name and icon */}
|
||||
<div className='flex shrink-0 grow-0 items-center gap-3'>
|
||||
<div className='relative shrink-0'>
|
||||
<AppIcon
|
||||
size='large'
|
||||
iconType={appDetail.site.icon_type}
|
||||
icon={appDetail.site.icon}
|
||||
background={appDetail.site.icon_background}
|
||||
imageUrl={appDetail.site.icon_url}
|
||||
/>
|
||||
<AppTypeIcon wrapperClassName='absolute -bottom-0.5 -right-0.5 w-4 h-4 shadow-sm'
|
||||
className='h-3 w-3' type={mode} />
|
||||
</div>
|
||||
<div className='w-0 grow py-[1px]'>
|
||||
<div className='flex items-center text-sm font-semibold leading-5 text-text-secondary'>
|
||||
<div className='truncate' title={appDetail.name}>{appDetail.name}</div>
|
||||
</div>
|
||||
<div className='flex items-center text-[10px] font-medium leading-[18px] text-text-tertiary'>
|
||||
{mode === 'advanced-chat' && <div className='truncate'>{t('app.types.advanced').toUpperCase()}</div>}
|
||||
{mode === 'chat' && <div className='truncate'>{t('app.types.chatbot').toUpperCase()}</div>}
|
||||
{mode === 'agent-chat' && <div className='truncate'>{t('app.types.agent').toUpperCase()}</div>}
|
||||
{mode === 'workflow' && <div className='truncate'>{t('app.types.workflow').toUpperCase()}</div>}
|
||||
{mode === 'completion' && <div className='truncate'>{t('app.types.completion').toUpperCase()}</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{appDetail.description && (
|
||||
<div className='system-sm-regular mt-[14px] shrink-0 text-text-secondary'>{appDetail.description}</div>
|
||||
)}
|
||||
<Button variant='primary' className='mt-3 flex w-full max-w-full' onClick={onCreate}>
|
||||
<RiAddLine className='mr-1 size-4 shrink-0' />
|
||||
<span className='truncate'>{t('explore.tryApp.createFromSampleApp')}</span>
|
||||
</Button>
|
||||
|
||||
{category && (
|
||||
<div className='mt-6 shrink-0'>
|
||||
<div className={headerClassName}>{t('explore.tryApp.category')}</div>
|
||||
<div className='system-md-regular text-text-secondary'>{category}</div>
|
||||
</div>
|
||||
)}
|
||||
{requirements.length > 0 && (
|
||||
<div className='mt-5 grow overflow-y-auto'>
|
||||
<div className={headerClassName}>{t('explore.tryApp.requirements')}</div>
|
||||
<div className='space-y-0.5'>
|
||||
{requirements.map(item => (
|
||||
<div className='flex items-center space-x-2 py-1' key={item.name}>
|
||||
<div className='size-5 rounded-md bg-cover shadow-xs' style={{ backgroundImage: `url(${item.iconUrl})` }} />
|
||||
<div className='system-md-regular w-0 grow truncate text-text-secondary'>{item.name}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(AppInfo)
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types'
|
||||
import type { ToolNodeType } from '@/app/components/workflow/nodes/tool/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { MARKETPLACE_API_PREFIX } from '@/config'
|
||||
import type { TryAppInfo } from '@/service/try-app'
|
||||
import { useGetTryAppFlowPreview } from '@/service/use-try-app'
|
||||
import type { AgentTool } from '@/types/app'
|
||||
import { uniqBy } from 'lodash-es'
|
||||
|
||||
type Params = {
|
||||
appDetail: TryAppInfo
|
||||
appId: string
|
||||
}
|
||||
|
||||
type RequirementItem = {
|
||||
name: string
|
||||
iconUrl: string
|
||||
}
|
||||
const getIconUrl = (provider: string, tool: string) => {
|
||||
return `${MARKETPLACE_API_PREFIX}/plugins/${provider}/${tool}/icon`
|
||||
}
|
||||
|
||||
const useGetRequirements = ({ appDetail, appId }: Params) => {
|
||||
const isBasic = ['chat', 'completion', 'agent-chat'].includes(appDetail.mode)
|
||||
const isAgent = appDetail.mode === 'agent-chat'
|
||||
const isAdvanced = !isBasic
|
||||
const { data: flowData } = useGetTryAppFlowPreview(appId, isBasic)
|
||||
|
||||
const requirements: RequirementItem[] = []
|
||||
if(isBasic) {
|
||||
const modelProviderAndName = appDetail.model_config.model.provider.split('/')
|
||||
const name = appDetail.model_config.model.provider.split('/').pop() || ''
|
||||
requirements.push({
|
||||
name,
|
||||
iconUrl: getIconUrl(modelProviderAndName[0], modelProviderAndName[1]),
|
||||
})
|
||||
}
|
||||
if(isAgent) {
|
||||
requirements.push(...appDetail.model_config.agent_mode.tools.filter(data => (data as AgentTool).enabled).map((data) => {
|
||||
const tool = data as AgentTool
|
||||
const modelProviderAndName = tool.provider_id.split('/')
|
||||
return {
|
||||
name: tool.tool_label,
|
||||
iconUrl: getIconUrl(modelProviderAndName[0], modelProviderAndName[1]),
|
||||
}
|
||||
}))
|
||||
}
|
||||
if(isAdvanced && flowData && flowData?.graph?.nodes?.length > 0) {
|
||||
const nodes = flowData.graph.nodes
|
||||
const llmNodes = nodes.filter(node => node.data.type === BlockEnum.LLM)
|
||||
requirements.push(...llmNodes.map((node) => {
|
||||
const data = node.data as LLMNodeType
|
||||
const modelProviderAndName = data.model.provider.split('/')
|
||||
return {
|
||||
name: data.model.name,
|
||||
iconUrl: getIconUrl(modelProviderAndName[0], modelProviderAndName[1]),
|
||||
}
|
||||
}))
|
||||
|
||||
const toolNodes = nodes.filter(node => node.data.type === BlockEnum.Tool)
|
||||
requirements.push(...toolNodes.map((node) => {
|
||||
const data = node.data as ToolNodeType
|
||||
const toolProviderAndName = data.provider_id.split('/')
|
||||
return {
|
||||
name: data.tool_label,
|
||||
iconUrl: getIconUrl(toolProviderAndName[0], toolProviderAndName[1]),
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
const uniqueRequirements = uniqBy(requirements, 'name')
|
||||
|
||||
return {
|
||||
requirements: uniqueRequirements,
|
||||
}
|
||||
}
|
||||
|
||||
export default useGetRequirements
|
||||
99
web/app/components/explore/try-app/app/chat.tsx
Normal file
99
web/app/components/explore/try-app/app/chat.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import ChatWrapper from '@/app/components/base/chat/embedded-chatbot/chat-wrapper'
|
||||
import { useThemeContext } from '../../../base/chat/embedded-chatbot/theme/theme-context'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import {
|
||||
EmbeddedChatbotContext,
|
||||
} from '@/app/components/base/chat/embedded-chatbot/context'
|
||||
import {
|
||||
useEmbeddedChatbot,
|
||||
} from '@/app/components/base/chat/embedded-chatbot/hooks'
|
||||
import cn from '@/utils/classnames'
|
||||
import { AppSourceType } from '@/service/share'
|
||||
import Alert from '@/app/components/base/alert'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import type { TryAppInfo } from '@/service/try-app'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import { RiResetLeftLine } from '@remixicon/react'
|
||||
import ViewFormDropdown from '@/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown'
|
||||
|
||||
type Props = {
|
||||
appId: string
|
||||
appDetail: TryAppInfo
|
||||
className: string
|
||||
}
|
||||
|
||||
const TryApp: FC<Props> = ({
|
||||
appId,
|
||||
appDetail,
|
||||
className,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
const themeBuilder = useThemeContext()
|
||||
const { removeConversationIdInfo, ...chatData } = useEmbeddedChatbot(AppSourceType.tryApp, appId)
|
||||
const currentConversationId = chatData.currentConversationId
|
||||
const inputsForms = chatData.inputsForms
|
||||
useEffect(() => {
|
||||
if (appId)
|
||||
removeConversationIdInfo(appId)
|
||||
}, [appId])
|
||||
const [isHideTryNotice, {
|
||||
setTrue: hideTryNotice,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const handleNewConversation = () => {
|
||||
removeConversationIdInfo(appId)
|
||||
chatData.handleNewConversation()
|
||||
}
|
||||
return (
|
||||
<EmbeddedChatbotContext.Provider value={{
|
||||
...chatData,
|
||||
disableFeedback: true,
|
||||
isMobile,
|
||||
themeBuilder,
|
||||
} as any}>
|
||||
<div className={cn('flex h-full flex-col rounded-2xl bg-background-section-burn', className)}>
|
||||
<div className='flex shrink-0 justify-between p-3'>
|
||||
<div className='flex grow items-center space-x-2'>
|
||||
<AppIcon
|
||||
size='large'
|
||||
iconType={appDetail.site.icon_type}
|
||||
icon={appDetail.site.icon}
|
||||
background={appDetail.site.icon_background}
|
||||
imageUrl={appDetail.site.icon_url}
|
||||
/>
|
||||
<div className='system-md-semibold grow truncate text-text-primary' title={appDetail.name}>{appDetail.name}</div>
|
||||
</div>
|
||||
<div className='flex items-center gap-1'>
|
||||
{currentConversationId && (
|
||||
<Tooltip
|
||||
popupContent={t('share.chat.resetChat')}
|
||||
>
|
||||
<ActionButton size='l' onClick={handleNewConversation}>
|
||||
<RiResetLeftLine className='h-[18px] w-[18px]' />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{currentConversationId && inputsForms.length > 0 && (
|
||||
<ViewFormDropdown />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='mx-auto mt-4 flex h-[0] w-[769px] grow flex-col'>
|
||||
{!isHideTryNotice && (
|
||||
<Alert className='mb-4 shrink-0' message={t('explore.tryApp.tryInfo')} onHide={hideTryNotice} />
|
||||
)}
|
||||
<ChatWrapper />
|
||||
</div>
|
||||
</div>
|
||||
</EmbeddedChatbotContext.Provider>
|
||||
)
|
||||
}
|
||||
export default React.memo(TryApp)
|
||||
44
web/app/components/explore/try-app/app/index.tsx
Normal file
44
web/app/components/explore/try-app/app/index.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import Chat from './chat'
|
||||
import TextGeneration from './text-generation'
|
||||
import type { AppData } from '@/models/share'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import type { TryAppInfo } from '@/service/try-app'
|
||||
|
||||
type Props = {
|
||||
appId: string
|
||||
appDetail: TryAppInfo
|
||||
}
|
||||
|
||||
const TryApp: FC<Props> = ({
|
||||
appId,
|
||||
appDetail,
|
||||
}) => {
|
||||
const mode = appDetail?.mode
|
||||
const isChat = ['chat', 'advanced-chat', 'agent-chat'].includes(mode!)
|
||||
const isCompletion = !isChat
|
||||
|
||||
useDocumentTitle(appDetail?.site?.title || '')
|
||||
return (
|
||||
<div className='flex h-full w-full'>
|
||||
{isChat && (
|
||||
<Chat appId={appId} appDetail={appDetail} className='h-full grow' />
|
||||
)}
|
||||
{isCompletion && (
|
||||
<TextGeneration
|
||||
appId={appId}
|
||||
className='h-full grow'
|
||||
isWorkflow={mode === 'workflow'}
|
||||
appData={{
|
||||
app_id: appId,
|
||||
custom_config: {},
|
||||
...appDetail,
|
||||
} as AppData}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(TryApp)
|
||||
252
web/app/components/explore/try-app/app/text-generation.tsx
Normal file
252
web/app/components/explore/try-app/app/text-generation.tsx
Normal file
@@ -0,0 +1,252 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { appDefaultIconBackground } from '@/config'
|
||||
import RunOnce from '../../../share/text-generation/run-once'
|
||||
import { useWebAppStore } from '@/context/web-app-context'
|
||||
import type { AppData, SiteInfo } from '@/models/share'
|
||||
import { useGetTryAppParams } from '@/service/use-try-app'
|
||||
import type { MoreLikeThisConfig, PromptConfig, TextToSpeechConfig } from '@/models/debug'
|
||||
import { userInputsFormToPromptVariables } from '@/utils/model-config'
|
||||
import type { VisionFile, VisionSettings } from '@/types/app'
|
||||
import { Resolution, TransferMethod } from '@/types/app'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { noop } from 'lodash-es'
|
||||
import type { Task } from '../../../share/text-generation/types'
|
||||
import Res from '@/app/components/share/text-generation/result'
|
||||
import { AppSourceType } from '@/service/share'
|
||||
import { TaskStatus } from '@/app/components/share/text-generation/types'
|
||||
import Alert from '@/app/components/base/alert'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type Props = {
|
||||
appId: string
|
||||
className?: string
|
||||
isWorkflow?: boolean
|
||||
appData: AppData | null
|
||||
}
|
||||
|
||||
const TextGeneration: FC<Props> = ({
|
||||
appId,
|
||||
className,
|
||||
isWorkflow,
|
||||
appData,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const media = useBreakpoints()
|
||||
const isPC = media === MediaType.pc
|
||||
|
||||
const [inputs, doSetInputs] = useState<Record<string, any>>({})
|
||||
const inputsRef = useRef<Record<string, any>>(inputs)
|
||||
const setInputs = useCallback((newInputs: Record<string, any>) => {
|
||||
doSetInputs(newInputs)
|
||||
inputsRef.current = newInputs
|
||||
}, [])
|
||||
|
||||
const updateAppInfo = useWebAppStore(s => s.updateAppInfo)
|
||||
const { data: tryAppParams } = useGetTryAppParams(appId)
|
||||
|
||||
const updateAppParams = useWebAppStore(s => s.updateAppParams)
|
||||
const appParams = useWebAppStore(s => s.appParams)
|
||||
const [siteInfo, setSiteInfo] = useState<SiteInfo | null>(null)
|
||||
const [promptConfig, setPromptConfig] = useState<PromptConfig | null>(null)
|
||||
const [customConfig, setCustomConfig] = useState<Record<string, any> | null>(null)
|
||||
const [moreLikeThisConfig, setMoreLikeThisConfig] = useState<MoreLikeThisConfig | null>(null)
|
||||
const [textToSpeechConfig, setTextToSpeechConfig] = useState<TextToSpeechConfig | null>(null)
|
||||
const [controlSend, setControlSend] = 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 [isShowResultPanel, { setTrue: doShowResultPanel, setFalse: hideResultPanel }] = useBoolean(false)
|
||||
const showResultPanel = () => {
|
||||
// fix: useClickAway hideResSidebar will close sidebar
|
||||
setTimeout(() => {
|
||||
doShowResultPanel()
|
||||
}, 0)
|
||||
}
|
||||
|
||||
const handleSend = () => {
|
||||
setControlSend(Date.now())
|
||||
showResultPanel()
|
||||
}
|
||||
|
||||
const [resultExisted, setResultExisted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!appData) return
|
||||
updateAppInfo(appData)
|
||||
}, [appData, updateAppInfo])
|
||||
|
||||
useEffect(() => {
|
||||
if (!tryAppParams) return
|
||||
updateAppParams(tryAppParams)
|
||||
}, [tryAppParams, updateAppParams])
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!appData || !appParams)
|
||||
return
|
||||
const { site: siteInfo, custom_config } = appData
|
||||
setSiteInfo(siteInfo as SiteInfo)
|
||||
setCustomConfig(custom_config)
|
||||
|
||||
const { user_input_form, more_like_this, file_upload, text_to_speech }: any = appParams
|
||||
setVisionConfig({
|
||||
// legacy of image upload compatible
|
||||
...file_upload,
|
||||
transfer_methods: file_upload?.allowed_file_upload_methods || file_upload?.allowed_upload_methods,
|
||||
// legacy of image upload compatible
|
||||
image_file_size_limit: appParams?.system_parameters.image_file_size_limit,
|
||||
fileUploadConfig: appParams?.system_parameters,
|
||||
} as any)
|
||||
const prompt_variables = userInputsFormToPromptVariables(user_input_form)
|
||||
setPromptConfig({
|
||||
prompt_template: '', // placeholder for future
|
||||
prompt_variables,
|
||||
} as PromptConfig)
|
||||
setMoreLikeThisConfig(more_like_this)
|
||||
setTextToSpeechConfig(text_to_speech)
|
||||
})()
|
||||
}, [appData, appParams])
|
||||
|
||||
const [isCompleted, setIsCompleted] = useState(false)
|
||||
const handleCompleted = useCallback(() => {
|
||||
setIsCompleted(true)
|
||||
}, [])
|
||||
const [isHideTryNotice, {
|
||||
setTrue: hideTryNotice,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const renderRes = (task?: Task) => (<Res
|
||||
key={task?.id}
|
||||
isWorkflow={!!isWorkflow}
|
||||
isCallBatchAPI={false}
|
||||
isPC={isPC}
|
||||
isMobile={!isPC}
|
||||
appSourceType={AppSourceType.tryApp}
|
||||
appId={appId}
|
||||
isError={task?.status === TaskStatus.failed}
|
||||
promptConfig={promptConfig}
|
||||
moreLikeThisEnabled={!!moreLikeThisConfig?.enabled}
|
||||
inputs={inputs}
|
||||
controlSend={controlSend}
|
||||
onShowRes={showResultPanel}
|
||||
handleSaveMessage={noop}
|
||||
taskId={task?.id}
|
||||
onCompleted={handleCompleted}
|
||||
visionConfig={visionConfig}
|
||||
completionFiles={completionFiles}
|
||||
isShowTextToSpeech={!!textToSpeechConfig?.enabled}
|
||||
siteInfo={siteInfo}
|
||||
onRunStart={() => setResultExisted(true)}
|
||||
/>)
|
||||
|
||||
const renderResWrap = (
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex h-full flex-col',
|
||||
'rounded-r-2xl bg-chatbot-bg',
|
||||
)}
|
||||
>
|
||||
<div className={cn(
|
||||
'flex h-0 grow flex-col overflow-y-auto p-6',
|
||||
)}>
|
||||
{isCompleted && !isHideTryNotice && (
|
||||
<Alert className='mb-3 shrink-0' message={t('explore.tryApp.tryInfo')} onHide={hideTryNotice} />
|
||||
)}
|
||||
{renderRes()}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
if (!siteInfo || !promptConfig) {
|
||||
return (
|
||||
<div className={cn('flex h-screen items-center', className)}>
|
||||
<Loading type='app' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'rounded-2xl border border-components-panel-border bg-background-section-burn',
|
||||
isPC && 'flex',
|
||||
!isPC && 'flex-col',
|
||||
'h-full rounded-2xl shadow-md',
|
||||
className,
|
||||
)}>
|
||||
{/* Left */}
|
||||
<div className={cn(
|
||||
'relative flex h-full shrink-0 flex-col',
|
||||
isPC && 'w-[600px] max-w-[50%]',
|
||||
'rounded-l-2xl bg-components-panel-bg',
|
||||
)}>
|
||||
{/* Header */}
|
||||
<div className={cn('shrink-0 space-y-4 pb-2', isPC ? ' p-8 pb-0' : 'p-4 pb-0')}>
|
||||
<div className='flex items-center gap-3'>
|
||||
<AppIcon
|
||||
size={isPC ? 'large' : 'small'}
|
||||
iconType={siteInfo.icon_type}
|
||||
icon={siteInfo.icon}
|
||||
background={siteInfo.icon_background || appDefaultIconBackground}
|
||||
imageUrl={siteInfo.icon_url}
|
||||
/>
|
||||
<div className='system-md-semibold grow truncate text-text-secondary'>{siteInfo.title}</div>
|
||||
</div>
|
||||
{siteInfo.description && (
|
||||
<div className='system-xs-regular text-text-tertiary'>{siteInfo.description}</div>
|
||||
)}
|
||||
</div>
|
||||
{/* form */}
|
||||
<div className={cn(
|
||||
'h-0 grow overflow-y-auto',
|
||||
isPC ? 'px-8' : 'px-4',
|
||||
!isPC && resultExisted && customConfig?.remove_webapp_brand && 'rounded-b-2xl border-b-[0.5px] border-divider-regular',
|
||||
)}>
|
||||
<RunOnce
|
||||
siteInfo={siteInfo}
|
||||
inputs={inputs}
|
||||
inputsRef={inputsRef}
|
||||
onInputsChange={setInputs}
|
||||
promptConfig={promptConfig}
|
||||
onSend={handleSend}
|
||||
visionConfig={visionConfig}
|
||||
onVisionFilesChange={setCompletionFiles}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Result */}
|
||||
<div className={cn('h-full w-0 grow')}>
|
||||
{!isPC && (
|
||||
<div
|
||||
className={cn(
|
||||
isShowResultPanel
|
||||
? 'flex items-center justify-center p-2 pt-6'
|
||||
: 'absolute left-0 top-0 z-10 flex w-full items-center justify-center px-2 pb-[57px] pt-[3px]',
|
||||
)}
|
||||
onClick={() => {
|
||||
if (isShowResultPanel)
|
||||
hideResultPanel()
|
||||
else
|
||||
showResultPanel()
|
||||
}}
|
||||
>
|
||||
<div className='h-1 w-8 cursor-grab rounded bg-divider-solid' />
|
||||
</div>
|
||||
)}
|
||||
{renderResWrap}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(TextGeneration)
|
||||
70
web/app/components/explore/try-app/index.tsx
Normal file
70
web/app/components/explore/try-app/index.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import Modal from '@/app/components/base/modal/index'
|
||||
import Tab, { TypeEnum } from './tab'
|
||||
import Button from '../../base/button'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import AppInfo from './app-info'
|
||||
import App from './app'
|
||||
import Preview from './preview'
|
||||
import { useGetTryAppInfo } from '@/service/use-try-app'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
|
||||
type Props = {
|
||||
appId: string
|
||||
category?: string
|
||||
onClose: () => void
|
||||
onCreate: () => void
|
||||
}
|
||||
|
||||
const TryApp: FC<Props> = ({
|
||||
appId,
|
||||
category,
|
||||
onClose,
|
||||
onCreate,
|
||||
}) => {
|
||||
const [type, setType] = useState<TypeEnum>(TypeEnum.TRY)
|
||||
const { data: appDetail, isLoading } = useGetTryAppInfo(appId)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow
|
||||
onClose={onClose}
|
||||
className='h-[calc(100vh-32px)] min-w-[1280px] max-w-[calc(100vw-32px)] overflow-x-auto p-2'
|
||||
>
|
||||
{isLoading ? (<div className='flex h-full items-center justify-center'>
|
||||
<Loading type='area' />
|
||||
</div>) : (
|
||||
<div className='flex h-full flex-col'>
|
||||
<div className='flex shrink-0 justify-between pl-4'>
|
||||
<Tab
|
||||
value={type}
|
||||
onChange={setType}
|
||||
/>
|
||||
<Button
|
||||
size='large'
|
||||
variant='tertiary'
|
||||
className='flex size-7 items-center justify-center rounded-[10px] p-0 text-components-button-tertiary-text'
|
||||
onClick={onClose}
|
||||
>
|
||||
<RiCloseLine className='size-5' onClick={onClose} />
|
||||
</Button>
|
||||
</div>
|
||||
{/* Main content */}
|
||||
<div className='mt-2 flex h-0 grow justify-between space-x-2'>
|
||||
{type === TypeEnum.TRY ? <App appId={appId} appDetail={appDetail!} /> : <Preview appId={appId} appDetail={appDetail!} />}
|
||||
<AppInfo
|
||||
className='w-[360px] shrink-0'
|
||||
appDetail={appDetail!}
|
||||
appId={appId}
|
||||
category={category}
|
||||
onCreate={onCreate}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
export default React.memo(TryApp)
|
||||
361
web/app/components/explore/try-app/preview/basic-app-preview.tsx
Normal file
361
web/app/components/explore/try-app/preview/basic-app-preview.tsx
Normal file
@@ -0,0 +1,361 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { clone } from 'lodash-es'
|
||||
|
||||
import Loading from '@/app/components/base/loading'
|
||||
|
||||
import type { ModelConfig as BackendModelConfig, PromptVariable } from '@/types/app'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
import Config from '@/app/components/app/configuration/config'
|
||||
import Debug from '@/app/components/app/configuration/debug'
|
||||
import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { ModelModeType, Resolution, TransferMethod } from '@/types/app'
|
||||
import type { ModelConfig } from '@/models/debug'
|
||||
import { PromptMode } from '@/models/debug'
|
||||
import { ANNOTATION_DEFAULT, DEFAULT_AGENT_SETTING, DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
|
||||
import { FeaturesProvider } from '@/app/components/base/features'
|
||||
import type { Features as FeaturesData, FileUpload } from '@/app/components/base/features/types'
|
||||
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
|
||||
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||
|
||||
import { useGetTryAppDataSets, useGetTryAppInfo } from '@/service/use-try-app'
|
||||
import { noop } from 'lodash-es'
|
||||
import { correctModelProvider, correctToolProvider } from '@/utils'
|
||||
import { userInputsFormToPromptVariables } from '@/utils/model-config'
|
||||
import { useTextGenerationCurrentProviderAndModelAndModelList } from '../../../header/account-setting/model-provider-page/hooks'
|
||||
import { useAllToolProviders } from '@/service/use-tools'
|
||||
import { basePath } from '@/utils/var'
|
||||
|
||||
type Props = {
|
||||
appId: string
|
||||
}
|
||||
|
||||
const defaultModelConfig = {
|
||||
provider: 'langgenius/openai/openai',
|
||||
model_id: 'gpt-3.5-turbo',
|
||||
mode: ModelModeType.unset,
|
||||
configs: {
|
||||
prompt_template: '',
|
||||
prompt_variables: [] as PromptVariable[],
|
||||
},
|
||||
more_like_this: null,
|
||||
opening_statement: '',
|
||||
suggested_questions: [],
|
||||
sensitive_word_avoidance: null,
|
||||
speech_to_text: null,
|
||||
text_to_speech: null,
|
||||
file_upload: null,
|
||||
suggested_questions_after_answer: null,
|
||||
retriever_resource: null,
|
||||
annotation_reply: null,
|
||||
dataSets: [],
|
||||
agentConfig: DEFAULT_AGENT_SETTING,
|
||||
}
|
||||
const BasicAppPreview: FC<Props> = ({
|
||||
appId,
|
||||
}) => {
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
|
||||
const { data: appDetail, isLoading: isLoadingAppDetail } = useGetTryAppInfo(appId)
|
||||
const { data: collectionListFromServer, isLoading: isLoadingToolProviders } = useAllToolProviders()
|
||||
const collectionList = collectionListFromServer?.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
icon: basePath && typeof item.icon == 'string' && !item.icon.includes(basePath) ? `${basePath}${item.icon}` : item.icon,
|
||||
}
|
||||
})
|
||||
const datasetIds = (() => {
|
||||
if (isLoadingAppDetail)
|
||||
return []
|
||||
const modelConfig = appDetail?.model_config
|
||||
if (!modelConfig)
|
||||
return []
|
||||
let datasets: any = null
|
||||
|
||||
if (modelConfig.agent_mode?.tools?.find(({ dataset }: any) => dataset?.enabled))
|
||||
datasets = modelConfig.agent_mode?.tools.filter(({ dataset }: any) => dataset?.enabled)
|
||||
// new dataset struct
|
||||
else if (modelConfig.dataset_configs.datasets?.datasets?.length > 0)
|
||||
datasets = modelConfig.dataset_configs?.datasets?.datasets
|
||||
|
||||
if (datasets?.length && datasets?.length > 0)
|
||||
return datasets.map(({ dataset }: any) => dataset.id)
|
||||
|
||||
return []
|
||||
})()
|
||||
const { data: dataSetData, isLoading: isLoadingDatasets } = useGetTryAppDataSets(appId, datasetIds)
|
||||
const dataSets = dataSetData?.data || []
|
||||
const isLoading = isLoadingAppDetail || isLoadingDatasets || isLoadingToolProviders
|
||||
|
||||
const modelConfig: ModelConfig = ((modelConfig?: BackendModelConfig) => {
|
||||
if (isLoading || !modelConfig)
|
||||
return defaultModelConfig
|
||||
|
||||
const model = modelConfig.model
|
||||
|
||||
const newModelConfig = {
|
||||
provider: correctModelProvider(model.provider),
|
||||
model_id: model.name,
|
||||
mode: model.mode,
|
||||
configs: {
|
||||
prompt_template: modelConfig.pre_prompt || '',
|
||||
prompt_variables: userInputsFormToPromptVariables(
|
||||
[
|
||||
...(modelConfig.user_input_form as any),
|
||||
...(
|
||||
modelConfig.external_data_tools?.length
|
||||
? modelConfig.external_data_tools.map((item: any) => {
|
||||
return {
|
||||
external_data_tool: {
|
||||
variable: item.variable as string,
|
||||
label: item.label as string,
|
||||
enabled: item.enabled,
|
||||
type: item.type as string,
|
||||
config: item.config,
|
||||
required: true,
|
||||
icon: item.icon,
|
||||
icon_background: item.icon_background,
|
||||
},
|
||||
}
|
||||
})
|
||||
: []
|
||||
),
|
||||
],
|
||||
modelConfig.dataset_query_variable,
|
||||
),
|
||||
},
|
||||
more_like_this: modelConfig.more_like_this,
|
||||
opening_statement: modelConfig.opening_statement,
|
||||
suggested_questions: modelConfig.suggested_questions,
|
||||
sensitive_word_avoidance: modelConfig.sensitive_word_avoidance,
|
||||
speech_to_text: modelConfig.speech_to_text,
|
||||
text_to_speech: modelConfig.text_to_speech,
|
||||
file_upload: modelConfig.file_upload,
|
||||
suggested_questions_after_answer: modelConfig.suggested_questions_after_answer,
|
||||
retriever_resource: modelConfig.retriever_resource,
|
||||
annotation_reply: modelConfig.annotation_reply,
|
||||
external_data_tools: modelConfig.external_data_tools,
|
||||
dataSets,
|
||||
agentConfig: appDetail?.mode === 'agent-chat' ? {
|
||||
max_iteration: DEFAULT_AGENT_SETTING.max_iteration,
|
||||
...modelConfig.agent_mode,
|
||||
// remove dataset
|
||||
enabled: true, // modelConfig.agent_mode?.enabled is not correct. old app: the value of app with dataset's is always true
|
||||
tools: modelConfig.agent_mode?.tools.filter((tool: any) => {
|
||||
return !tool.dataset
|
||||
}).map((tool: any) => {
|
||||
const toolInCollectionList = collectionList?.find(c => tool.provider_id === c.id)
|
||||
return {
|
||||
...tool,
|
||||
isDeleted: appDetail?.deleted_tools?.some((deletedTool: any) => deletedTool.id === tool.id && deletedTool.tool_name === tool.tool_name),
|
||||
notAuthor: toolInCollectionList?.is_team_authorization === false,
|
||||
...(tool.provider_type === 'builtin' ? {
|
||||
provider_id: correctToolProvider(tool.provider_name, !!toolInCollectionList),
|
||||
provider_name: correctToolProvider(tool.provider_name, !!toolInCollectionList),
|
||||
} : {}),
|
||||
}
|
||||
}),
|
||||
} : DEFAULT_AGENT_SETTING,
|
||||
}
|
||||
return (newModelConfig as any)
|
||||
})(appDetail?.model_config)
|
||||
const mode = appDetail?.mode
|
||||
// const isChatApp = ['chat', 'advanced-chat', 'agent-chat'].includes(mode!)
|
||||
|
||||
// chat configuration
|
||||
const promptMode = modelConfig?.prompt_type === PromptMode.advanced ? PromptMode.advanced : PromptMode.simple
|
||||
const isAdvancedMode = promptMode === PromptMode.advanced
|
||||
const isAgent = mode === 'agent-chat'
|
||||
const chatPromptConfig = isAdvancedMode ? (modelConfig?.chat_prompt_config || clone(DEFAULT_CHAT_PROMPT_CONFIG)) : undefined
|
||||
const suggestedQuestions = modelConfig?.suggested_questions || []
|
||||
const moreLikeThisConfig = modelConfig?.more_like_this || { enabled: false }
|
||||
const suggestedQuestionsAfterAnswerConfig = modelConfig?.suggested_questions_after_answer || { enabled: false }
|
||||
const speechToTextConfig = modelConfig?.speech_to_text || { enabled: false }
|
||||
const textToSpeechConfig = modelConfig?.text_to_speech || { enabled: false, voice: '', language: '' }
|
||||
const citationConfig = modelConfig?.retriever_resource || { enabled: false }
|
||||
const annotationConfig = modelConfig?.annotation_reply || {
|
||||
id: '',
|
||||
enabled: false,
|
||||
score_threshold: ANNOTATION_DEFAULT.score_threshold,
|
||||
embedding_model: {
|
||||
embedding_provider_name: '',
|
||||
embedding_model_name: '',
|
||||
},
|
||||
}
|
||||
const moderationConfig = modelConfig?.sensitive_word_avoidance || { enabled: false }
|
||||
// completion configuration
|
||||
const completionPromptConfig = modelConfig?.completion_prompt_config || clone(DEFAULT_COMPLETION_PROMPT_CONFIG) as any
|
||||
|
||||
// prompt & model config
|
||||
const inputs = {}
|
||||
const query = ''
|
||||
const completionParams = useState<FormValue>({})
|
||||
|
||||
const {
|
||||
currentModel: currModel,
|
||||
} = useTextGenerationCurrentProviderAndModelAndModelList(
|
||||
{
|
||||
provider: modelConfig.provider,
|
||||
model: modelConfig.model_id,
|
||||
},
|
||||
)
|
||||
|
||||
const isShowVisionConfig = !!currModel?.features?.includes(ModelFeatureEnum.vision)
|
||||
const isShowDocumentConfig = !!currModel?.features?.includes(ModelFeatureEnum.document)
|
||||
const isShowAudioConfig = !!currModel?.features?.includes(ModelFeatureEnum.audio)
|
||||
const isAllowVideoUpload = !!currModel?.features?.includes(ModelFeatureEnum.video)
|
||||
const visionConfig = {
|
||||
enabled: false,
|
||||
number_limits: 2,
|
||||
detail: Resolution.low,
|
||||
transfer_methods: [TransferMethod.local_file],
|
||||
}
|
||||
|
||||
const featuresData: FeaturesData = useMemo(() => {
|
||||
return {
|
||||
moreLikeThis: modelConfig.more_like_this || { enabled: false },
|
||||
opening: {
|
||||
enabled: !!modelConfig.opening_statement,
|
||||
opening_statement: modelConfig.opening_statement || '',
|
||||
suggested_questions: modelConfig.suggested_questions || [],
|
||||
},
|
||||
moderation: modelConfig.sensitive_word_avoidance || { enabled: false },
|
||||
speech2text: modelConfig.speech_to_text || { enabled: false },
|
||||
text2speech: modelConfig.text_to_speech || { enabled: false },
|
||||
file: {
|
||||
image: {
|
||||
detail: modelConfig.file_upload?.image?.detail || Resolution.high,
|
||||
enabled: !!modelConfig.file_upload?.image?.enabled,
|
||||
number_limits: modelConfig.file_upload?.image?.number_limits || 3,
|
||||
transfer_methods: modelConfig.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
|
||||
},
|
||||
enabled: !!(modelConfig.file_upload?.enabled || modelConfig.file_upload?.image?.enabled),
|
||||
allowed_file_types: modelConfig.file_upload?.allowed_file_types || [],
|
||||
allowed_file_extensions: modelConfig.file_upload?.allowed_file_extensions || [...FILE_EXTS[SupportUploadFileTypes.image], ...FILE_EXTS[SupportUploadFileTypes.video]].map(ext => `.${ext}`),
|
||||
allowed_file_upload_methods: modelConfig.file_upload?.allowed_file_upload_methods || modelConfig.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
|
||||
number_limits: modelConfig.file_upload?.number_limits || modelConfig.file_upload?.image?.number_limits || 3,
|
||||
fileUploadConfig: {},
|
||||
} as FileUpload,
|
||||
suggested: modelConfig.suggested_questions_after_answer || { enabled: false },
|
||||
citation: modelConfig.retriever_resource || { enabled: false },
|
||||
annotationReply: modelConfig.annotation_reply || { enabled: false },
|
||||
}
|
||||
}, [modelConfig])
|
||||
|
||||
if (isLoading) {
|
||||
return <div className='flex h-full items-center justify-center'>
|
||||
<Loading type='area' />
|
||||
</div>
|
||||
}
|
||||
const value = {
|
||||
readonly: true,
|
||||
appId,
|
||||
isAPIKeySet: true,
|
||||
isTrailFinished: false,
|
||||
mode,
|
||||
modelModeType: '',
|
||||
promptMode,
|
||||
isAdvancedMode,
|
||||
isAgent,
|
||||
isOpenAI: false,
|
||||
isFunctionCall: false,
|
||||
collectionList: [],
|
||||
setPromptMode: noop,
|
||||
canReturnToSimpleMode: false,
|
||||
setCanReturnToSimpleMode: noop,
|
||||
chatPromptConfig,
|
||||
completionPromptConfig,
|
||||
currentAdvancedPrompt: '',
|
||||
setCurrentAdvancedPrompt: noop,
|
||||
conversationHistoriesRole: completionPromptConfig.conversation_histories_role,
|
||||
showHistoryModal: false,
|
||||
setConversationHistoriesRole: noop,
|
||||
hasSetBlockStatus: true,
|
||||
conversationId: '',
|
||||
introduction: '',
|
||||
setIntroduction: noop,
|
||||
suggestedQuestions,
|
||||
setSuggestedQuestions: noop,
|
||||
setConversationId: noop,
|
||||
controlClearChatMessage: false,
|
||||
setControlClearChatMessage: noop,
|
||||
prevPromptConfig: {},
|
||||
setPrevPromptConfig: noop,
|
||||
moreLikeThisConfig,
|
||||
setMoreLikeThisConfig: noop,
|
||||
suggestedQuestionsAfterAnswerConfig,
|
||||
setSuggestedQuestionsAfterAnswerConfig: noop,
|
||||
speechToTextConfig,
|
||||
setSpeechToTextConfig: noop,
|
||||
textToSpeechConfig,
|
||||
setTextToSpeechConfig: noop,
|
||||
citationConfig,
|
||||
setCitationConfig: noop,
|
||||
annotationConfig,
|
||||
setAnnotationConfig: noop,
|
||||
moderationConfig,
|
||||
setModerationConfig: noop,
|
||||
externalDataToolsConfig: {},
|
||||
setExternalDataToolsConfig: noop,
|
||||
formattingChanged: false,
|
||||
setFormattingChanged: noop,
|
||||
inputs,
|
||||
setInputs: noop,
|
||||
query,
|
||||
setQuery: noop,
|
||||
completionParams,
|
||||
setCompletionParams: noop,
|
||||
modelConfig,
|
||||
setModelConfig: noop,
|
||||
showSelectDataSet: noop,
|
||||
dataSets,
|
||||
setDataSets: noop,
|
||||
datasetConfigs: [],
|
||||
datasetConfigsRef: {},
|
||||
setDatasetConfigs: noop,
|
||||
hasSetContextVar: true,
|
||||
isShowVisionConfig,
|
||||
visionConfig,
|
||||
setVisionConfig: noop,
|
||||
isAllowVideoUpload,
|
||||
isShowDocumentConfig,
|
||||
isShowAudioConfig,
|
||||
rerankSettingModalOpen: false,
|
||||
setRerankSettingModalOpen: noop,
|
||||
}
|
||||
return (
|
||||
<ConfigContext.Provider value={value as any}>
|
||||
<FeaturesProvider features={featuresData}>
|
||||
<div className="flex h-full w-full flex-col bg-components-panel-on-panel-item-bg">
|
||||
<div className='relative flex h-[200px] grow'>
|
||||
<div className={'flex h-full w-full shrink-0 flex-col sm:w-1/2'}>
|
||||
<Config />
|
||||
</div>
|
||||
{!isMobile && <div className="relative flex h-full w-1/2 grow flex-col overflow-y-auto " style={{ borderColor: 'rgba(0, 0, 0, 0.02)' }}>
|
||||
<div className='flex grow flex-col rounded-tl-2xl border-l-[0.5px] border-t-[0.5px] border-components-panel-border bg-chatbot-bg '>
|
||||
<Debug
|
||||
isAPIKeySet
|
||||
onSetting={noop}
|
||||
inputs={inputs}
|
||||
modelParameterParams={{
|
||||
setModel: noop,
|
||||
onCompletionParamsChange: noop,
|
||||
}}
|
||||
debugWithMultipleModel={false}
|
||||
multipleModelConfigs={[]}
|
||||
onMultipleModelConfigsChange={noop}
|
||||
/>
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
</FeaturesProvider>
|
||||
</ConfigContext.Provider>
|
||||
)
|
||||
}
|
||||
export default React.memo(BasicAppPreview)
|
||||
@@ -0,0 +1,37 @@
|
||||
'use client'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { useGetTryAppFlowPreview } from '@/service/use-try-app'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import WorkflowPreview from '@/app/components/workflow/workflow-preview'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
appId: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
const FlowAppPreview: FC<Props> = ({
|
||||
appId,
|
||||
className,
|
||||
}) => {
|
||||
const { data, isLoading } = useGetTryAppFlowPreview(appId)
|
||||
|
||||
if (isLoading) {
|
||||
return <div className='flex h-full items-center justify-center'>
|
||||
<Loading type='area' />
|
||||
</div>
|
||||
}
|
||||
if (!data)
|
||||
return null
|
||||
return (
|
||||
<div className='h-full w-full'>
|
||||
<WorkflowPreview
|
||||
{...data.graph}
|
||||
className={cn(className)}
|
||||
miniMapToRight
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(FlowAppPreview)
|
||||
23
web/app/components/explore/try-app/preview/index.tsx
Normal file
23
web/app/components/explore/try-app/preview/index.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import BasicAppPreview from './basic-app-preview'
|
||||
import FlowAppPreview from './flow-app-preview'
|
||||
import type { TryAppInfo } from '@/service/try-app'
|
||||
|
||||
type Props = {
|
||||
appId: string
|
||||
appDetail: TryAppInfo
|
||||
}
|
||||
|
||||
const Preview: FC<Props> = ({
|
||||
appId,
|
||||
appDetail,
|
||||
}) => {
|
||||
const isBasicApp = ['agent-chat', 'chat', 'completion'].includes(appDetail.mode)
|
||||
|
||||
return <div className='h-full w-full'>
|
||||
{isBasicApp ? <BasicAppPreview appId={appId} /> : <FlowAppPreview appId={appId} className='h-full' />}
|
||||
</div>
|
||||
}
|
||||
export default React.memo(Preview)
|
||||
37
web/app/components/explore/try-app/tab.tsx
Normal file
37
web/app/components/explore/try-app/tab.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import TabHeader from '../../base/tab-header'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export enum TypeEnum {
|
||||
TRY = 'try',
|
||||
DETAIL = 'detail',
|
||||
}
|
||||
|
||||
type Props = {
|
||||
value: TypeEnum
|
||||
onChange: (value: TypeEnum) => void
|
||||
}
|
||||
|
||||
const Tab: FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const tabs = [
|
||||
{ id: TypeEnum.TRY, name: t('explore.tryApp.tabHeader.try') },
|
||||
{ id: TypeEnum.DETAIL, name: t('explore.tryApp.tabHeader.detail') },
|
||||
]
|
||||
return (
|
||||
<TabHeader
|
||||
items={tabs}
|
||||
value={value}
|
||||
onChange={onChange as (value: string) => void}
|
||||
itemClassName='ml-0 system-md-semibold-uppercase'
|
||||
itemWrapClassName='pt-2'
|
||||
activeItemClassName='border-util-colors-blue-brand-blue-brand-500'
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default React.memo(Tab)
|
||||
@@ -14,7 +14,7 @@ import RunBatch from './run-batch'
|
||||
import ResDownload from './run-batch/res-download'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import RunOnce from '@/app/components/share/text-generation/run-once'
|
||||
import { fetchSavedMessage as doFetchSavedMessage, removeMessage, saveMessage } from '@/service/share'
|
||||
import { AppSourceType, fetchSavedMessage as doFetchSavedMessage, removeMessage, saveMessage } from '@/service/share'
|
||||
import type { SiteInfo } from '@/models/share'
|
||||
import type {
|
||||
MoreLikeThisConfig,
|
||||
@@ -41,24 +41,9 @@ import { AccessMode } from '@/models/access-control'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { useWebAppStore } from '@/context/web-app-context'
|
||||
|
||||
import type { Task } from './types'
|
||||
import { TaskStatus } from './types'
|
||||
const GROUP_SIZE = 5 // to avoid RPM(Request per minute) limit. The group task finished then the next group.
|
||||
enum TaskStatus {
|
||||
pending = 'pending',
|
||||
running = 'running',
|
||||
completed = 'completed',
|
||||
failed = 'failed',
|
||||
}
|
||||
|
||||
type TaskParam = {
|
||||
inputs: Record<string, any>
|
||||
}
|
||||
|
||||
type Task = {
|
||||
id: number
|
||||
status: TaskStatus
|
||||
params: TaskParam
|
||||
}
|
||||
|
||||
export type IMainProps = {
|
||||
isInstalledApp?: boolean
|
||||
@@ -72,6 +57,7 @@ const TextGeneration: FC<IMainProps> = ({
|
||||
isWorkflow = false,
|
||||
}) => {
|
||||
const { notify } = Toast
|
||||
const appSourceType = isInstalledApp ? AppSourceType.installedApp : AppSourceType.webApp
|
||||
|
||||
const { t } = useTranslation()
|
||||
const media = useBreakpoints()
|
||||
@@ -101,16 +87,18 @@ const TextGeneration: FC<IMainProps> = ({
|
||||
// save message
|
||||
const [savedMessages, setSavedMessages] = useState<SavedMessage[]>([])
|
||||
const fetchSavedMessage = useCallback(async () => {
|
||||
const res: any = await doFetchSavedMessage(isInstalledApp, appId)
|
||||
if (!appId)
|
||||
return
|
||||
const res: any = await doFetchSavedMessage(appSourceType, appId)
|
||||
setSavedMessages(res.data)
|
||||
}, [isInstalledApp, appId])
|
||||
}, [appSourceType, appId])
|
||||
const handleSaveMessage = async (messageId: string) => {
|
||||
await saveMessage(messageId, isInstalledApp, appId)
|
||||
await saveMessage(messageId, appSourceType, appId)
|
||||
notify({ type: 'success', message: t('common.api.saved') })
|
||||
fetchSavedMessage()
|
||||
}
|
||||
const handleRemoveSavedMessage = async (messageId: string) => {
|
||||
await removeMessage(messageId, isInstalledApp, appId)
|
||||
await removeMessage(messageId, appSourceType, appId)
|
||||
notify({ type: 'success', message: t('common.api.remove') })
|
||||
fetchSavedMessage()
|
||||
}
|
||||
@@ -416,8 +404,8 @@ const TextGeneration: FC<IMainProps> = ({
|
||||
isCallBatchAPI={isCallBatchAPI}
|
||||
isPC={isPC}
|
||||
isMobile={!isPC}
|
||||
isInstalledApp={isInstalledApp}
|
||||
installedAppInfo={installedAppInfo}
|
||||
appSourceType={isInstalledApp ? AppSourceType.installedApp : AppSourceType.webApp}
|
||||
appId={installedAppInfo?.id}
|
||||
isError={task?.status === TaskStatus.failed}
|
||||
promptConfig={promptConfig}
|
||||
moreLikeThisEnabled={!!moreLikeThisConfig?.enabled}
|
||||
|
||||
@@ -7,11 +7,11 @@ import { produce } from 'immer'
|
||||
import TextGenerationRes from '@/app/components/app/text-generate/item'
|
||||
import NoData from '@/app/components/share/text-generation/no-data'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import type { AppSourceType } from '@/service/share'
|
||||
import { sendCompletionMessage, sendWorkflowMessage, updateFeedback } from '@/service/share'
|
||||
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import type { PromptConfig } from '@/models/debug'
|
||||
import type { InstalledApp } from '@/models/explore'
|
||||
import { TransferMethod, type VisionFile, type VisionSettings } from '@/types/app'
|
||||
import { NodeRunningStatus, WorkflowRunningStatus } from '@/app/components/workflow/types'
|
||||
import type { WorkflowProcess } from '@/app/components/base/chat/types'
|
||||
@@ -30,8 +30,8 @@ export type IResultProps = {
|
||||
isCallBatchAPI: boolean
|
||||
isPC: boolean
|
||||
isMobile: boolean
|
||||
isInstalledApp: boolean
|
||||
installedAppInfo?: InstalledApp
|
||||
appSourceType: AppSourceType
|
||||
appId?: string
|
||||
isError: boolean
|
||||
isShowTextToSpeech: boolean
|
||||
promptConfig: PromptConfig | null
|
||||
@@ -55,8 +55,8 @@ const Result: FC<IResultProps> = ({
|
||||
isCallBatchAPI,
|
||||
isPC,
|
||||
isMobile,
|
||||
isInstalledApp,
|
||||
installedAppInfo,
|
||||
appSourceType,
|
||||
appId,
|
||||
isError,
|
||||
isShowTextToSpeech,
|
||||
promptConfig,
|
||||
@@ -104,7 +104,7 @@ const Result: FC<IResultProps> = ({
|
||||
})
|
||||
|
||||
const handleFeedback = async (feedback: FeedbackType) => {
|
||||
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, isInstalledApp, installedAppInfo?.id)
|
||||
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, appSourceType, appId)
|
||||
setFeedback(feedback)
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ const Result: FC<IResultProps> = ({
|
||||
|
||||
let hasEmptyInput = ''
|
||||
const requiredVars = prompt_variables?.filter(({ key, name, required, type }) => {
|
||||
if(type === 'boolean' || type === 'checkbox')
|
||||
if (type === 'boolean' || type === 'checkbox')
|
||||
return false // boolean/checkbox input is not required
|
||||
const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null)
|
||||
return res
|
||||
@@ -374,8 +374,8 @@ const Result: FC<IResultProps> = ({
|
||||
}))
|
||||
},
|
||||
},
|
||||
isInstalledApp,
|
||||
installedAppInfo?.id,
|
||||
appSourceType,
|
||||
appId,
|
||||
)
|
||||
}
|
||||
else {
|
||||
@@ -408,7 +408,7 @@ const Result: FC<IResultProps> = ({
|
||||
onCompleted(getCompletionRes(), taskId, false)
|
||||
isEnd = true
|
||||
},
|
||||
}, isInstalledApp, installedAppInfo?.id)
|
||||
}, appSourceType, appId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,8 +439,8 @@ const Result: FC<IResultProps> = ({
|
||||
feedback={feedback}
|
||||
onSave={handleSaveMessage}
|
||||
isMobile={isMobile}
|
||||
isInstalledApp={isInstalledApp}
|
||||
installedAppId={installedAppInfo?.id}
|
||||
appSourceType={appSourceType}
|
||||
installedAppId={appId}
|
||||
isLoading={isCallBatchAPI ? (!completionRes && isResponding) : false}
|
||||
taskId={isCallBatchAPI ? ((taskId as number) < 10 ? `0${taskId}` : `${taskId}`) : undefined}
|
||||
controlClearMoreLikeThis={controlClearMoreLikeThis}
|
||||
|
||||
16
web/app/components/share/text-generation/types.ts
Normal file
16
web/app/components/share/text-generation/types.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
type TaskParam = {
|
||||
inputs: Record<string, any>
|
||||
}
|
||||
|
||||
export type Task = {
|
||||
id: number
|
||||
status: TaskStatus
|
||||
params: TaskParam
|
||||
}
|
||||
|
||||
export enum TaskStatus {
|
||||
pending = 'pending',
|
||||
running = 'running',
|
||||
completed = 'completed',
|
||||
failed = 'failed',
|
||||
}
|
||||
@@ -9,6 +9,7 @@ type Props = {
|
||||
value: boolean
|
||||
required?: boolean
|
||||
onChange: (value: boolean) => void
|
||||
readonly?: boolean
|
||||
}
|
||||
|
||||
const BoolInput: FC<Props> = ({
|
||||
@@ -16,6 +17,7 @@ const BoolInput: FC<Props> = ({
|
||||
onChange,
|
||||
name,
|
||||
required,
|
||||
readonly,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const handleChange = useCallback(() => {
|
||||
@@ -27,6 +29,7 @@ const BoolInput: FC<Props> = ({
|
||||
className='!h-4 !w-4'
|
||||
checked={!!value}
|
||||
onCheck={handleChange}
|
||||
disabled={readonly}
|
||||
/>
|
||||
<div className='system-sm-medium flex items-center gap-1 text-text-secondary'>
|
||||
{name}
|
||||
|
||||
@@ -61,12 +61,14 @@ type WorkflowPreviewProps = {
|
||||
edges: Edge[]
|
||||
viewport: Viewport
|
||||
className?: string
|
||||
miniMapToRight?: boolean
|
||||
}
|
||||
const WorkflowPreview = ({
|
||||
nodes,
|
||||
edges,
|
||||
viewport,
|
||||
className,
|
||||
miniMapToRight,
|
||||
}: WorkflowPreviewProps) => {
|
||||
const [nodesData, setNodesData] = useState(() => initialNodes(nodes, edges))
|
||||
const [edgesData, setEdgesData] = useState(() => initialEdges(edges, nodes))
|
||||
@@ -97,8 +99,9 @@ const WorkflowPreview = ({
|
||||
height: 72,
|
||||
}}
|
||||
maskColor='var(--color-workflow-minimap-bg)'
|
||||
className='!absolute !bottom-14 !left-4 z-[9] !m-0 !h-[72px] !w-[102px] !rounded-lg !border-[0.5px]
|
||||
!border-divider-subtle !bg-background-default-subtle !shadow-md !shadow-shadow-shadow-5'
|
||||
className={cn('!absolute !bottom-14 z-[9] !m-0 !h-[72px] !w-[102px] !rounded-lg !border-[0.5px] !border-divider-subtle !bg-background-default-subtle !shadow-md !shadow-shadow-shadow-5',
|
||||
miniMapToRight ? '!right-4' : '!left-4',
|
||||
)}
|
||||
/>
|
||||
<div className='absolute bottom-4 left-4 z-[9] mt-1 flex items-center gap-2'>
|
||||
<ZoomInOut />
|
||||
|
||||
19
web/context/app-list-context.ts
Normal file
19
web/context/app-list-context.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { createContext } from 'use-context-selector'
|
||||
import { noop } from 'lodash-es'
|
||||
import type { CurrentTryAppParams } from './explore-context'
|
||||
|
||||
type Props = {
|
||||
currentApp?: CurrentTryAppParams
|
||||
isShowTryAppPanel: boolean
|
||||
setShowTryAppPanel: (showTryAppPanel: boolean, params?: CurrentTryAppParams) => void
|
||||
controlHideCreateFromTemplatePanel: number
|
||||
}
|
||||
|
||||
const AppListContext = createContext<Props>({
|
||||
isShowTryAppPanel: false,
|
||||
setShowTryAppPanel: noop,
|
||||
currentApp: undefined,
|
||||
controlHideCreateFromTemplatePanel: 0,
|
||||
})
|
||||
|
||||
export default AppListContext
|
||||
@@ -30,6 +30,7 @@ import type { Collection } from '@/app/components/tools/types'
|
||||
import { noop } from 'lodash-es'
|
||||
|
||||
type IDebugConfiguration = {
|
||||
readonly?: boolean
|
||||
appId: string
|
||||
isAPIKeySet: boolean
|
||||
isTrailFinished: boolean
|
||||
@@ -109,6 +110,7 @@ type IDebugConfiguration = {
|
||||
}
|
||||
|
||||
const DebugConfigurationContext = createContext<IDebugConfiguration>({
|
||||
readonly: false,
|
||||
appId: '',
|
||||
isAPIKeySet: false,
|
||||
isTrailFinished: false,
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { createContext } from 'use-context-selector'
|
||||
import type { InstalledApp } from '@/models/explore'
|
||||
import type { App, InstalledApp } from '@/models/explore'
|
||||
import { noop } from 'lodash-es'
|
||||
|
||||
export type CurrentTryAppParams = {
|
||||
appId: string
|
||||
app: App
|
||||
}
|
||||
|
||||
type IExplore = {
|
||||
controlUpdateInstalledApps: number
|
||||
setControlUpdateInstalledApps: (controlUpdateInstalledApps: number) => void
|
||||
@@ -10,6 +15,9 @@ type IExplore = {
|
||||
setInstalledApps: (installedApps: InstalledApp[]) => void
|
||||
isFetchingInstalledApps: boolean
|
||||
setIsFetchingInstalledApps: (isFetchingInstalledApps: boolean) => void
|
||||
currentApp?: CurrentTryAppParams
|
||||
isShowTryAppPanel: boolean
|
||||
setShowTryAppPanel: (showTryAppPanel: boolean, params?: CurrentTryAppParams) => void
|
||||
}
|
||||
|
||||
const ExploreContext = createContext<IExplore>({
|
||||
@@ -20,6 +28,9 @@ const ExploreContext = createContext<IExplore>({
|
||||
setInstalledApps: noop,
|
||||
isFetchingInstalledApps: false,
|
||||
setIsFetchingInstalledApps: noop,
|
||||
isShowTryAppPanel: false,
|
||||
setShowTryAppPanel: noop,
|
||||
currentApp: undefined,
|
||||
})
|
||||
|
||||
export default ExploreContext
|
||||
|
||||
@@ -2,19 +2,19 @@
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { camelCase } = require('lodash')
|
||||
const { camelCase } = require('lodash-es')
|
||||
|
||||
// Import the NAMESPACES array from i18next-config.ts
|
||||
function getNamespacesFromConfig() {
|
||||
const configPath = path.join(__dirname, 'i18next-config.ts')
|
||||
const configContent = fs.readFileSync(configPath, 'utf8')
|
||||
|
||||
|
||||
// Extract NAMESPACES array using regex
|
||||
const namespacesMatch = configContent.match(/const NAMESPACES = \[([\s\S]*?)\]/)
|
||||
if (!namespacesMatch) {
|
||||
throw new Error('Could not find NAMESPACES array in i18next-config.ts')
|
||||
}
|
||||
|
||||
|
||||
// Parse the namespaces
|
||||
const namespacesStr = namespacesMatch[1]
|
||||
const namespaces = namespacesStr
|
||||
@@ -22,25 +22,25 @@ function getNamespacesFromConfig() {
|
||||
.map(line => line.trim())
|
||||
.filter(line => line.startsWith("'") || line.startsWith('"'))
|
||||
.map(line => line.slice(1, -1)) // Remove quotes
|
||||
|
||||
|
||||
return namespaces
|
||||
}
|
||||
|
||||
function getNamespacesFromTypes() {
|
||||
const typesPath = path.join(__dirname, '../types/i18n.d.ts')
|
||||
|
||||
|
||||
if (!fs.existsSync(typesPath)) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
const typesContent = fs.readFileSync(typesPath, 'utf8')
|
||||
|
||||
|
||||
// Extract namespaces from Messages type
|
||||
const messagesMatch = typesContent.match(/export type Messages = \{([\s\S]*?)\}/)
|
||||
if (!messagesMatch) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
// Parse the properties
|
||||
const propertiesStr = messagesMatch[1]
|
||||
const properties = propertiesStr
|
||||
@@ -49,66 +49,66 @@ function getNamespacesFromTypes() {
|
||||
.filter(line => line.includes(':'))
|
||||
.map(line => line.split(':')[0].trim())
|
||||
.filter(prop => prop.length > 0)
|
||||
|
||||
|
||||
return properties
|
||||
}
|
||||
|
||||
function main() {
|
||||
try {
|
||||
console.log('🔍 Checking i18n types synchronization...')
|
||||
|
||||
|
||||
// Get namespaces from config
|
||||
const configNamespaces = getNamespacesFromConfig()
|
||||
console.log(`📦 Found ${configNamespaces.length} namespaces in config`)
|
||||
|
||||
|
||||
// Convert to camelCase for comparison
|
||||
const configCamelCase = configNamespaces.map(ns => camelCase(ns)).sort()
|
||||
|
||||
|
||||
// Get namespaces from type definitions
|
||||
const typeNamespaces = getNamespacesFromTypes()
|
||||
|
||||
|
||||
if (!typeNamespaces) {
|
||||
console.error('❌ Type definitions file not found or invalid')
|
||||
console.error(' Run: pnpm run gen:i18n-types')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
|
||||
console.log(`🔧 Found ${typeNamespaces.length} namespaces in types`)
|
||||
|
||||
|
||||
const typeCamelCase = typeNamespaces.sort()
|
||||
|
||||
|
||||
// Compare arrays
|
||||
const configSet = new Set(configCamelCase)
|
||||
const typeSet = new Set(typeCamelCase)
|
||||
|
||||
|
||||
// Find missing in types
|
||||
const missingInTypes = configCamelCase.filter(ns => !typeSet.has(ns))
|
||||
|
||||
|
||||
// Find extra in types
|
||||
const extraInTypes = typeCamelCase.filter(ns => !configSet.has(ns))
|
||||
|
||||
|
||||
let hasErrors = false
|
||||
|
||||
|
||||
if (missingInTypes.length > 0) {
|
||||
hasErrors = true
|
||||
console.error('❌ Missing in type definitions:')
|
||||
missingInTypes.forEach(ns => console.error(` - ${ns}`))
|
||||
}
|
||||
|
||||
|
||||
if (extraInTypes.length > 0) {
|
||||
hasErrors = true
|
||||
console.error('❌ Extra in type definitions:')
|
||||
extraInTypes.forEach(ns => console.error(` - ${ns}`))
|
||||
}
|
||||
|
||||
|
||||
if (hasErrors) {
|
||||
console.error('\n💡 To fix synchronization issues:')
|
||||
console.error(' Run: pnpm run gen:i18n-types')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
|
||||
console.log('✅ i18n types are synchronized')
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error:', error.message)
|
||||
process.exit(1)
|
||||
@@ -117,4 +117,4 @@ function main() {
|
||||
|
||||
if (require.main === module) {
|
||||
main()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,19 +2,19 @@
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { camelCase } = require('lodash')
|
||||
const { camelCase } = require('lodash-es')
|
||||
|
||||
// Import the NAMESPACES array from i18next-config.ts
|
||||
function getNamespacesFromConfig() {
|
||||
const configPath = path.join(__dirname, 'i18next-config.ts')
|
||||
const configContent = fs.readFileSync(configPath, 'utf8')
|
||||
|
||||
|
||||
// Extract NAMESPACES array using regex
|
||||
const namespacesMatch = configContent.match(/const NAMESPACES = \[([\s\S]*?)\]/)
|
||||
if (!namespacesMatch) {
|
||||
throw new Error('Could not find NAMESPACES array in i18next-config.ts')
|
||||
}
|
||||
|
||||
|
||||
// Parse the namespaces
|
||||
const namespacesStr = namespacesMatch[1]
|
||||
const namespaces = namespacesStr
|
||||
@@ -22,7 +22,7 @@ function getNamespacesFromConfig() {
|
||||
.map(line => line.trim())
|
||||
.filter(line => line.startsWith("'") || line.startsWith('"'))
|
||||
.map(line => line.slice(1, -1)) // Remove quotes
|
||||
|
||||
|
||||
return namespaces
|
||||
}
|
||||
|
||||
@@ -90,40 +90,40 @@ declare module 'i18next' {
|
||||
function main() {
|
||||
const args = process.argv.slice(2)
|
||||
const checkMode = args.includes('--check')
|
||||
|
||||
|
||||
try {
|
||||
console.log('📦 Generating i18n type definitions...')
|
||||
|
||||
|
||||
// Get namespaces from config
|
||||
const namespaces = getNamespacesFromConfig()
|
||||
console.log(`✅ Found ${namespaces.length} namespaces`)
|
||||
|
||||
|
||||
// Generate type definitions
|
||||
const typeDefinitions = generateTypeDefinitions(namespaces)
|
||||
|
||||
|
||||
const outputPath = path.join(__dirname, '../types/i18n.d.ts')
|
||||
|
||||
|
||||
if (checkMode) {
|
||||
// Check mode: compare with existing file
|
||||
if (!fs.existsSync(outputPath)) {
|
||||
console.error('❌ Type definitions file does not exist')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
|
||||
const existingContent = fs.readFileSync(outputPath, 'utf8')
|
||||
if (existingContent.trim() !== typeDefinitions.trim()) {
|
||||
console.error('❌ Type definitions are out of sync')
|
||||
console.error(' Run: pnpm run gen:i18n-types')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
|
||||
console.log('✅ Type definitions are in sync')
|
||||
} else {
|
||||
// Generate mode: write file
|
||||
fs.writeFileSync(outputPath, typeDefinitions)
|
||||
console.log(`✅ Generated type definitions: ${outputPath}`)
|
||||
}
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error:', error.message)
|
||||
process.exit(1)
|
||||
@@ -132,4 +132,4 @@ function main() {
|
||||
|
||||
if (require.main === module) {
|
||||
main()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
const translation = {
|
||||
title: 'Entdecken',
|
||||
sidebar: {
|
||||
discovery: 'Entdeckung',
|
||||
chat: 'Chat',
|
||||
workspace: 'Arbeitsbereich',
|
||||
action: {
|
||||
pin: 'Anheften',
|
||||
unpin: 'Lösen',
|
||||
@@ -16,12 +14,8 @@ const translation = {
|
||||
},
|
||||
},
|
||||
apps: {
|
||||
title: 'Apps von Dify erkunden',
|
||||
description: 'Nutzen Sie diese Vorlagen-Apps sofort oder passen Sie Ihre eigenen Apps basierend auf den Vorlagen an.',
|
||||
allCategories: 'Alle Kategorien',
|
||||
},
|
||||
appCard: {
|
||||
addToWorkspace: 'Zum Arbeitsbereich hinzufügen',
|
||||
customize: 'Anpassen',
|
||||
},
|
||||
appCustomize: {
|
||||
|
||||
@@ -672,6 +672,7 @@ const translation = {
|
||||
hitScore: 'Retrieval Score:',
|
||||
},
|
||||
inputPlaceholder: 'Talk to {{botName}}',
|
||||
inputDisabledPlaceholder: 'Preview Only',
|
||||
thinking: 'Thinking...',
|
||||
thought: 'Thought',
|
||||
resend: 'Resend',
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
const translation = {
|
||||
title: 'Explore',
|
||||
sidebar: {
|
||||
discovery: 'Discovery',
|
||||
title: 'App gallery',
|
||||
chat: 'Chat',
|
||||
workspace: 'Workspace',
|
||||
webApps: 'Web apps',
|
||||
action: {
|
||||
pin: 'Pin',
|
||||
unpin: 'Unpin',
|
||||
@@ -14,15 +14,31 @@ const translation = {
|
||||
title: 'Delete app',
|
||||
content: 'Are you sure you want to delete this app?',
|
||||
},
|
||||
noApps: {
|
||||
title: 'No web apps',
|
||||
description: 'Published web apps will appear here',
|
||||
learnMore: 'Learn more',
|
||||
},
|
||||
},
|
||||
apps: {
|
||||
title: 'Explore Apps',
|
||||
description: 'Use these template apps instantly or customize your own apps based on the templates.',
|
||||
allCategories: 'Recommended',
|
||||
title: 'Try Dify\'s curated apps to find AI solutions for your business',
|
||||
allCategories: 'All',
|
||||
resultNum: '{{num}} results',
|
||||
resetFilter: 'Clear filter',
|
||||
},
|
||||
appCard: {
|
||||
addToWorkspace: 'Add to Workspace',
|
||||
customize: 'Customize',
|
||||
addToWorkspace: 'Use template',
|
||||
try: 'Details',
|
||||
},
|
||||
tryApp: {
|
||||
tabHeader: {
|
||||
try: 'Try it',
|
||||
detail: 'Orchestration Details',
|
||||
},
|
||||
createFromSampleApp: 'Create from this sample app',
|
||||
category: 'Category',
|
||||
requirements: 'Requirements',
|
||||
tryInfo: 'This is a sample app. You can try up to 5 messages. To keep using it, click "Create form this sample app" and set it up!',
|
||||
},
|
||||
appCustomize: {
|
||||
title: 'Create app from {{name}}',
|
||||
@@ -39,6 +55,9 @@ const translation = {
|
||||
Workflow: 'Workflow',
|
||||
Entertainment: 'Entertainment',
|
||||
},
|
||||
banner: {
|
||||
viewMore: 'VIEW MORE',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
const translation = {
|
||||
title: 'Explorar',
|
||||
sidebar: {
|
||||
discovery: 'Descubrimiento',
|
||||
chat: 'Chat',
|
||||
workspace: 'Espacio de trabajo',
|
||||
action: {
|
||||
pin: 'Anclar',
|
||||
unpin: 'Desanclar',
|
||||
@@ -16,12 +14,8 @@ const translation = {
|
||||
},
|
||||
},
|
||||
apps: {
|
||||
title: 'Explorar aplicaciones de Dify',
|
||||
description: 'Utiliza estas aplicaciones de plantilla al instante o personaliza tus propias aplicaciones basadas en las plantillas.',
|
||||
allCategories: 'Recomendado',
|
||||
},
|
||||
appCard: {
|
||||
addToWorkspace: 'Agregar al espacio de trabajo',
|
||||
customize: 'Personalizar',
|
||||
},
|
||||
appCustomize: {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
const translation = {
|
||||
title: 'کاوش',
|
||||
sidebar: {
|
||||
discovery: 'کشف',
|
||||
chat: 'چت',
|
||||
workspace: 'فضای کاری',
|
||||
action: {
|
||||
pin: 'سنجاق کردن',
|
||||
unpin: 'برداشتن سنجاق',
|
||||
@@ -16,12 +14,8 @@ const translation = {
|
||||
},
|
||||
},
|
||||
apps: {
|
||||
title: 'کاوش برنامهها توسط دیفی',
|
||||
description: 'از این برنامههای قالبی بلافاصله استفاده کنید یا برنامههای خود را بر اساس این قالبها سفارشی کنید.',
|
||||
allCategories: 'پیشنهاد شده',
|
||||
},
|
||||
appCard: {
|
||||
addToWorkspace: 'افزودن به فضای کاری',
|
||||
customize: 'سفارشی کردن',
|
||||
},
|
||||
appCustomize: {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
const translation = {
|
||||
title: 'Explorer',
|
||||
sidebar: {
|
||||
discovery: 'Découverte',
|
||||
chat: 'Discussion',
|
||||
workspace: 'Espace de travail',
|
||||
action: {
|
||||
pin: 'Épingle',
|
||||
unpin: 'Détacher',
|
||||
@@ -16,12 +14,8 @@ const translation = {
|
||||
},
|
||||
},
|
||||
apps: {
|
||||
title: 'Explorez les applications par Dify',
|
||||
description: 'Utilisez ces applications modèles instantanément ou personnalisez vos propres applications basées sur les modèles.',
|
||||
allCategories: 'Recommandé',
|
||||
},
|
||||
appCard: {
|
||||
addToWorkspace: 'Ajouter à l\'espace de travail',
|
||||
customize: 'Personnaliser',
|
||||
},
|
||||
appCustomize: {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
const translation = {
|
||||
title: 'अन्वेषण करें',
|
||||
sidebar: {
|
||||
discovery: 'खोज',
|
||||
chat: 'चैट',
|
||||
workspace: 'कार्यक्षेत्र',
|
||||
action: {
|
||||
pin: 'पिन करें',
|
||||
unpin: 'पिन हटाएँ',
|
||||
@@ -16,13 +14,8 @@ const translation = {
|
||||
},
|
||||
},
|
||||
apps: {
|
||||
title: 'डिफ़ी द्वारा ऐप्स का अन्वेषण करें',
|
||||
description:
|
||||
'इन टेम्प्लेट ऐप्स का तुरंत उपयोग करें या टेम्प्लेट्स के आधार पर अपने स्वयं के ऐप्स को कस्टमाइज़ करें।',
|
||||
allCategories: 'अनुशंसित',
|
||||
},
|
||||
appCard: {
|
||||
addToWorkspace: 'कार्यक्षेत्र में जोड़ें',
|
||||
customize: 'अनुकूलित करें',
|
||||
},
|
||||
appCustomize: {
|
||||
|
||||
@@ -10,18 +10,12 @@ const translation = {
|
||||
content: 'Apakah Anda yakin ingin menghapus aplikasi ini?',
|
||||
title: 'Hapus aplikasi',
|
||||
},
|
||||
workspace: 'Workspace',
|
||||
discovery: 'Penemuan',
|
||||
chat: 'Mengobrol',
|
||||
},
|
||||
apps: {
|
||||
allCategories: 'Direkomendasikan',
|
||||
description: 'Gunakan aplikasi templat ini secara instan atau sesuaikan aplikasi Anda sendiri berdasarkan templat.',
|
||||
title: 'Jelajahi Aplikasi',
|
||||
},
|
||||
appCard: {
|
||||
customize: 'Menyesuaikan',
|
||||
addToWorkspace: 'Tambahkan ke Ruang Kerja',
|
||||
},
|
||||
appCustomize: {
|
||||
subTitle: 'Ikon & nama aplikasi',
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
const translation = {
|
||||
title: 'Esplora',
|
||||
sidebar: {
|
||||
discovery: 'Scoperta',
|
||||
chat: 'Chat',
|
||||
workspace: 'Workspace',
|
||||
action: {
|
||||
pin: 'Fissa',
|
||||
unpin: 'Sblocca',
|
||||
@@ -16,13 +14,8 @@ const translation = {
|
||||
},
|
||||
},
|
||||
apps: {
|
||||
title: 'Esplora App di Dify',
|
||||
description:
|
||||
'Usa queste app modello istantaneamente o personalizza le tue app basate sui modelli.',
|
||||
allCategories: 'Consigliato',
|
||||
},
|
||||
appCard: {
|
||||
addToWorkspace: 'Aggiungi a Workspace',
|
||||
customize: 'Personalizza',
|
||||
},
|
||||
appCustomize: {
|
||||
|
||||
@@ -656,6 +656,7 @@ const translation = {
|
||||
hitScore: '検索スコア:',
|
||||
},
|
||||
inputPlaceholder: '{{botName}} と話す',
|
||||
inputDisabledPlaceholder: 'プレビューのみ',
|
||||
thought: '思考',
|
||||
thinking: '考え中...',
|
||||
resend: '再送信してください',
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
const translation = {
|
||||
title: '探索',
|
||||
sidebar: {
|
||||
discovery: '探索',
|
||||
title: 'アプリギャラリー',
|
||||
chat: 'チャット',
|
||||
workspace: 'ワークスペース',
|
||||
webApps: 'Webアプリ',
|
||||
action: {
|
||||
pin: 'ピン留め',
|
||||
unpin: 'ピン留め解除',
|
||||
@@ -14,16 +14,33 @@ const translation = {
|
||||
title: 'アプリを削除',
|
||||
content: 'このアプリを削除してもよろしいですか?',
|
||||
},
|
||||
noApps: {
|
||||
title: 'Webアプリなし',
|
||||
description: '公開されたWebアプリがここに表示されます',
|
||||
learnMore: '詳細',
|
||||
},
|
||||
},
|
||||
apps: {
|
||||
title: 'アプリを探索',
|
||||
description: 'これらのテンプレートアプリを即座に使用するか、テンプレートに基づいて独自のアプリをカスタマイズしてください。',
|
||||
allCategories: '推奨',
|
||||
title: 'Difyの厳選アプリを試して、ビジネス向けのAIソリューションを見つけましょう',
|
||||
allCategories: '全て',
|
||||
resultNum: '{{num}}件の結果',
|
||||
resetFilter: 'クリア',
|
||||
},
|
||||
appCard: {
|
||||
addToWorkspace: 'ワークスペースに追加',
|
||||
addToWorkspace: 'テンプレートを使用',
|
||||
try: '詳細',
|
||||
customize: 'カスタマイズ',
|
||||
},
|
||||
tryApp: {
|
||||
tabHeader: {
|
||||
try: 'お試し',
|
||||
detail: 'オーケストレーション詳細',
|
||||
},
|
||||
createFromSampleApp: 'テンプレートから作成',
|
||||
category: 'カテゴリー',
|
||||
requirements: '必要項目',
|
||||
tryInfo: 'これはサンプルアプリです。最大5件のメッセージまでお試しいただけます。引き続き利用するには、「テンプレートから作成」 をクリックして設定を行ってください。',
|
||||
},
|
||||
appCustomize: {
|
||||
title: '{{name}}からアプリを作成',
|
||||
subTitle: 'アプリアイコンと名前',
|
||||
@@ -39,6 +56,9 @@ const translation = {
|
||||
Agent: 'エージェント',
|
||||
Entertainment: 'エンターテイメント',
|
||||
},
|
||||
banner: {
|
||||
viewMore: 'もっと見る',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
const translation = {
|
||||
title: '탐색',
|
||||
sidebar: {
|
||||
discovery: '탐색',
|
||||
chat: '채팅',
|
||||
workspace: '작업 공간',
|
||||
action: {
|
||||
pin: '고정',
|
||||
unpin: '고정 해제',
|
||||
@@ -16,12 +14,8 @@ const translation = {
|
||||
},
|
||||
},
|
||||
apps: {
|
||||
title: 'Dify 로 앱 탐색',
|
||||
description: '이 템플릿 앱을 즉시 사용하거나 템플릿을 기반으로 고유한 앱을 사용자 정의하세요.',
|
||||
allCategories: '모든 카테고리',
|
||||
},
|
||||
appCard: {
|
||||
addToWorkspace: '작업 공간에 추가',
|
||||
customize: '사용자 정의',
|
||||
},
|
||||
appCustomize: {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
const translation = {
|
||||
title: 'Odkryj',
|
||||
sidebar: {
|
||||
discovery: 'Odkrywanie',
|
||||
chat: 'Czat',
|
||||
workspace: 'Przestrzeń robocza',
|
||||
action: {
|
||||
pin: 'Przypnij',
|
||||
unpin: 'Odepnij',
|
||||
@@ -16,13 +14,8 @@ const translation = {
|
||||
},
|
||||
},
|
||||
apps: {
|
||||
title: 'Odkrywaj aplikacje stworzone przez Dify',
|
||||
description:
|
||||
'Wykorzystaj te aplikacje szablonowe natychmiast lub dostosuj własne aplikacje na podstawie szablonów.',
|
||||
allCategories: 'Polecane',
|
||||
},
|
||||
appCard: {
|
||||
addToWorkspace: 'Dodaj do przestrzeni roboczej',
|
||||
customize: 'Dostosuj',
|
||||
},
|
||||
appCustomize: {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
const translation = {
|
||||
title: 'Badać',
|
||||
sidebar: {
|
||||
discovery: 'Descoberta',
|
||||
chat: 'Chat',
|
||||
workspace: 'Espaço de Trabalho',
|
||||
action: {
|
||||
pin: 'Fixar',
|
||||
unpin: 'Desafixar',
|
||||
@@ -16,12 +14,8 @@ const translation = {
|
||||
},
|
||||
},
|
||||
apps: {
|
||||
title: 'Explorar Aplicações por Dify',
|
||||
description: 'Use esses aplicativos modelo instantaneamente ou personalize seus próprios aplicativos com base nos modelos.',
|
||||
allCategories: 'Recomendado',
|
||||
},
|
||||
appCard: {
|
||||
addToWorkspace: 'Adicionar ao Espaço de Trabalho',
|
||||
customize: 'Personalizar',
|
||||
},
|
||||
appCustomize: {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
const translation = {
|
||||
title: 'Explorați',
|
||||
sidebar: {
|
||||
discovery: 'Descoperire',
|
||||
chat: 'Chat',
|
||||
workspace: 'Spațiu de lucru',
|
||||
action: {
|
||||
pin: 'Fixați',
|
||||
unpin: 'Deblocați',
|
||||
@@ -16,12 +14,8 @@ const translation = {
|
||||
},
|
||||
},
|
||||
apps: {
|
||||
title: 'Explorați aplicațiile Dify',
|
||||
description: 'Utilizați aceste aplicații model imediat sau personalizați-vă propria aplicație pe baza modelelor.',
|
||||
allCategories: 'Recomandate',
|
||||
},
|
||||
appCard: {
|
||||
addToWorkspace: 'Adăugați la spațiul de lucru',
|
||||
customize: 'Personalizați',
|
||||
},
|
||||
appCustomize: {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
const translation = {
|
||||
title: 'Обзор',
|
||||
sidebar: {
|
||||
discovery: 'Открытия',
|
||||
chat: 'Чат',
|
||||
workspace: 'Рабочее пространство',
|
||||
action: {
|
||||
pin: 'Закрепить',
|
||||
unpin: 'Открепить',
|
||||
@@ -16,12 +14,8 @@ const translation = {
|
||||
},
|
||||
},
|
||||
apps: {
|
||||
title: 'Обзор приложений от Dify',
|
||||
description: 'Используйте эти шаблонные приложения мгновенно или настройте свои собственные приложения на основе шаблонов.',
|
||||
allCategories: 'Рекомендуемые',
|
||||
},
|
||||
appCard: {
|
||||
addToWorkspace: 'Добавить в рабочее пространство',
|
||||
customize: 'Настроить',
|
||||
},
|
||||
appCustomize: {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
const translation = {
|
||||
title: 'Razišči',
|
||||
sidebar: {
|
||||
discovery: 'Odkritja',
|
||||
chat: 'Klepet',
|
||||
workspace: 'Delovni prostor',
|
||||
action: {
|
||||
pin: 'Pripni',
|
||||
unpin: 'Odpni',
|
||||
@@ -16,12 +14,8 @@ const translation = {
|
||||
},
|
||||
},
|
||||
apps: {
|
||||
title: 'Razišči aplikacije Dify',
|
||||
description: 'Uporabite te predloge aplikacij takoj ali prilagodite svoje aplikacije na podlagi predlog.',
|
||||
allCategories: 'Priporočeno',
|
||||
},
|
||||
appCard: {
|
||||
addToWorkspace: 'Dodaj v delovni prostor',
|
||||
customize: 'Prilagodi',
|
||||
},
|
||||
appCustomize: {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
const translation = {
|
||||
title: 'สํารวจ',
|
||||
sidebar: {
|
||||
discovery: 'การค้นพบ',
|
||||
chat: 'สนทนา',
|
||||
workspace: 'พื้นที่',
|
||||
action: {
|
||||
pin: 'เข็มกลัด',
|
||||
unpin: 'ปลดหมุด',
|
||||
@@ -16,12 +14,8 @@ const translation = {
|
||||
},
|
||||
},
|
||||
apps: {
|
||||
title: 'สํารวจแอพโดย Dify',
|
||||
description: 'ใช้แอปเทมเพลตเหล่านี้ทันทีหรือปรับแต่งแอปของคุณเองตามเทมเพลต',
|
||||
allCategories: 'แนะ นำ',
|
||||
},
|
||||
appCard: {
|
||||
addToWorkspace: 'เพิ่มไปยังพื้นที่ทํางาน',
|
||||
customize: 'ปรับแต่ง',
|
||||
},
|
||||
appCustomize: {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
const translation = {
|
||||
title: 'Keşfet',
|
||||
sidebar: {
|
||||
discovery: 'Keşif',
|
||||
chat: 'Sohbet',
|
||||
workspace: 'Çalışma Alanı',
|
||||
action: {
|
||||
pin: 'Sabitle',
|
||||
unpin: 'Sabitlemeyi Kaldır',
|
||||
@@ -16,12 +14,8 @@ const translation = {
|
||||
},
|
||||
},
|
||||
apps: {
|
||||
title: 'Dify Tarafından Keşfet Uygulamaları',
|
||||
description: 'Bu şablon uygulamalarını anında kullanın veya şablonlara dayalı kendi uygulamalarınızı özelleştirin.',
|
||||
allCategories: 'Önerilen',
|
||||
},
|
||||
appCard: {
|
||||
addToWorkspace: 'Çalışma Alanına Ekle',
|
||||
customize: 'Özelleştir',
|
||||
},
|
||||
appCustomize: {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
const translation = {
|
||||
title: 'Досліджувати',
|
||||
sidebar: {
|
||||
discovery: 'Відкриття',
|
||||
chat: 'Чат',
|
||||
workspace: 'Робочий простір',
|
||||
action: {
|
||||
pin: 'Закріпити',
|
||||
unpin: 'Відкріпити',
|
||||
@@ -16,12 +14,8 @@ const translation = {
|
||||
},
|
||||
},
|
||||
apps: {
|
||||
title: 'Вивчайте програми від Dify',
|
||||
description: 'Використовуйте ці шаблони миттєво або налаштуйте власні програми на основі шаблонів.',
|
||||
allCategories: 'Рекомендовані',
|
||||
},
|
||||
appCard: {
|
||||
addToWorkspace: 'Додати до робочого простору',
|
||||
customize: 'Налаштувати',
|
||||
},
|
||||
appCustomize: {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
const translation = {
|
||||
title: 'Khám phá',
|
||||
sidebar: {
|
||||
discovery: 'Khám phá',
|
||||
chat: 'Trò chuyện',
|
||||
workspace: 'Không gian làm việc',
|
||||
action: {
|
||||
pin: 'Ghim',
|
||||
unpin: 'Bỏ ghim',
|
||||
@@ -16,12 +14,8 @@ const translation = {
|
||||
},
|
||||
},
|
||||
apps: {
|
||||
title: 'Khám phá ứng dụng bởi Dify',
|
||||
description: 'Sử dụng ngay các ứng dụng mẫu này hoặc tùy chỉnh ứng dụng của bạn dựa trên các mẫu có sẵn.',
|
||||
allCategories: 'Tất cả danh mục',
|
||||
},
|
||||
appCard: {
|
||||
addToWorkspace: 'Thêm vào không gian làm việc',
|
||||
customize: 'Tùy chỉnh',
|
||||
},
|
||||
appCustomize: {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user