mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 02:18:08 +08:00
Merge remote-tracking branch 'origin/main' into feat/tax-text
This commit is contained in:
@ -16,7 +16,7 @@ jest.mock('cmdk', () => ({
|
||||
Item: ({ children, onSelect, value, className }: any) => (
|
||||
<div
|
||||
className={className}
|
||||
onClick={() => onSelect && onSelect()}
|
||||
onClick={() => onSelect?.()}
|
||||
data-value={value}
|
||||
data-testid={`command-item-${value}`}
|
||||
>
|
||||
|
||||
@ -4,6 +4,7 @@ import React, { useCallback, useRef, useState } from 'react'
|
||||
|
||||
import type { PopupProps } from './config-popup'
|
||||
import ConfigPopup from './config-popup'
|
||||
import cn from '@/utils/classnames'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
@ -45,7 +46,7 @@ const ConfigBtn: FC<Props> = ({
|
||||
offset={12}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={handleTrigger}>
|
||||
<div className="select-none">
|
||||
<div className={cn('select-none', className)}>
|
||||
{children}
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
|
||||
@ -28,7 +28,8 @@ const CSVUploader: FC<Props> = ({
|
||||
const handleDragEnter = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.target !== dragRef.current && setDragging(true)
|
||||
if (e.target !== dragRef.current)
|
||||
setDragging(true)
|
||||
}
|
||||
const handleDragOver = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
@ -37,7 +38,8 @@ const CSVUploader: FC<Props> = ({
|
||||
const handleDragLeave = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.target === dragRef.current && setDragging(false)
|
||||
if (e.target === dragRef.current)
|
||||
setDragging(false)
|
||||
}
|
||||
const handleDrop = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
@ -38,7 +38,7 @@ const Annotation: FC<Props> = (props) => {
|
||||
const [isShowEdit, setIsShowEdit] = useState(false)
|
||||
const [annotationConfig, setAnnotationConfig] = useState<AnnotationReplyConfig | null>(null)
|
||||
const [isChatApp] = useState(appDetail.mode !== 'completion')
|
||||
const [controlRefreshSwitch, setControlRefreshSwitch] = useState(Date.now())
|
||||
const [controlRefreshSwitch, setControlRefreshSwitch] = useState(() => Date.now())
|
||||
const { plan, enableBilling } = useProviderContext()
|
||||
const isAnnotationFull = enableBilling && plan.usage.annotatedResponse >= plan.total.annotatedResponse
|
||||
const [isShowAnnotationFullModal, setIsShowAnnotationFullModal] = useState(false)
|
||||
@ -48,7 +48,7 @@ const Annotation: FC<Props> = (props) => {
|
||||
const [list, setList] = useState<AnnotationItem[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [controlUpdateList, setControlUpdateList] = useState(Date.now())
|
||||
const [controlUpdateList, setControlUpdateList] = useState(() => Date.now())
|
||||
const [currItem, setCurrItem] = useState<AnnotationItem | null>(null)
|
||||
const [isShowViewModal, setIsShowViewModal] = useState(false)
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([])
|
||||
|
||||
@ -348,7 +348,8 @@ const AppPublisher = ({
|
||||
<SuggestedAction
|
||||
className='flex-1'
|
||||
onClick={() => {
|
||||
publishedAt && handleOpenInExplore()
|
||||
if (publishedAt)
|
||||
handleOpenInExplore()
|
||||
}}
|
||||
disabled={!publishedAt || (systemFeatures.webapp_auth.enabled && !userCanAccessApp?.result)}
|
||||
icon={<RiPlanetLine className='h-4 w-4' />}
|
||||
|
||||
@ -40,7 +40,8 @@ const VersionInfoModal: FC<VersionInfoModalProps> = ({
|
||||
return
|
||||
}
|
||||
else {
|
||||
titleError && setTitleError(false)
|
||||
if (titleError)
|
||||
setTitleError(false)
|
||||
}
|
||||
|
||||
if (releaseNotes.length > RELEASE_NOTES_MAX_LENGTH) {
|
||||
@ -52,7 +53,8 @@ const VersionInfoModal: FC<VersionInfoModalProps> = ({
|
||||
return
|
||||
}
|
||||
else {
|
||||
releaseNotesError && setReleaseNotesError(false)
|
||||
if (releaseNotesError)
|
||||
setReleaseNotesError(false)
|
||||
}
|
||||
|
||||
onPublish({ title, releaseNotes, id: versionInfo?.id })
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
import type { SVGProps } from 'react'
|
||||
|
||||
const CitationIcon = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M7 6h10M7 12h6M7 18h10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M5 6c0-1.105.895-2 2-2h10c1.105 0 2 .895 2 2v12c0 1.105-.895 2-2 2H9l-4 3v-3H7"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default CitationIcon
|
||||
|
||||
@ -25,7 +25,7 @@ const PromptEditorHeightResizeWrap: FC<Props> = ({
|
||||
}) => {
|
||||
const [clientY, setClientY] = useState(0)
|
||||
const [isResizing, setIsResizing] = useState(false)
|
||||
const [prevUserSelectStyle, setPrevUserSelectStyle] = useState(getComputedStyle(document.body).userSelect)
|
||||
const [prevUserSelectStyle, setPrevUserSelectStyle] = useState(() => getComputedStyle(document.body).userSelect)
|
||||
const [oldHeight, setOldHeight] = useState(height)
|
||||
|
||||
const handleStartResize = useCallback((e: React.MouseEvent<HTMLElement>) => {
|
||||
|
||||
@ -32,6 +32,19 @@ import { TransferMethod } from '@/types/app'
|
||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
|
||||
const TEXT_MAX_LENGTH = 256
|
||||
const CHECKBOX_DEFAULT_TRUE_VALUE = 'true'
|
||||
const CHECKBOX_DEFAULT_FALSE_VALUE = 'false'
|
||||
|
||||
const getCheckboxDefaultSelectValue = (value: InputVar['default']) => {
|
||||
if (typeof value === 'boolean')
|
||||
return value ? CHECKBOX_DEFAULT_TRUE_VALUE : CHECKBOX_DEFAULT_FALSE_VALUE
|
||||
if (typeof value === 'string')
|
||||
return value.toLowerCase() === CHECKBOX_DEFAULT_TRUE_VALUE ? CHECKBOX_DEFAULT_TRUE_VALUE : CHECKBOX_DEFAULT_FALSE_VALUE
|
||||
return CHECKBOX_DEFAULT_FALSE_VALUE
|
||||
}
|
||||
|
||||
const parseCheckboxSelectValue = (value: string) =>
|
||||
value === CHECKBOX_DEFAULT_TRUE_VALUE
|
||||
|
||||
export type IConfigModalProps = {
|
||||
isCreate?: boolean
|
||||
@ -53,7 +66,7 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
||||
}) => {
|
||||
const { modelConfig } = useContext(ConfigContext)
|
||||
const { t } = useTranslation()
|
||||
const [tempPayload, setTempPayload] = useState<InputVar>(payload || getNewVarInWorkflow('') as any)
|
||||
const [tempPayload, setTempPayload] = useState<InputVar>(() => payload || getNewVarInWorkflow('') as any)
|
||||
const { type, label, variable, options, max_length } = tempPayload
|
||||
const modalRef = useRef<HTMLDivElement>(null)
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
@ -66,7 +79,7 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(tempPayload.json_schema).properties, null, 2)
|
||||
}
|
||||
catch (_e) {
|
||||
catch {
|
||||
return ''
|
||||
}
|
||||
}, [tempPayload.json_schema])
|
||||
@ -110,7 +123,7 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
||||
}
|
||||
handlePayloadChange('json_schema')(JSON.stringify(res, null, 2))
|
||||
}
|
||||
catch (_e) {
|
||||
catch {
|
||||
return null
|
||||
}
|
||||
}, [handlePayloadChange])
|
||||
@ -198,6 +211,8 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
||||
handlePayloadChange('variable')(e.target.value)
|
||||
}, [handlePayloadChange, t])
|
||||
|
||||
const checkboxDefaultSelectValue = useMemo(() => getCheckboxDefaultSelectValue(tempPayload.default), [tempPayload.default])
|
||||
|
||||
const handleConfirm = () => {
|
||||
const moreInfo = tempPayload.variable === payload?.variable
|
||||
? undefined
|
||||
@ -324,6 +339,23 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
||||
</Field>
|
||||
)}
|
||||
|
||||
{type === InputVarType.checkbox && (
|
||||
<Field title={t('appDebug.variableConfig.defaultValue')}>
|
||||
<SimpleSelect
|
||||
className="w-full"
|
||||
optionWrapClassName="max-h-[140px] overflow-y-auto"
|
||||
items={[
|
||||
{ value: CHECKBOX_DEFAULT_TRUE_VALUE, name: t('appDebug.variableConfig.startChecked') },
|
||||
{ value: CHECKBOX_DEFAULT_FALSE_VALUE, name: t('appDebug.variableConfig.noDefaultSelected') },
|
||||
]}
|
||||
defaultValue={checkboxDefaultSelectValue}
|
||||
onSelect={item => handlePayloadChange('default')(parseCheckboxSelectValue(String(item.value)))}
|
||||
placeholder={t('appDebug.variableConfig.selectDefaultValue')}
|
||||
allowSearch={false}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
{type === InputVarType.select && (
|
||||
<>
|
||||
<Field title={t('appDebug.variableConfig.options')}>
|
||||
|
||||
@ -65,13 +65,40 @@ const DatasetConfig: FC = () => {
|
||||
const onRemove = (id: string) => {
|
||||
const filteredDataSets = dataSet.filter(item => item.id !== id)
|
||||
setDataSet(filteredDataSets)
|
||||
const retrievalConfig = getMultipleRetrievalConfig(datasetConfigs as any, filteredDataSets, dataSet, {
|
||||
const { datasets, retrieval_model, score_threshold_enabled, ...restConfigs } = datasetConfigs
|
||||
const {
|
||||
top_k,
|
||||
score_threshold,
|
||||
reranking_model,
|
||||
reranking_mode,
|
||||
weights,
|
||||
reranking_enable,
|
||||
} = restConfigs
|
||||
const oldRetrievalConfig = {
|
||||
top_k,
|
||||
score_threshold,
|
||||
reranking_model: (reranking_model.reranking_provider_name && reranking_model.reranking_model_name) ? {
|
||||
provider: reranking_model.reranking_provider_name,
|
||||
model: reranking_model.reranking_model_name,
|
||||
} : undefined,
|
||||
reranking_mode,
|
||||
weights,
|
||||
reranking_enable,
|
||||
}
|
||||
const retrievalConfig = getMultipleRetrievalConfig(oldRetrievalConfig, filteredDataSets, dataSet, {
|
||||
provider: currentRerankProvider?.provider,
|
||||
model: currentRerankModel?.model,
|
||||
})
|
||||
setDatasetConfigs({
|
||||
...(datasetConfigs as any),
|
||||
...datasetConfigsRef.current,
|
||||
...retrievalConfig,
|
||||
reranking_model: {
|
||||
reranking_provider_name: retrievalConfig?.reranking_model?.provider || '',
|
||||
reranking_model_name: retrievalConfig?.reranking_model?.model || '',
|
||||
},
|
||||
retrieval_model,
|
||||
score_threshold_enabled,
|
||||
datasets,
|
||||
})
|
||||
const {
|
||||
allExternal,
|
||||
|
||||
@ -30,11 +30,11 @@ import { noop } from 'lodash-es'
|
||||
type Props = {
|
||||
datasetConfigs: DatasetConfigs
|
||||
onChange: (configs: DatasetConfigs, isRetrievalModeChange?: boolean) => void
|
||||
selectedDatasets?: DataSet[]
|
||||
isInWorkflow?: boolean
|
||||
singleRetrievalModelConfig?: ModelConfig
|
||||
onSingleRetrievalModelChange?: (config: ModelConfig) => void
|
||||
onSingleRetrievalModelParamsChange?: (config: ModelConfig) => void
|
||||
selectedDatasets?: DataSet[]
|
||||
}
|
||||
|
||||
const ConfigContent: FC<Props> = ({
|
||||
@ -61,22 +61,28 @@ const ConfigContent: FC<Props> = ({
|
||||
|
||||
const {
|
||||
modelList: rerankModelList,
|
||||
currentModel: validDefaultRerankModel,
|
||||
currentProvider: validDefaultRerankProvider,
|
||||
} = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.rerank)
|
||||
|
||||
/**
|
||||
* If reranking model is set and is valid, use the reranking model
|
||||
* Otherwise, check if the default reranking model is valid
|
||||
*/
|
||||
const {
|
||||
currentModel: currentRerankModel,
|
||||
} = useCurrentProviderAndModel(
|
||||
rerankModelList,
|
||||
{
|
||||
provider: datasetConfigs.reranking_model?.reranking_provider_name,
|
||||
model: datasetConfigs.reranking_model?.reranking_model_name,
|
||||
provider: datasetConfigs.reranking_model?.reranking_provider_name || validDefaultRerankProvider?.provider || '',
|
||||
model: datasetConfigs.reranking_model?.reranking_model_name || validDefaultRerankModel?.model || '',
|
||||
},
|
||||
)
|
||||
|
||||
const rerankModel = useMemo(() => {
|
||||
return {
|
||||
provider_name: datasetConfigs?.reranking_model?.reranking_provider_name ?? '',
|
||||
model_name: datasetConfigs?.reranking_model?.reranking_model_name ?? '',
|
||||
provider_name: datasetConfigs.reranking_model?.reranking_provider_name ?? '',
|
||||
model_name: datasetConfigs.reranking_model?.reranking_model_name ?? '',
|
||||
}
|
||||
}, [datasetConfigs.reranking_model])
|
||||
|
||||
@ -135,7 +141,7 @@ const ConfigContent: FC<Props> = ({
|
||||
})
|
||||
}
|
||||
|
||||
const model = singleRetrievalConfig
|
||||
const model = singleRetrievalConfig // Legacy code, for compatibility, have to keep it
|
||||
|
||||
const rerankingModeOptions = [
|
||||
{
|
||||
@ -158,7 +164,7 @@ const ConfigContent: FC<Props> = ({
|
||||
|
||||
const canManuallyToggleRerank = useMemo(() => {
|
||||
return (selectedDatasetsMode.allInternal && selectedDatasetsMode.allEconomic)
|
||||
|| selectedDatasetsMode.allExternal
|
||||
|| selectedDatasetsMode.allExternal
|
||||
}, [selectedDatasetsMode.allEconomic, selectedDatasetsMode.allExternal, selectedDatasetsMode.allInternal])
|
||||
|
||||
const showRerankModel = useMemo(() => {
|
||||
@ -168,7 +174,7 @@ const ConfigContent: FC<Props> = ({
|
||||
return datasetConfigs.reranking_enable
|
||||
}, [datasetConfigs.reranking_enable, canManuallyToggleRerank])
|
||||
|
||||
const handleDisabledSwitchClick = useCallback((enable: boolean) => {
|
||||
const handleManuallyToggleRerank = useCallback((enable: boolean) => {
|
||||
if (!currentRerankModel && enable)
|
||||
Toast.notify({ type: 'error', message: t('workflow.errorMsg.rerankModelRequired') })
|
||||
onChange({
|
||||
@ -255,12 +261,11 @@ const ConfigContent: FC<Props> = ({
|
||||
<div className='mt-2'>
|
||||
<div className='flex items-center'>
|
||||
{
|
||||
selectedDatasetsMode.allEconomic && !selectedDatasetsMode.mixtureInternalAndExternal && (
|
||||
canManuallyToggleRerank && (
|
||||
<Switch
|
||||
size='md'
|
||||
defaultValue={showRerankModel}
|
||||
disabled={!canManuallyToggleRerank}
|
||||
onChange={handleDisabledSwitchClick}
|
||||
onChange={handleManuallyToggleRerank}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -35,8 +35,8 @@ const useAdvancedPromptConfig = ({
|
||||
setStop,
|
||||
}: Param) => {
|
||||
const isAdvancedPrompt = promptMode === PromptMode.advanced
|
||||
const [chatPromptConfig, setChatPromptConfig] = useState<ChatPromptConfig>(clone(DEFAULT_CHAT_PROMPT_CONFIG))
|
||||
const [completionPromptConfig, setCompletionPromptConfig] = useState<CompletionPromptConfig>(clone(DEFAULT_COMPLETION_PROMPT_CONFIG))
|
||||
const [chatPromptConfig, setChatPromptConfig] = useState<ChatPromptConfig>(() => clone(DEFAULT_CHAT_PROMPT_CONFIG))
|
||||
const [completionPromptConfig, setCompletionPromptConfig] = useState<CompletionPromptConfig>(() => clone(DEFAULT_COMPLETION_PROMPT_CONFIG))
|
||||
|
||||
const currentAdvancedPrompt = (() => {
|
||||
if (!isAdvancedPrompt)
|
||||
|
||||
@ -284,18 +284,28 @@ const Configuration: FC = () => {
|
||||
setRerankSettingModalOpen(true)
|
||||
|
||||
const { datasets, retrieval_model, score_threshold_enabled, ...restConfigs } = datasetConfigs
|
||||
const {
|
||||
top_k,
|
||||
score_threshold,
|
||||
reranking_model,
|
||||
reranking_mode,
|
||||
weights,
|
||||
reranking_enable,
|
||||
} = restConfigs
|
||||
|
||||
const retrievalConfig = getMultipleRetrievalConfig({
|
||||
top_k: restConfigs.top_k,
|
||||
score_threshold: restConfigs.score_threshold,
|
||||
reranking_model: restConfigs.reranking_model && {
|
||||
provider: restConfigs.reranking_model.reranking_provider_name,
|
||||
model: restConfigs.reranking_model.reranking_model_name,
|
||||
},
|
||||
reranking_mode: restConfigs.reranking_mode,
|
||||
weights: restConfigs.weights,
|
||||
reranking_enable: restConfigs.reranking_enable,
|
||||
}, newDatasets, dataSets, {
|
||||
const oldRetrievalConfig = {
|
||||
top_k,
|
||||
score_threshold,
|
||||
reranking_model: (reranking_model.reranking_provider_name && reranking_model.reranking_model_name) ? {
|
||||
provider: reranking_model.reranking_provider_name,
|
||||
model: reranking_model.reranking_model_name,
|
||||
} : undefined,
|
||||
reranking_mode,
|
||||
weights,
|
||||
reranking_enable,
|
||||
}
|
||||
|
||||
const retrievalConfig = getMultipleRetrievalConfig(oldRetrievalConfig, newDatasets, dataSets, {
|
||||
provider: currentRerankProvider?.provider,
|
||||
model: currentRerankModel?.model,
|
||||
})
|
||||
@ -470,7 +480,7 @@ const Configuration: FC = () => {
|
||||
Toast.notify({ type: 'warning', message: `${t('common.modelProvider.parametersInvalidRemoved')}: ${Object.entries(removedDetails).map(([k, reason]) => `${k} (${reason})`).join(', ')}` })
|
||||
setCompletionParams(filtered)
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
Toast.notify({ type: 'error', message: t('common.error') })
|
||||
setCompletionParams({})
|
||||
}
|
||||
|
||||
@ -192,7 +192,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={canNotRun}
|
||||
onClick={() => onSend && onSend()}
|
||||
onClick={() => onSend?.()}
|
||||
className="w-[96px]">
|
||||
<RiPlayLargeFill className="mr-0.5 h-4 w-4 shrink-0" aria-hidden="true" />
|
||||
{t('appDebug.inputs.run')}
|
||||
@ -203,7 +203,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={canNotRun}
|
||||
onClick={() => onSend && onSend()}
|
||||
onClick={() => onSend?.()}
|
||||
className="w-[96px]">
|
||||
<RiPlayLargeFill className="mr-0.5 h-4 w-4 shrink-0" aria-hidden="true" />
|
||||
{t('appDebug.inputs.run')}
|
||||
|
||||
@ -38,7 +38,8 @@ const Uploader: FC<Props> = ({
|
||||
const handleDragEnter = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.target !== dragRef.current && setDragging(true)
|
||||
if (e.target !== dragRef.current)
|
||||
setDragging(true)
|
||||
}
|
||||
const handleDragOver = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
@ -47,7 +48,8 @@ const Uploader: FC<Props> = ({
|
||||
const handleDragLeave = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.target === dragRef.current && setDragging(false)
|
||||
if (e.target === dragRef.current)
|
||||
setDragging(false)
|
||||
}
|
||||
const handleDrop = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
@ -107,7 +107,8 @@ const Chart: React.FC<IChartProps> = ({
|
||||
const { t } = useTranslation()
|
||||
const statistics = chartData.data
|
||||
const statisticsLen = statistics.length
|
||||
const extraDataForMarkLine = new Array(statisticsLen >= 2 ? statisticsLen - 2 : statisticsLen).fill('1')
|
||||
const markLineLength = statisticsLen >= 2 ? statisticsLen - 2 : statisticsLen
|
||||
const extraDataForMarkLine = Array.from({ length: markLineLength }, () => '1')
|
||||
extraDataForMarkLine.push('')
|
||||
extraDataForMarkLine.unshift('')
|
||||
|
||||
|
||||
@ -127,7 +127,7 @@ export default class AudioPlayer {
|
||||
}
|
||||
catch {
|
||||
this.isLoadData = false
|
||||
this.callback && this.callback('error')
|
||||
this.callback?.('error')
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,15 +137,14 @@ export default class AudioPlayer {
|
||||
if (this.audioContext.state === 'suspended') {
|
||||
this.audioContext.resume().then((_) => {
|
||||
this.audio.play()
|
||||
this.callback && this.callback('play')
|
||||
this.callback?.('play')
|
||||
})
|
||||
}
|
||||
else if (this.audio.ended) {
|
||||
this.audio.play()
|
||||
this.callback && this.callback('play')
|
||||
this.callback?.('play')
|
||||
}
|
||||
if (this.callback)
|
||||
this.callback('play')
|
||||
this.callback?.('play')
|
||||
}
|
||||
else {
|
||||
this.isLoadData = true
|
||||
@ -189,24 +188,24 @@ export default class AudioPlayer {
|
||||
if (this.audio.paused) {
|
||||
this.audioContext.resume().then((_) => {
|
||||
this.audio.play()
|
||||
this.callback && this.callback('play')
|
||||
this.callback?.('play')
|
||||
})
|
||||
}
|
||||
else if (this.audio.ended) {
|
||||
this.audio.play()
|
||||
this.callback && this.callback('play')
|
||||
this.callback?.('play')
|
||||
}
|
||||
else if (this.audio.played) { /* empty */ }
|
||||
|
||||
else {
|
||||
this.audio.play()
|
||||
this.callback && this.callback('play')
|
||||
this.callback?.('play')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public pauseAudio() {
|
||||
this.callback && this.callback('paused')
|
||||
this.callback?.('paused')
|
||||
this.audio.pause()
|
||||
this.audioContext.suspend()
|
||||
}
|
||||
|
||||
@ -128,7 +128,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
const localState = localStorage.getItem('webappSidebarCollapse')
|
||||
return localState === 'collapsed'
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
// localStorage may be disabled in private browsing mode or by security settings
|
||||
// fallback to default value
|
||||
return false
|
||||
@ -142,7 +142,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
try {
|
||||
localStorage.setItem('webappSidebarCollapse', state ? 'collapsed' : 'expanded')
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
// localStorage may be disabled, continue without persisting state
|
||||
}
|
||||
}
|
||||
@ -235,13 +235,15 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
}
|
||||
}
|
||||
|
||||
if(item.checkbox) {
|
||||
if (item.checkbox) {
|
||||
const preset = initInputs[item.checkbox.variable] === true
|
||||
return {
|
||||
...item.checkbox,
|
||||
default: false,
|
||||
default: preset || item.default || item.checkbox.default,
|
||||
type: 'checkbox',
|
||||
}
|
||||
}
|
||||
|
||||
if (item.select) {
|
||||
const isInputInOptions = item.select.options.includes(initInputs[item.select.variable])
|
||||
return {
|
||||
|
||||
@ -101,10 +101,14 @@ const Answer: FC<AnswerProps> = ({
|
||||
}, [])
|
||||
|
||||
const handleSwitchSibling = useCallback((direction: 'prev' | 'next') => {
|
||||
if (direction === 'prev')
|
||||
item.prevSibling && switchSibling?.(item.prevSibling)
|
||||
else
|
||||
item.nextSibling && switchSibling?.(item.nextSibling)
|
||||
if (direction === 'prev') {
|
||||
if (item.prevSibling)
|
||||
switchSibling?.(item.prevSibling)
|
||||
}
|
||||
else {
|
||||
if (item.nextSibling)
|
||||
switchSibling?.(item.nextSibling)
|
||||
}
|
||||
}, [switchSibling, item.prevSibling, item.nextSibling])
|
||||
|
||||
return (
|
||||
|
||||
@ -160,8 +160,13 @@ const Chat: FC<ChatProps> = ({
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('resize', debounce(handleWindowResize))
|
||||
return () => window.removeEventListener('resize', handleWindowResize)
|
||||
const debouncedHandler = debounce(handleWindowResize, 200)
|
||||
window.addEventListener('resize', debouncedHandler)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', debouncedHandler)
|
||||
debouncedHandler.cancel()
|
||||
}
|
||||
}, [handleWindowResize])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
.dot-flashing {
|
||||
position: relative;
|
||||
animation: 1s infinite linear alternate;
|
||||
animation: dot-flashing 1s infinite linear alternate;
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
animation: 1s infinite linear alternate;
|
||||
animation: dot-flashing 1s infinite linear alternate;
|
||||
}
|
||||
|
||||
.dot-flashing::before {
|
||||
@ -51,15 +51,21 @@
|
||||
border-radius: 50%;
|
||||
background-color: #667085;
|
||||
color: #667085;
|
||||
animation-name: dot-flashing;
|
||||
animation: dot-flashing 1s infinite linear alternate;
|
||||
}
|
||||
|
||||
.text {
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
|
||||
.text::before {
|
||||
left: -7px;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.text::after {
|
||||
left: 7px;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
.avatar,
|
||||
@ -70,13 +76,19 @@
|
||||
border-radius: 50%;
|
||||
background-color: #155EEF;
|
||||
color: #155EEF;
|
||||
animation-name: dot-flashing-avatar;
|
||||
animation: dot-flashing-avatar 1s infinite linear alternate;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
|
||||
.avatar::before {
|
||||
left: -5px;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.avatar::after {
|
||||
left: 5px;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
@ -73,10 +73,14 @@ const Question: FC<QuestionProps> = ({
|
||||
}, [content])
|
||||
|
||||
const handleSwitchSibling = useCallback((direction: 'prev' | 'next') => {
|
||||
if (direction === 'prev')
|
||||
item.prevSibling && switchSibling?.(item.prevSibling)
|
||||
else
|
||||
item.nextSibling && switchSibling?.(item.nextSibling)
|
||||
if (direction === 'prev') {
|
||||
if (item.prevSibling)
|
||||
switchSibling?.(item.prevSibling)
|
||||
}
|
||||
else {
|
||||
if (item.nextSibling)
|
||||
switchSibling?.(item.nextSibling)
|
||||
}
|
||||
}, [switchSibling, item.prevSibling, item.nextSibling])
|
||||
|
||||
const getContentWidth = () => {
|
||||
|
||||
@ -195,13 +195,16 @@ export const useEmbeddedChatbot = () => {
|
||||
type: 'number',
|
||||
}
|
||||
}
|
||||
|
||||
if (item.checkbox) {
|
||||
const preset = initInputs[item.checkbox.variable] === true
|
||||
return {
|
||||
...item.checkbox,
|
||||
default: false,
|
||||
default: preset || item.default || item.checkbox.default,
|
||||
type: 'checkbox',
|
||||
}
|
||||
}
|
||||
|
||||
if (item.select) {
|
||||
const isInputInOptions = item.select.options.includes(initInputs[item.select.variable])
|
||||
return {
|
||||
|
||||
@ -55,8 +55,8 @@ const DatePicker = ({
|
||||
const [currentDate, setCurrentDate] = useState(inputValue || defaultValue)
|
||||
const [selectedDate, setSelectedDate] = useState(inputValue)
|
||||
|
||||
const [selectedMonth, setSelectedMonth] = useState((inputValue || defaultValue).month())
|
||||
const [selectedYear, setSelectedYear] = useState((inputValue || defaultValue).year())
|
||||
const [selectedMonth, setSelectedMonth] = useState(() => (inputValue || defaultValue).month())
|
||||
const [selectedYear, setSelectedYear] = useState(() => (inputValue || defaultValue).year())
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
|
||||
@ -28,7 +28,7 @@ const TimePicker = ({
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const isInitial = useRef(true)
|
||||
const [selectedTime, setSelectedTime] = useState(value ? getDateWithTimezone({ timezone, date: value }) : undefined)
|
||||
const [selectedTime, setSelectedTime] = useState(() => value ? getDateWithTimezone({ timezone, date: value }) : undefined)
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
|
||||
@ -124,7 +124,7 @@ export const parseDateWithFormat = (dateString: string, format?: string): Dayjs
|
||||
}
|
||||
|
||||
// Format date output with localization support
|
||||
export const formatDateForOutput = (date: Dayjs, includeTime: boolean = false, locale: string = 'en-US'): string => {
|
||||
export const formatDateForOutput = (date: Dayjs, includeTime: boolean = false, _locale: string = 'en-US'): string => {
|
||||
if (!date || !date.isValid()) return ''
|
||||
|
||||
if (includeTime) {
|
||||
|
||||
@ -47,7 +47,10 @@ export default function Drawer({
|
||||
<Dialog
|
||||
unmount={unmount}
|
||||
open={isOpen}
|
||||
onClose={() => !clickOutsideNotOpen && onClose()}
|
||||
onClose={() => {
|
||||
if (!clickOutsideNotOpen)
|
||||
onClose()
|
||||
}}
|
||||
className={cn('fixed inset-0 z-[30] overflow-y-auto', dialogClassName)}
|
||||
>
|
||||
<div className={cn('flex h-screen w-screen justify-end', positionCenter && '!justify-center')}>
|
||||
@ -55,7 +58,8 @@ export default function Drawer({
|
||||
<DialogBackdrop
|
||||
className={cn('fixed inset-0 z-[40]', mask && 'bg-black/30', dialogBackdropClassName)}
|
||||
onClick={() => {
|
||||
!clickOutsideNotOpen && onClose()
|
||||
if (!clickOutsideNotOpen)
|
||||
onClose()
|
||||
}}
|
||||
/>
|
||||
<div className={cn('relative z-[50] flex w-full max-w-sm flex-col justify-between overflow-hidden bg-components-panel-bg p-6 text-left align-middle shadow-xl', panelClassName)}>
|
||||
@ -80,11 +84,11 @@ export default function Drawer({
|
||||
<Button
|
||||
className='mr-2'
|
||||
onClick={() => {
|
||||
onCancel && onCancel()
|
||||
onCancel?.()
|
||||
}}>{t('common.operation.cancel')}</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onOk && onOk()
|
||||
onOk?.()
|
||||
}}>{t('common.operation.save')}</Button>
|
||||
</div>)}
|
||||
</div>
|
||||
|
||||
@ -45,7 +45,7 @@ const EmojiPicker: FC<IEmojiPickerProps> = ({
|
||||
<Divider className='mb-0 mt-3' />
|
||||
<div className='flex w-full items-center justify-center gap-2 p-3'>
|
||||
<Button className='w-full' onClick={() => {
|
||||
onClose && onClose()
|
||||
onClose?.()
|
||||
}}>
|
||||
{t('app.iconPicker.cancel')}
|
||||
</Button>
|
||||
@ -54,7 +54,7 @@ const EmojiPicker: FC<IEmojiPickerProps> = ({
|
||||
variant="primary"
|
||||
className='w-full'
|
||||
onClick={() => {
|
||||
onSelect && onSelect(selectedEmoji, selectedBackground!)
|
||||
onSelect?.(selectedEmoji, selectedBackground!)
|
||||
}}>
|
||||
{t('app.iconPicker.ok')}
|
||||
</Button>
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import {
|
||||
@ -19,11 +18,13 @@ type Shape = {
|
||||
|
||||
export const createFileStore = (
|
||||
value: FileEntity[] = [],
|
||||
onChange?: (files: FileEntity[]) => void,
|
||||
) => {
|
||||
return create<Shape>(set => ({
|
||||
files: value ? [...value] : [],
|
||||
setFiles: (files) => {
|
||||
set({ files })
|
||||
onChange?.(files)
|
||||
},
|
||||
}))
|
||||
}
|
||||
@ -54,35 +55,9 @@ export const FileContextProvider = ({
|
||||
onChange,
|
||||
}: FileProviderProps) => {
|
||||
const storeRef = useRef<FileStore | undefined>(undefined)
|
||||
const onChangeRef = useRef<FileProviderProps['onChange']>(onChange)
|
||||
const isSyncingRef = useRef(false)
|
||||
|
||||
if (!storeRef.current)
|
||||
storeRef.current = createFileStore(value)
|
||||
|
||||
// keep latest onChange
|
||||
useEffect(() => {
|
||||
onChangeRef.current = onChange
|
||||
}, [onChange])
|
||||
|
||||
// subscribe to store changes and call latest onChange
|
||||
useEffect(() => {
|
||||
const store = storeRef.current!
|
||||
const unsubscribe = store.subscribe((state: Shape) => {
|
||||
if (isSyncingRef.current) return
|
||||
onChangeRef.current?.(state.files)
|
||||
})
|
||||
return unsubscribe
|
||||
}, [])
|
||||
|
||||
// sync external value into internal store when value changes
|
||||
useEffect(() => {
|
||||
const store = storeRef.current!
|
||||
const nextFiles = value ? [...value] : []
|
||||
isSyncingRef.current = true
|
||||
store.setState({ files: nextFiles })
|
||||
isSyncingRef.current = false
|
||||
}, [value])
|
||||
storeRef.current = createFileStore(value, onChange)
|
||||
|
||||
return (
|
||||
<FileContext.Provider value={storeRef.current}>
|
||||
|
||||
@ -33,7 +33,10 @@ const SelectField = ({
|
||||
<PureSelect
|
||||
value={field.state.value}
|
||||
options={options}
|
||||
onChange={value => field.handleChange(value)}
|
||||
onChange={(value) => {
|
||||
field.handleChange(value)
|
||||
onChange?.(value)
|
||||
}}
|
||||
{...selectProps}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -62,7 +62,7 @@ const ImageList: FC<ImageListProps> = ({
|
||||
{item.progress === -1 && (
|
||||
<RefreshCcw01
|
||||
className="h-5 w-5 text-white"
|
||||
onClick={() => onReUpload && onReUpload(item._id)}
|
||||
onClick={() => onReUpload?.(item._id)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@ -122,7 +122,7 @@ const ImageList: FC<ImageListProps> = ({
|
||||
'rounded-2xl shadow-lg hover:bg-state-base-hover',
|
||||
item.progress === -1 ? 'flex' : 'hidden group-hover:flex',
|
||||
)}
|
||||
onClick={() => onRemove && onRemove(item._id)}
|
||||
onClick={() => onRemove?.(item._id)}
|
||||
>
|
||||
<RiCloseLine className="h-3 w-3 text-text-tertiary" />
|
||||
</button>
|
||||
|
||||
@ -20,7 +20,7 @@ const isBase64 = (str: string): boolean => {
|
||||
try {
|
||||
return btoa(atob(str)) === str
|
||||
}
|
||||
catch (err) {
|
||||
catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,12 +8,14 @@ import {
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import CopyIcon from '@/app/components/base/copy-icon'
|
||||
import SVGBtn from '@/app/components/base/svg'
|
||||
import Flowchart from '@/app/components/base/mermaid'
|
||||
import { Theme } from '@/types/app'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import SVGRenderer from '../svg-gallery' // Assumes svg-gallery.tsx is in /base directory
|
||||
import MarkdownMusic from '@/app/components/base/markdown-blocks/music'
|
||||
import ErrorBoundary from '@/app/components/base/markdown/error-boundary'
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
const Flowchart = dynamic(() => import('@/app/components/base/mermaid'), { ssr: false })
|
||||
|
||||
// Available language https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/AVAILABLE_LANGUAGES_HLJS.MD
|
||||
const capitalizationLanguageNameMap: Record<string, string> = {
|
||||
@ -125,7 +127,7 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any
|
||||
|
||||
// Store event handlers in useMemo to avoid recreating them
|
||||
const echartsEvents = useMemo(() => ({
|
||||
finished: (params: EChartsEventParams) => {
|
||||
finished: (_params: EChartsEventParams) => {
|
||||
// Limit finished event frequency to avoid infinite loops
|
||||
finishedEventCountRef.current++
|
||||
if (finishedEventCountRef.current > 3) {
|
||||
|
||||
@ -37,7 +37,7 @@ const removeEndThink = (children: any): any => {
|
||||
|
||||
const useThinkTimer = (children: any) => {
|
||||
const { isResponding } = useChatContext()
|
||||
const [startTime] = useState(Date.now())
|
||||
const [startTime] = useState(() => Date.now())
|
||||
const [elapsedTime, setElapsedTime] = useState(0)
|
||||
const [isComplete, setIsComplete] = useState(false)
|
||||
const timerRef = useRef<NodeJS.Timeout>()
|
||||
|
||||
@ -1,25 +1,11 @@
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import dynamic from 'next/dynamic'
|
||||
import 'katex/dist/katex.min.css'
|
||||
import RemarkMath from 'remark-math'
|
||||
import RemarkBreaks from 'remark-breaks'
|
||||
import RehypeKatex from 'rehype-katex'
|
||||
import RemarkGfm from 'remark-gfm'
|
||||
import RehypeRaw from 'rehype-raw'
|
||||
import { flow } from 'lodash-es'
|
||||
import cn from '@/utils/classnames'
|
||||
import { customUrlTransform, preprocessLaTeX, preprocessThinkTag } from './markdown-utils'
|
||||
import {
|
||||
AudioBlock,
|
||||
CodeBlock,
|
||||
Img,
|
||||
Link,
|
||||
MarkdownButton,
|
||||
MarkdownForm,
|
||||
Paragraph,
|
||||
ScriptBlock,
|
||||
ThinkBlock,
|
||||
VideoBlock,
|
||||
} from '@/app/components/base/markdown-blocks'
|
||||
import { preprocessLaTeX, preprocessThinkTag } from './markdown-utils'
|
||||
import type { ReactMarkdownWrapperProps } from './react-markdown-wrapper'
|
||||
|
||||
const ReactMarkdown = dynamic(() => import('./react-markdown-wrapper').then(mod => mod.ReactMarkdownWrapper), { ssr: false })
|
||||
|
||||
/**
|
||||
* @fileoverview Main Markdown rendering component.
|
||||
@ -31,9 +17,7 @@ import {
|
||||
export type MarkdownProps = {
|
||||
content: string
|
||||
className?: string
|
||||
customDisallowedElements?: string[]
|
||||
customComponents?: Record<string, React.ComponentType<any>>
|
||||
}
|
||||
} & Pick<ReactMarkdownWrapperProps, 'customComponents' | 'customDisallowedElements'>
|
||||
|
||||
export const Markdown = (props: MarkdownProps) => {
|
||||
const { customComponents = {} } = props
|
||||
@ -44,53 +28,7 @@ export const Markdown = (props: MarkdownProps) => {
|
||||
|
||||
return (
|
||||
<div className={cn('markdown-body', '!text-text-primary', props.className)}>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[
|
||||
RemarkGfm,
|
||||
[RemarkMath, { singleDollarTextMath: false }],
|
||||
RemarkBreaks,
|
||||
]}
|
||||
rehypePlugins={[
|
||||
RehypeKatex,
|
||||
RehypeRaw as any,
|
||||
// The Rehype plug-in is used to remove the ref attribute of an element
|
||||
() => {
|
||||
return (tree: any) => {
|
||||
const iterate = (node: any) => {
|
||||
if (node.type === 'element' && node.properties?.ref)
|
||||
delete node.properties.ref
|
||||
|
||||
if (node.type === 'element' && !/^[a-z][a-z0-9]*$/i.test(node.tagName)) {
|
||||
node.type = 'text'
|
||||
node.value = `<${node.tagName}`
|
||||
}
|
||||
|
||||
if (node.children)
|
||||
node.children.forEach(iterate)
|
||||
}
|
||||
tree.children.forEach(iterate)
|
||||
}
|
||||
},
|
||||
]}
|
||||
urlTransform={customUrlTransform}
|
||||
disallowedElements={['iframe', 'head', 'html', 'meta', 'link', 'style', 'body', ...(props.customDisallowedElements || [])]}
|
||||
components={{
|
||||
code: CodeBlock,
|
||||
img: Img,
|
||||
video: VideoBlock,
|
||||
audio: AudioBlock,
|
||||
a: Link,
|
||||
p: Paragraph,
|
||||
button: MarkdownButton,
|
||||
form: MarkdownForm,
|
||||
script: ScriptBlock as any,
|
||||
details: ThinkBlock,
|
||||
...customComponents,
|
||||
}}
|
||||
>
|
||||
{/* Markdown detect has problem. */}
|
||||
{latexContent}
|
||||
</ReactMarkdown>
|
||||
<ReactMarkdown latexContent={latexContent} customComponents={customComponents} customDisallowedElements={props.customDisallowedElements} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
82
web/app/components/base/markdown/react-markdown-wrapper.tsx
Normal file
82
web/app/components/base/markdown/react-markdown-wrapper.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import RemarkMath from 'remark-math'
|
||||
import RemarkBreaks from 'remark-breaks'
|
||||
import RehypeKatex from 'rehype-katex'
|
||||
import RemarkGfm from 'remark-gfm'
|
||||
import RehypeRaw from 'rehype-raw'
|
||||
import AudioBlock from '@/app/components/base/markdown-blocks/audio-block'
|
||||
import Img from '@/app/components/base/markdown-blocks/img'
|
||||
import Link from '@/app/components/base/markdown-blocks/link'
|
||||
import MarkdownButton from '@/app/components/base/markdown-blocks/button'
|
||||
import MarkdownForm from '@/app/components/base/markdown-blocks/form'
|
||||
import Paragraph from '@/app/components/base/markdown-blocks/paragraph'
|
||||
import ScriptBlock from '@/app/components/base/markdown-blocks/script-block'
|
||||
import ThinkBlock from '@/app/components/base/markdown-blocks/think-block'
|
||||
import VideoBlock from '@/app/components/base/markdown-blocks/video-block'
|
||||
import { customUrlTransform } from './markdown-utils'
|
||||
|
||||
import type { FC } from 'react'
|
||||
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
const CodeBlock = dynamic(() => import('@/app/components/base/markdown-blocks/code-block'), { ssr: false })
|
||||
|
||||
export type ReactMarkdownWrapperProps = {
|
||||
latexContent: any
|
||||
customDisallowedElements?: string[]
|
||||
customComponents?: Record<string, React.ComponentType<any>>
|
||||
}
|
||||
|
||||
export const ReactMarkdownWrapper: FC<ReactMarkdownWrapperProps> = (props) => {
|
||||
const { customComponents, latexContent } = props
|
||||
|
||||
return (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[
|
||||
RemarkGfm,
|
||||
[RemarkMath, { singleDollarTextMath: false }],
|
||||
RemarkBreaks,
|
||||
]}
|
||||
rehypePlugins={[
|
||||
RehypeKatex,
|
||||
RehypeRaw as any,
|
||||
// The Rehype plug-in is used to remove the ref attribute of an element
|
||||
() => {
|
||||
return (tree: any) => {
|
||||
const iterate = (node: any) => {
|
||||
if (node.type === 'element' && node.properties?.ref)
|
||||
delete node.properties.ref
|
||||
|
||||
if (node.type === 'element' && !/^[a-z][a-z0-9]*$/i.test(node.tagName)) {
|
||||
node.type = 'text'
|
||||
node.value = `<${node.tagName}`
|
||||
}
|
||||
|
||||
if (node.children)
|
||||
node.children.forEach(iterate)
|
||||
}
|
||||
tree.children.forEach(iterate)
|
||||
}
|
||||
},
|
||||
]}
|
||||
urlTransform={customUrlTransform}
|
||||
disallowedElements={['iframe', 'head', 'html', 'meta', 'link', 'style', 'body', ...(props.customDisallowedElements || [])]}
|
||||
components={{
|
||||
code: CodeBlock,
|
||||
img: Img,
|
||||
video: VideoBlock,
|
||||
audio: AudioBlock,
|
||||
a: Link,
|
||||
p: Paragraph,
|
||||
button: MarkdownButton,
|
||||
form: MarkdownForm,
|
||||
script: ScriptBlock as any,
|
||||
details: ThinkBlock,
|
||||
...customComponents,
|
||||
}}
|
||||
>
|
||||
{/* Markdown detect has problem. */}
|
||||
{latexContent}
|
||||
</ReactMarkdown>
|
||||
)
|
||||
}
|
||||
@ -60,7 +60,7 @@ export function svgToBase64(svgGraph: string): Promise<string> {
|
||||
reader.readAsDataURL(blob)
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
catch {
|
||||
return Promise.resolve('')
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,7 +93,7 @@ const NotionPageSelector = ({
|
||||
const defaultSelectedPagesId = useMemo(() => {
|
||||
return [...Array.from(pagesMapAndSelectedPagesId[1]), ...(value || [])]
|
||||
}, [pagesMapAndSelectedPagesId, value])
|
||||
const [selectedPagesId, setSelectedPagesId] = useState<Set<string>>(new Set(defaultSelectedPagesId))
|
||||
const [selectedPagesId, setSelectedPagesId] = useState<Set<string>>(() => new Set(defaultSelectedPagesId))
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedPagesId(new Set(defaultSelectedPagesId))
|
||||
|
||||
@ -10,9 +10,7 @@ const usePagination = ({
|
||||
edgePageCount,
|
||||
middlePagesSiblingCount,
|
||||
}: IPaginationProps): IUsePagination => {
|
||||
const pages = new Array(totalPages)
|
||||
.fill(0)
|
||||
.map((_, i) => i + 1)
|
||||
const pages = React.useMemo(() => Array.from({ length: totalPages }, (_, i) => i + 1), [totalPages])
|
||||
|
||||
const hasPreviousPage = currentPage > 1
|
||||
const hasNextPage = currentPage < totalPages
|
||||
|
||||
@ -57,7 +57,34 @@ const CustomizedPagination: FC<Props> = ({
|
||||
if (isNaN(Number.parseInt(value)))
|
||||
return setInputValue('')
|
||||
setInputValue(Number.parseInt(value))
|
||||
handlePaging(value)
|
||||
}
|
||||
|
||||
const handleInputConfirm = () => {
|
||||
if (inputValue !== '' && String(inputValue) !== String(current + 1)) {
|
||||
handlePaging(String(inputValue))
|
||||
return
|
||||
}
|
||||
|
||||
if (inputValue === '')
|
||||
setInputValue(current + 1)
|
||||
|
||||
setShowInput(false)
|
||||
}
|
||||
|
||||
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
handleInputConfirm()
|
||||
}
|
||||
else if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
setInputValue(current + 1)
|
||||
setShowInput(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleInputBlur = () => {
|
||||
handleInputConfirm()
|
||||
}
|
||||
|
||||
return (
|
||||
@ -105,7 +132,8 @@ const CustomizedPagination: FC<Props> = ({
|
||||
autoFocus
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
onBlur={() => setShowInput(false)}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
onBlur={handleInputBlur}
|
||||
/>
|
||||
)}
|
||||
<Pagination.NextButton
|
||||
|
||||
@ -37,13 +37,16 @@ export default function CustomPopover({
|
||||
const timeOutRef = useRef<number | null>(null)
|
||||
|
||||
const onMouseEnter = (isOpen: boolean) => {
|
||||
timeOutRef.current && window.clearTimeout(timeOutRef.current)
|
||||
!isOpen && buttonRef.current?.click()
|
||||
if (timeOutRef.current != null)
|
||||
window.clearTimeout(timeOutRef.current)
|
||||
if (!isOpen)
|
||||
buttonRef.current?.click()
|
||||
}
|
||||
|
||||
const onMouseLeave = (isOpen: boolean) => {
|
||||
timeOutRef.current = window.setTimeout(() => {
|
||||
isOpen && buttonRef.current?.click()
|
||||
if (isOpen)
|
||||
buttonRef.current?.click()
|
||||
}, timeoutDuration)
|
||||
}
|
||||
|
||||
|
||||
@ -43,7 +43,7 @@ export default function LocaleSigninSelect({
|
||||
className={'group flex w-full items-center rounded-lg px-3 py-2 text-sm text-text-secondary data-[active]:bg-state-base-hover'}
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault()
|
||||
onChange && onChange(item.value)
|
||||
onChange?.(item.value)
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
|
||||
@ -43,7 +43,7 @@ export default function Select({
|
||||
className={'group flex w-full items-center rounded-lg px-3 py-2 text-sm text-text-secondary data-[active]:bg-state-base-hover'}
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault()
|
||||
onChange && onChange(item.value)
|
||||
onChange?.(item.value)
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
|
||||
@ -21,7 +21,7 @@ const TabSlider: FC<TabSliderProps> = ({
|
||||
onChange,
|
||||
options,
|
||||
}) => {
|
||||
const [activeIndex, setActiveIndex] = useState(options.findIndex(option => option.value === value))
|
||||
const [activeIndex, setActiveIndex] = useState(() => options.findIndex(option => option.value === value))
|
||||
const [sliderStyle, setSliderStyle] = useState({})
|
||||
const { data: pluginList } = useInstalledPluginList()
|
||||
|
||||
|
||||
@ -97,10 +97,13 @@ const Panel = (props: PanelProps) => {
|
||||
const removeTagIDs = value.filter(v => !selectedTagIDs.includes(v))
|
||||
const selectedTags = tagList.filter(tag => selectedTagIDs.includes(tag.id))
|
||||
onCacheUpdate(selectedTags)
|
||||
Promise.all([
|
||||
...(addTagIDs.length ? [bind(addTagIDs)] : []),
|
||||
...[removeTagIDs.length ? removeTagIDs.map(tagID => unbind(tagID)) : []],
|
||||
]).finally(() => {
|
||||
const operations: Promise<unknown>[] = []
|
||||
if (addTagIDs.length)
|
||||
operations.push(bind(addTagIDs))
|
||||
if (removeTagIDs.length)
|
||||
operations.push(...removeTagIDs.map(tagID => unbind(tagID)))
|
||||
|
||||
Promise.all(operations).finally(() => {
|
||||
if (onChange)
|
||||
onChange()
|
||||
})
|
||||
|
||||
@ -81,7 +81,8 @@ const VoiceInput = ({
|
||||
setStartRecord(false)
|
||||
setStartConvert(true)
|
||||
recorder.current.stop()
|
||||
drawRecordId.current && cancelAnimationFrame(drawRecordId.current)
|
||||
if (drawRecordId.current)
|
||||
cancelAnimationFrame(drawRecordId.current)
|
||||
drawRecordId.current = null
|
||||
const canvas = canvasRef.current!
|
||||
const ctx = ctxRef.current!
|
||||
|
||||
@ -38,7 +38,7 @@ const CustomWebAppBrand = () => {
|
||||
isCurrentWorkspaceManager,
|
||||
} = useAppContext()
|
||||
const [fileId, setFileId] = useState('')
|
||||
const [imgKey, setImgKey] = useState(Date.now())
|
||||
const [imgKey, setImgKey] = useState(() => Date.now())
|
||||
const [uploadProgress, setUploadProgress] = useState(0)
|
||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
||||
const isSandbox = enableBilling && plan.type === Plan.sandbox
|
||||
|
||||
@ -40,7 +40,7 @@ const RetrievalMethodConfig: FC<Props> = ({
|
||||
onChange({
|
||||
...value,
|
||||
search_method: retrieveMethod,
|
||||
...(!value.reranking_model.reranking_model_name
|
||||
...((!value.reranking_model.reranking_model_name || !value.reranking_model.reranking_provider_name)
|
||||
? {
|
||||
reranking_model: {
|
||||
reranking_provider_name: isRerankDefaultModelValid ? rerankDefaultModel?.provider?.provider ?? '' : '',
|
||||
@ -57,7 +57,7 @@ const RetrievalMethodConfig: FC<Props> = ({
|
||||
onChange({
|
||||
...value,
|
||||
search_method: retrieveMethod,
|
||||
...(!value.reranking_model.reranking_model_name
|
||||
...((!value.reranking_model.reranking_model_name || !value.reranking_model.reranking_provider_name)
|
||||
? {
|
||||
reranking_model: {
|
||||
reranking_provider_name: isRerankDefaultModelValid ? rerankDefaultModel?.provider?.provider ?? '' : '',
|
||||
|
||||
@ -54,7 +54,7 @@ const RetrievalParamConfig: FC<Props> = ({
|
||||
},
|
||||
)
|
||||
|
||||
const handleDisabledSwitchClick = useCallback((enable: boolean) => {
|
||||
const handleToggleRerankEnable = useCallback((enable: boolean) => {
|
||||
if (enable && !currentModel)
|
||||
Toast.notify({ type: 'error', message: t('workflow.errorMsg.rerankModelRequired') })
|
||||
onChange({
|
||||
@ -119,7 +119,7 @@ const RetrievalParamConfig: FC<Props> = ({
|
||||
<Switch
|
||||
size='md'
|
||||
defaultValue={value.reranking_enable}
|
||||
onChange={handleDisabledSwitchClick}
|
||||
onChange={handleToggleRerankEnable}
|
||||
/>
|
||||
)}
|
||||
<div className='flex items-center'>
|
||||
|
||||
@ -34,7 +34,8 @@ const Uploader: FC<Props> = ({
|
||||
const handleDragEnter = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.target !== dragRef.current && setDragging(true)
|
||||
if (e.target !== dragRef.current)
|
||||
setDragging(true)
|
||||
}
|
||||
const handleDragOver = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
@ -43,7 +44,8 @@ const Uploader: FC<Props> = ({
|
||||
const handleDragLeave = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.target === dragRef.current && setDragging(false)
|
||||
if (e.target === dragRef.current)
|
||||
setDragging(false)
|
||||
}
|
||||
const handleDrop = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
@ -185,7 +185,8 @@ const FileUploader = ({
|
||||
const handleDragEnter = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.target !== dragRef.current && setDragging(true)
|
||||
if (e.target !== dragRef.current)
|
||||
setDragging(true)
|
||||
}
|
||||
const handleDragOver = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
@ -194,7 +195,8 @@ const FileUploader = ({
|
||||
const handleDragLeave = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.target === dragRef.current && setDragging(false)
|
||||
if (e.target === dragRef.current)
|
||||
setDragging(false)
|
||||
}
|
||||
type FileWithPath = {
|
||||
relativePath?: string
|
||||
|
||||
@ -568,9 +568,9 @@ const StepTwo = ({
|
||||
params,
|
||||
{
|
||||
onSuccess(data) {
|
||||
updateIndexingTypeCache && updateIndexingTypeCache(indexType as string)
|
||||
updateResultCache && updateResultCache(data)
|
||||
updateRetrievalMethodCache && updateRetrievalMethodCache(retrievalConfig.search_method as string)
|
||||
updateIndexingTypeCache?.(indexType as string)
|
||||
updateResultCache?.(data)
|
||||
updateRetrievalMethodCache?.(retrievalConfig.search_method as string)
|
||||
},
|
||||
},
|
||||
)
|
||||
@ -578,17 +578,18 @@ const StepTwo = ({
|
||||
else {
|
||||
await createDocumentMutation.mutateAsync(params, {
|
||||
onSuccess(data) {
|
||||
updateIndexingTypeCache && updateIndexingTypeCache(indexType as string)
|
||||
updateResultCache && updateResultCache(data)
|
||||
updateRetrievalMethodCache && updateRetrievalMethodCache(retrievalConfig.search_method as string)
|
||||
updateIndexingTypeCache?.(indexType as string)
|
||||
updateResultCache?.(data)
|
||||
updateRetrievalMethodCache?.(retrievalConfig.search_method as string)
|
||||
},
|
||||
})
|
||||
}
|
||||
if (mutateDatasetRes)
|
||||
mutateDatasetRes()
|
||||
invalidDatasetList()
|
||||
onStepChange && onStepChange(+1)
|
||||
isSetting && onSave && onSave()
|
||||
onStepChange?.(+1)
|
||||
if (isSetting)
|
||||
onSave?.()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@ -1026,7 +1027,7 @@ const StepTwo = ({
|
||||
{!isSetting
|
||||
? (
|
||||
<div className='mt-8 flex items-center py-2'>
|
||||
<Button onClick={() => onStepChange && onStepChange(-1)}>
|
||||
<Button onClick={() => onStepChange?.(-1)}>
|
||||
<RiArrowLeftLine className='mr-1 h-4 w-4' />
|
||||
{t('datasetCreation.stepTwo.previousStep')}
|
||||
</Button>
|
||||
|
||||
@ -7,7 +7,6 @@ import DocumentFileIcon from '@/app/components/datasets/common/document-file-ico
|
||||
import cn from '@/utils/classnames'
|
||||
import type { CustomFile as File, FileItem } from '@/models/datasets'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import SimplePieChart from '@/app/components/base/simple-pie-chart'
|
||||
import { upload } from '@/service/base'
|
||||
import I18n from '@/context/i18n'
|
||||
import { LanguagesSupported } from '@/i18n-config/language'
|
||||
@ -17,6 +16,9 @@ import useTheme from '@/hooks/use-theme'
|
||||
import { useFileUploadConfig } from '@/service/use-common'
|
||||
import { useDataSourceStore, useDataSourceStoreWithSelector } from '../store'
|
||||
import produce from 'immer'
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
const SimplePieChart = dynamic(() => import('@/app/components/base/simple-pie-chart'), { ssr: false })
|
||||
|
||||
const FILES_NUMBER_LIMIT = 20
|
||||
|
||||
@ -198,7 +200,8 @@ const LocalFile = ({
|
||||
const handleDragEnter = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.target !== dragRef.current && setDragging(true)
|
||||
if (e.target !== dragRef.current)
|
||||
setDragging(true)
|
||||
}
|
||||
const handleDragOver = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
@ -207,7 +210,8 @@ const LocalFile = ({
|
||||
const handleDragLeave = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.target === dragRef.current && setDragging(false)
|
||||
if (e.target === dragRef.current)
|
||||
setDragging(false)
|
||||
}
|
||||
|
||||
const handleDrop = useCallback((e: DragEvent) => {
|
||||
|
||||
@ -45,10 +45,13 @@ const CrawledResult = ({
|
||||
|
||||
const handleItemCheckChange = useCallback((item: CrawlResultItem) => {
|
||||
return (checked: boolean) => {
|
||||
if (checked)
|
||||
isMultipleChoice ? onSelectedChange([...checkedList, item]) : onSelectedChange([item])
|
||||
else
|
||||
onSelectedChange(checkedList.filter(checkedItem => checkedItem.source_url !== item.source_url))
|
||||
if (checked) {
|
||||
if (isMultipleChoice)
|
||||
onSelectedChange([...checkedList, item])
|
||||
else
|
||||
onSelectedChange([item])
|
||||
}
|
||||
else { onSelectedChange(checkedList.filter(checkedItem => checkedItem.source_url !== item.source_url)) }
|
||||
}
|
||||
}, [checkedList, onSelectedChange, isMultipleChoice])
|
||||
|
||||
|
||||
@ -326,7 +326,10 @@ const CreateFormPipeline = () => {
|
||||
}, [])
|
||||
|
||||
const handleSubmit = useCallback((data: Record<string, any>) => {
|
||||
isPreview.current ? handlePreviewChunks(data) : handleProcess(data)
|
||||
if (isPreview.current)
|
||||
handlePreviewChunks(data)
|
||||
else
|
||||
handleProcess(data)
|
||||
}, [handlePreviewChunks, handleProcess])
|
||||
|
||||
const handlePreviewFileChange = useCallback((file: DocumentItem) => {
|
||||
|
||||
@ -99,7 +99,8 @@ const CSVUploader: FC<Props> = ({
|
||||
const handleDragEnter = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.target !== dragRef.current && setDragging(true)
|
||||
if (e.target !== dragRef.current)
|
||||
setDragging(true)
|
||||
}
|
||||
const handleDragOver = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
@ -108,7 +109,8 @@ const CSVUploader: FC<Props> = ({
|
||||
const handleDragLeave = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.target === dragRef.current && setDragging(false)
|
||||
if (e.target === dragRef.current)
|
||||
setDragging(false)
|
||||
}
|
||||
const handleDrop = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
@ -284,7 +284,8 @@ const Completed: FC<ICompletedProps> = ({
|
||||
onSuccess: () => {
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
resetList()
|
||||
!segId && setSelectedSegmentIds([])
|
||||
if (!segId)
|
||||
setSelectedSegmentIds([])
|
||||
},
|
||||
onError: () => {
|
||||
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
|
||||
@ -438,7 +439,8 @@ const Completed: FC<ICompletedProps> = ({
|
||||
}
|
||||
else {
|
||||
resetList()
|
||||
currentPage !== totalPages && setCurrentPage(totalPages)
|
||||
if (currentPage !== totalPages)
|
||||
setCurrentPage(totalPages)
|
||||
}
|
||||
}, [segmentListData, limit, currentPage, resetList])
|
||||
|
||||
@ -491,7 +493,8 @@ const Completed: FC<ICompletedProps> = ({
|
||||
}
|
||||
else {
|
||||
resetChildList()
|
||||
currentPage !== totalPages && setCurrentPage(totalPages)
|
||||
if (currentPage !== totalPages)
|
||||
setCurrentPage(totalPages)
|
||||
}
|
||||
}, [childChunkListData, limit, currentPage, resetChildList])
|
||||
|
||||
|
||||
@ -66,7 +66,7 @@ export const FieldInfo: FC<IFieldInfoProps> = ({
|
||||
? displayedValue
|
||||
: inputType === 'select'
|
||||
? <SimpleSelect
|
||||
onSelect={({ value }) => onUpdate && onUpdate(value as string)}
|
||||
onSelect={({ value }) => onUpdate?.(value as string)}
|
||||
items={selectOptions}
|
||||
defaultValue={value}
|
||||
className={s.select}
|
||||
@ -75,7 +75,7 @@ export const FieldInfo: FC<IFieldInfoProps> = ({
|
||||
/>
|
||||
: inputType === 'textarea'
|
||||
? <AutoHeightTextarea
|
||||
onChange={e => onUpdate && onUpdate(e.target.value)}
|
||||
onChange={e => onUpdate?.(e.target.value)}
|
||||
value={value}
|
||||
className={s.textArea}
|
||||
placeholder={`${t('datasetDocuments.metadata.placeholder.add')}${label}`}
|
||||
|
||||
@ -148,7 +148,10 @@ const PipelineSettings = ({
|
||||
}, [])
|
||||
|
||||
const handleSubmit = useCallback((data: Record<string, any>) => {
|
||||
isPreview.current ? handlePreviewChunks(data) : handleProcess(data)
|
||||
if (isPreview.current)
|
||||
handlePreviewChunks(data)
|
||||
else
|
||||
handleProcess(data)
|
||||
}, [handlePreviewChunks, handleProcess])
|
||||
|
||||
if (isFetchingLastRunData) {
|
||||
|
||||
@ -418,6 +418,8 @@ const DocumentList: FC<IDocumentListProps> = ({
|
||||
</td>
|
||||
<td>
|
||||
<Operations
|
||||
selectedIds={selectedIds}
|
||||
onSelectedIdChange={onSelectedIdChange}
|
||||
embeddingAvailable={embeddingAvailable}
|
||||
datasetId={datasetId}
|
||||
detail={pick(doc, ['name', 'enabled', 'archived', 'id', 'data_source_type', 'doc_form', 'display_status'])}
|
||||
|
||||
@ -35,7 +35,7 @@ import {
|
||||
} from '@remixicon/react'
|
||||
import CustomPopover from '../../base/popover'
|
||||
import s from './style.module.css'
|
||||
import { DataSourceType } from '@/models/datasets'
|
||||
import { DataSourceType, DocumentActionType } from '@/models/datasets'
|
||||
import Confirm from '../../base/confirm'
|
||||
import RenameModal from './rename-modal'
|
||||
|
||||
@ -50,6 +50,8 @@ type OperationsProps = {
|
||||
doc_form: string
|
||||
display_status?: string
|
||||
}
|
||||
selectedIds?: string[]
|
||||
onSelectedIdChange?: (ids: string[]) => void
|
||||
datasetId: string
|
||||
onUpdate: (operationName?: string) => void
|
||||
scene?: 'list' | 'detail'
|
||||
@ -60,6 +62,8 @@ const Operations = ({
|
||||
embeddingAvailable,
|
||||
datasetId,
|
||||
detail,
|
||||
selectedIds,
|
||||
onSelectedIdChange,
|
||||
onUpdate,
|
||||
scene = 'list',
|
||||
className = '',
|
||||
@ -116,17 +120,20 @@ const Operations = ({
|
||||
const [e] = await asyncRunSafe<CommonResponse>(opApi({ datasetId, documentId: id }) as Promise<CommonResponse>)
|
||||
if (!e) {
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
// If it is a delete operation, need to update the selectedIds state
|
||||
if (selectedIds && onSelectedIdChange && operationName === DocumentActionType.delete)
|
||||
onSelectedIdChange(selectedIds.filter(selectedId => selectedId !== id))
|
||||
onUpdate(operationName)
|
||||
}
|
||||
else { notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) }
|
||||
if (operationName === 'delete')
|
||||
if (operationName === DocumentActionType.delete)
|
||||
setDeleting(false)
|
||||
}
|
||||
|
||||
const { run: handleSwitch } = useDebounceFn((operationName: OperationName) => {
|
||||
if (operationName === 'enable' && enabled)
|
||||
if (operationName === DocumentActionType.enable && enabled)
|
||||
return
|
||||
if (operationName === 'disable' && !enabled)
|
||||
if (operationName === DocumentActionType.disable && !enabled)
|
||||
return
|
||||
onOperate(operationName)
|
||||
}, { wait: 500 })
|
||||
|
||||
@ -80,7 +80,8 @@ const TextAreaWithButton = ({
|
||||
onUpdateList?.()
|
||||
}
|
||||
setLoading(false)
|
||||
_onSubmit && _onSubmit()
|
||||
if (_onSubmit)
|
||||
_onSubmit()
|
||||
}
|
||||
|
||||
const externalRetrievalTestingOnSubmit = async () => {
|
||||
|
||||
@ -157,12 +157,12 @@ const DatasetCard = ({
|
||||
data-disable-nprogress={true}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
isExternalProvider
|
||||
? push(`/datasets/${dataset.id}/hitTesting`)
|
||||
// eslint-disable-next-line sonarjs/no-nested-conditional
|
||||
: isPipelineUnpublished
|
||||
? push(`/datasets/${dataset.id}/pipeline`)
|
||||
: push(`/datasets/${dataset.id}/documents`)
|
||||
if (isExternalProvider)
|
||||
push(`/datasets/${dataset.id}/hitTesting`)
|
||||
else if (isPipelineUnpublished)
|
||||
push(`/datasets/${dataset.id}/pipeline`)
|
||||
else
|
||||
push(`/datasets/${dataset.id}/documents`)
|
||||
}}
|
||||
>
|
||||
{!dataset.embedding_available && (
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
const DatasetsLoading = () => null
|
||||
|
||||
export default DatasetsLoading
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
const DatasetPreview = () => null
|
||||
|
||||
export default DatasetPreview
|
||||
|
||||
@ -193,8 +193,8 @@ function CodeGroupPanels({ children, targetCode, ...props }: ICodeGroupPanelsPro
|
||||
if ((targetCode?.length ?? 0) > 1) {
|
||||
return (
|
||||
<TabPanels>
|
||||
{targetCode!.map(code => (
|
||||
<TabPanel>
|
||||
{targetCode!.map((code, index) => (
|
||||
<TabPanel key={code.title || code.tag || index}>
|
||||
<CodePanel {...props} targetCode={code} />
|
||||
</TabPanel>
|
||||
))}
|
||||
@ -206,8 +206,8 @@ function CodeGroupPanels({ children, targetCode, ...props }: ICodeGroupPanelsPro
|
||||
}
|
||||
|
||||
function usePreventLayoutShift() {
|
||||
const positionRef = useRef<any>()
|
||||
const rafRef = useRef<any>()
|
||||
const positionRef = useRef<any>(null)
|
||||
const rafRef = useRef<any>(null)
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
||||
@ -152,23 +152,20 @@ const Apps = ({
|
||||
<div className={cn(
|
||||
'mt-6 flex items-center justify-between px-12',
|
||||
)}>
|
||||
<>
|
||||
<Category
|
||||
list={categories}
|
||||
value={currCategory}
|
||||
onChange={setCurrCategory}
|
||||
allCategoriesEn={allCategoriesEn}
|
||||
/>
|
||||
</>
|
||||
<Category
|
||||
list={categories}
|
||||
value={currCategory}
|
||||
onChange={setCurrCategory}
|
||||
allCategoriesEn={allCategoriesEn}
|
||||
/>
|
||||
<Input
|
||||
showLeftIcon
|
||||
showClearIcon
|
||||
wrapperClassName='w-[200px]'
|
||||
wrapperClassName='w-[200px] self-start'
|
||||
value={keywords}
|
||||
onChange={e => handleKeywordsChange(e.target.value)}
|
||||
onClear={() => handleKeywordsChange('')}
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
<div className={cn(
|
||||
|
||||
@ -39,7 +39,7 @@ const Collapse = ({
|
||||
<div className='mx-1 mb-1 rounded-lg border-t border-divider-subtle bg-components-panel-on-panel-item-bg py-1'>
|
||||
{
|
||||
items.map(item => (
|
||||
<div key={item.key} onClick={() => onSelect && onSelect(item)}>
|
||||
<div key={item.key} onClick={() => onSelect?.(item)}>
|
||||
{renderItem(item)}
|
||||
</div>
|
||||
))
|
||||
|
||||
@ -52,7 +52,7 @@ const InstallFromMarketplace = ({
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='system-md-semibold flex cursor-pointer items-center gap-1 text-text-primary' onClick={() => setCollapse(!collapse)}>
|
||||
<RiArrowDownSLine className={cn('h-4 w-4', collapse && '-rotate-90')} />
|
||||
{t('common.modelProvider.installProvider')}
|
||||
{t('common.modelProvider.installDataSourceProvider')}
|
||||
</div>
|
||||
<div className='mb-2 flex items-center pt-2'>
|
||||
<span className='system-sm-regular pr-1 text-text-tertiary'>{t('common.modelProvider.discoverMore')}</span>
|
||||
|
||||
@ -323,15 +323,18 @@ export const useRefreshModel = () => {
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const updateModelProviders = useUpdateModelProviders()
|
||||
const updateModelList = useUpdateModelList()
|
||||
const handleRefreshModel = useCallback((provider: ModelProvider, configurationMethod: ConfigurationMethodEnum, CustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields) => {
|
||||
const handleRefreshModel = useCallback((
|
||||
provider: ModelProvider,
|
||||
CustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
|
||||
refreshModelList?: boolean,
|
||||
) => {
|
||||
updateModelProviders()
|
||||
|
||||
provider.supported_model_types.forEach((type) => {
|
||||
updateModelList(type)
|
||||
})
|
||||
|
||||
if (configurationMethod === ConfigurationMethodEnum.customizableModel
|
||||
&& provider.custom_configuration.status === CustomConfigurationStatusEnum.active) {
|
||||
if (refreshModelList && provider.custom_configuration.status === CustomConfigurationStatusEnum.active) {
|
||||
eventEmitter?.emit({
|
||||
type: UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST,
|
||||
payload: provider.provider,
|
||||
|
||||
@ -90,7 +90,7 @@ export const useAuth = (
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
})
|
||||
handleRefreshModel(provider, configurationMethod, undefined)
|
||||
handleRefreshModel(provider, undefined, true)
|
||||
}
|
||||
finally {
|
||||
handleSetDoingAction(false)
|
||||
@ -125,7 +125,7 @@ export const useAuth = (
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
})
|
||||
handleRefreshModel(provider, configurationMethod, undefined)
|
||||
handleRefreshModel(provider, undefined, true)
|
||||
onRemove?.(pendingOperationCredentialId.current ?? '')
|
||||
closeConfirmDelete()
|
||||
}
|
||||
@ -147,7 +147,7 @@ export const useAuth = (
|
||||
|
||||
if (res.result === 'success') {
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
handleRefreshModel(provider, configurationMethod, undefined)
|
||||
handleRefreshModel(provider, undefined, !payload.credential_id)
|
||||
}
|
||||
}
|
||||
finally {
|
||||
|
||||
@ -49,7 +49,7 @@ const ModelLoadBalancingConfigs = ({
|
||||
provider,
|
||||
model,
|
||||
configurationMethod,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
currentCustomConfigurationModelFixedFields: _currentCustomConfigurationModelFixedFields,
|
||||
withSwitch = false,
|
||||
className,
|
||||
modelCredential,
|
||||
|
||||
@ -159,7 +159,7 @@ const ModelLoadBalancingModal = ({
|
||||
)
|
||||
if (res.result === 'success') {
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
handleRefreshModel(provider, configurateMethod, currentCustomConfigurationModelFixedFields)
|
||||
handleRefreshModel(provider, currentCustomConfigurationModelFixedFields, false)
|
||||
onSave?.(provider.provider)
|
||||
onClose?.()
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ import { useLanguage } from '@/app/components/header/account-setting/model-provi
|
||||
const MaintenanceNotice = () => {
|
||||
const locale = useLanguage()
|
||||
|
||||
const [showNotice, setShowNotice] = useState(localStorage.getItem('hide-maintenance-notice') !== '1')
|
||||
const [showNotice, setShowNotice] = useState(() => localStorage.getItem('hide-maintenance-notice') !== '1')
|
||||
const handleJumpNotice = () => {
|
||||
window.open(NOTICE_I18N.href, '_blank')
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@ type Props = {
|
||||
}
|
||||
|
||||
const AppPicker: FC<Props> = ({
|
||||
scope,
|
||||
scope: _scope,
|
||||
disabled,
|
||||
trigger,
|
||||
placement = 'right-start',
|
||||
@ -90,7 +90,7 @@ const AppPicker: FC<Props> = ({
|
||||
}
|
||||
|
||||
// Set up MutationObserver to watch DOM changes
|
||||
mutationObserver = new MutationObserver((mutations) => {
|
||||
mutationObserver = new MutationObserver((_mutations) => {
|
||||
if (observerTarget.current) {
|
||||
setupIntersectionObserver()
|
||||
mutationObserver?.disconnect()
|
||||
|
||||
@ -148,7 +148,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
||||
})
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
Toast.notify({ type: 'error', message: t('common.error') })
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,7 +51,7 @@ export const useFieldList = ({
|
||||
|
||||
const handleListSortChange = useCallback((list: SortableItem[]) => {
|
||||
const newInputFields = list.map((item) => {
|
||||
const { id, chosen, selected, ...filed } = item
|
||||
const { id: _id, chosen: _chosen, selected: _selected, ...filed } = item
|
||||
return filed
|
||||
})
|
||||
handleInputFieldsChange(newInputFields)
|
||||
|
||||
@ -15,7 +15,8 @@ const Header = () => {
|
||||
isPreparingDataSource,
|
||||
setIsPreparingDataSource,
|
||||
} = workflowStore.getState()
|
||||
isPreparingDataSource && setIsPreparingDataSource?.(false)
|
||||
if (isPreparingDataSource)
|
||||
setIsPreparingDataSource?.(false)
|
||||
handleCancelDebugAndPreviewPanel()
|
||||
}, [workflowStore])
|
||||
|
||||
|
||||
@ -104,7 +104,7 @@ export const useNodesSyncDraft = () => {
|
||||
const res = await syncWorkflowDraft(postParams)
|
||||
setSyncWorkflowDraftHash(res.hash)
|
||||
setDraftUpdatedAt(res.updated_at)
|
||||
callback?.onSuccess && callback.onSuccess()
|
||||
callback?.onSuccess?.()
|
||||
}
|
||||
catch (error: any) {
|
||||
if (error && error.json && !error.bodyUsed) {
|
||||
@ -113,10 +113,10 @@ export const useNodesSyncDraft = () => {
|
||||
handleRefreshWorkflowDraft()
|
||||
})
|
||||
}
|
||||
callback?.onError && callback.onError()
|
||||
callback?.onError?.()
|
||||
}
|
||||
finally {
|
||||
callback?.onSettled && callback.onSettled()
|
||||
callback?.onSettled?.()
|
||||
}
|
||||
}
|
||||
}, [getPostParams, getNodesReadOnly, workflowStore, handleRefreshWorkflowDraft])
|
||||
|
||||
@ -363,7 +363,8 @@ const TextGeneration: FC<IMainProps> = ({
|
||||
(async () => {
|
||||
if (!appData || !appParams)
|
||||
return
|
||||
!isWorkflow && fetchSavedMessage()
|
||||
if (!isWorkflow)
|
||||
fetchSavedMessage()
|
||||
const { app_id: appId, site: siteInfo, custom_config } = appData
|
||||
setAppId(appId)
|
||||
setSiteInfo(siteInfo as SiteInfo)
|
||||
|
||||
@ -126,8 +126,8 @@ const Result: FC<IResultProps> = ({
|
||||
|
||||
let hasEmptyInput = ''
|
||||
const requiredVars = prompt_variables?.filter(({ key, name, required, type }) => {
|
||||
if(type === 'boolean')
|
||||
return false // boolean input is not required
|
||||
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
|
||||
}) || [] // compatible with old version
|
||||
|
||||
@ -51,6 +51,8 @@ const RunOnce: FC<IRunOnceProps> = ({
|
||||
promptConfig.prompt_variables.forEach((item) => {
|
||||
if (item.type === 'string' || item.type === 'paragraph')
|
||||
newInputs[item.key] = ''
|
||||
else if (item.type === 'checkbox')
|
||||
newInputs[item.key] = false
|
||||
else
|
||||
newInputs[item.key] = undefined
|
||||
})
|
||||
@ -77,6 +79,8 @@ const RunOnce: FC<IRunOnceProps> = ({
|
||||
newInputs[item.key] = item.default || ''
|
||||
else if (item.type === 'number')
|
||||
newInputs[item.key] = item.default
|
||||
else if (item.type === 'checkbox')
|
||||
newInputs[item.key] = item.default || false
|
||||
else if (item.type === 'file')
|
||||
newInputs[item.key] = item.default
|
||||
else if (item.type === 'file-list')
|
||||
@ -96,7 +100,7 @@ const RunOnce: FC<IRunOnceProps> = ({
|
||||
{(inputs === null || inputs === undefined || Object.keys(inputs).length === 0) || !isInitialized ? null
|
||||
: promptConfig.prompt_variables.map(item => (
|
||||
<div className='mt-4 w-full' key={item.key}>
|
||||
{item.type !== 'boolean' && (
|
||||
{item.type !== 'checkbox' && (
|
||||
<label className='system-md-semibold flex h-6 items-center text-text-secondary'>{item.name}</label>
|
||||
)}
|
||||
<div className='mt-1'>
|
||||
@ -134,7 +138,7 @@ const RunOnce: FC<IRunOnceProps> = ({
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => { handleInputsChange({ ...inputsRef.current, [item.key]: e.target.value }) }}
|
||||
/>
|
||||
)}
|
||||
{item.type === 'boolean' && (
|
||||
{item.type === 'checkbox' && (
|
||||
<BoolInput
|
||||
name={item.name || item.key}
|
||||
value={!!inputs[item.key]}
|
||||
|
||||
@ -12,7 +12,7 @@ type CountdownProps = {
|
||||
|
||||
export default function Countdown({ onResend }: CountdownProps) {
|
||||
const { t } = useTranslation()
|
||||
const [leftTime, setLeftTime] = useState(Number(localStorage.getItem(COUNT_DOWN_KEY) || COUNT_DOWN_TIME_MS))
|
||||
const [leftTime, setLeftTime] = useState(() => Number(localStorage.getItem(COUNT_DOWN_KEY) || COUNT_DOWN_TIME_MS))
|
||||
const [time] = useCountDown({
|
||||
leftTime,
|
||||
onEnd: () => {
|
||||
|
||||
@ -62,8 +62,10 @@ const SwrInitializer = ({
|
||||
return
|
||||
}
|
||||
if (searchParams.has('access_token') || searchParams.has('refresh_token')) {
|
||||
consoleToken && localStorage.setItem('console_token', consoleToken)
|
||||
refreshToken && localStorage.setItem('refresh_token', refreshToken)
|
||||
if (consoleToken)
|
||||
localStorage.setItem('console_token', consoleToken)
|
||||
if (refreshToken)
|
||||
localStorage.setItem('refresh_token', refreshToken)
|
||||
const redirectUrl = resolvePostLoginRedirect(searchParams)
|
||||
if (redirectUrl)
|
||||
location.replace(redirectUrl)
|
||||
|
||||
@ -65,7 +65,7 @@ const MCPModal = ({
|
||||
const originalServerID = data?.server_identifier
|
||||
const [url, setUrl] = React.useState(data?.server_url || '')
|
||||
const [name, setName] = React.useState(data?.name || '')
|
||||
const [appIcon, setAppIcon] = useState<AppIconSelection>(getIcon(data))
|
||||
const [appIcon, setAppIcon] = useState<AppIconSelection>(() => getIcon(data))
|
||||
const [showAppIconPicker, setShowAppIconPicker] = useState(false)
|
||||
const [serverIdentifier, setServerIdentifier] = React.useState(data?.server_identifier || '')
|
||||
const [timeout, setMcpTimeout] = React.useState(data?.timeout || 30)
|
||||
|
||||
@ -17,7 +17,7 @@ import CardMoreInfo from '@/app/components/plugins/card/card-more-info'
|
||||
import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel'
|
||||
import MCPList from './mcp'
|
||||
import { useAllToolProviders } from '@/service/use-tools'
|
||||
import { useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
||||
import { useCheckInstalled, useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { ToolTypeEnum } from '../workflow/block-selector/types'
|
||||
import { useMarketplace } from './marketplace/hooks'
|
||||
@ -77,12 +77,14 @@ const ProviderList = () => {
|
||||
const currentProvider = useMemo<Collection | undefined>(() => {
|
||||
return filteredCollectionList.find(collection => collection.id === currentProviderId)
|
||||
}, [currentProviderId, filteredCollectionList])
|
||||
const { data: pluginList } = useInstalledPluginList()
|
||||
const { data: checkedInstalledData } = useCheckInstalled({
|
||||
pluginIds: currentProvider?.plugin_id ? [currentProvider.plugin_id] : [],
|
||||
enabled: !!currentProvider?.plugin_id,
|
||||
})
|
||||
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
|
||||
const currentPluginDetail = useMemo(() => {
|
||||
const detail = pluginList?.plugins.find(plugin => plugin.plugin_id === currentProvider?.plugin_id)
|
||||
return detail
|
||||
}, [currentProvider?.plugin_id, pluginList?.plugins])
|
||||
return checkedInstalledData?.plugins?.[0]
|
||||
}, [checkedInstalledData])
|
||||
|
||||
const toolListTailRef = useRef<HTMLDivElement>(null)
|
||||
const showMarketplacePanel = useCallback(() => {
|
||||
|
||||
@ -124,7 +124,7 @@ export const useNodesSyncDraft = () => {
|
||||
const res = await syncWorkflowDraft(postParams)
|
||||
setSyncWorkflowDraftHash(res.hash)
|
||||
setDraftUpdatedAt(res.updated_at)
|
||||
callback?.onSuccess && callback.onSuccess()
|
||||
callback?.onSuccess?.()
|
||||
}
|
||||
catch (error: any) {
|
||||
if (error && error.json && !error.bodyUsed) {
|
||||
@ -133,10 +133,10 @@ export const useNodesSyncDraft = () => {
|
||||
handleRefreshWorkflowDraft()
|
||||
})
|
||||
}
|
||||
callback?.onError && callback.onError()
|
||||
callback?.onError?.()
|
||||
}
|
||||
finally {
|
||||
callback?.onSettled && callback.onSettled()
|
||||
callback?.onSettled?.()
|
||||
}
|
||||
}
|
||||
}, [workflowStore, getPostParams, getNodesReadOnly, handleRefreshWorkflowDraft])
|
||||
|
||||
@ -8,7 +8,7 @@ import {
|
||||
} from '@/app/components/workflow/types'
|
||||
import {
|
||||
useWorkflowInit,
|
||||
} from './hooks'
|
||||
} from './hooks/use-workflow-init'
|
||||
import {
|
||||
initialEdges,
|
||||
initialNodes,
|
||||
|
||||
@ -73,7 +73,7 @@ const Tool: FC<Props> = ({
|
||||
if (isHovering && !isAllSelected) {
|
||||
return (
|
||||
<span className='system-xs-regular text-components-button-secondary-accent-text'
|
||||
onClick={(e) => {
|
||||
onClick={() => {
|
||||
onSelectMultiple?.(BlockEnum.Tool, actions.filter(action => !getIsDisabled(action)).map((tool) => {
|
||||
const params: Record<string, string> = {}
|
||||
if (tool.parameters) {
|
||||
|
||||
@ -107,7 +107,8 @@ export const useShortcuts = (): void => {
|
||||
const { showDebugAndPreviewPanel } = workflowStore.getState()
|
||||
if (shouldHandleShortcut(e) && !showDebugAndPreviewPanel) {
|
||||
e.preventDefault()
|
||||
workflowHistoryShortcutsEnabled && handleHistoryBack()
|
||||
if (workflowHistoryShortcutsEnabled)
|
||||
handleHistoryBack()
|
||||
}
|
||||
}, { exactMatch: true, useCapture: true })
|
||||
|
||||
@ -116,7 +117,8 @@ export const useShortcuts = (): void => {
|
||||
(e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
workflowHistoryShortcutsEnabled && handleHistoryForward()
|
||||
if (workflowHistoryShortcutsEnabled)
|
||||
handleHistoryForward()
|
||||
}
|
||||
},
|
||||
{ exactMatch: true, useCapture: true },
|
||||
|
||||
@ -16,23 +16,25 @@ import type { WorkflowHistoryEventMeta } from '../workflow-history-store'
|
||||
* - InputChange events in Node Panels do not trigger state changes.
|
||||
* - Resizing UI elements does not trigger state changes.
|
||||
*/
|
||||
export enum WorkflowHistoryEvent {
|
||||
NodeTitleChange = 'NodeTitleChange',
|
||||
NodeDescriptionChange = 'NodeDescriptionChange',
|
||||
NodeDragStop = 'NodeDragStop',
|
||||
NodeChange = 'NodeChange',
|
||||
NodeConnect = 'NodeConnect',
|
||||
NodePaste = 'NodePaste',
|
||||
NodeDelete = 'NodeDelete',
|
||||
EdgeDelete = 'EdgeDelete',
|
||||
EdgeDeleteByDeleteBranch = 'EdgeDeleteByDeleteBranch',
|
||||
NodeAdd = 'NodeAdd',
|
||||
NodeResize = 'NodeResize',
|
||||
NoteAdd = 'NoteAdd',
|
||||
NoteChange = 'NoteChange',
|
||||
NoteDelete = 'NoteDelete',
|
||||
LayoutOrganize = 'LayoutOrganize',
|
||||
}
|
||||
export const WorkflowHistoryEvent = {
|
||||
NodeTitleChange: 'NodeTitleChange',
|
||||
NodeDescriptionChange: 'NodeDescriptionChange',
|
||||
NodeDragStop: 'NodeDragStop',
|
||||
NodeChange: 'NodeChange',
|
||||
NodeConnect: 'NodeConnect',
|
||||
NodePaste: 'NodePaste',
|
||||
NodeDelete: 'NodeDelete',
|
||||
EdgeDelete: 'EdgeDelete',
|
||||
EdgeDeleteByDeleteBranch: 'EdgeDeleteByDeleteBranch',
|
||||
NodeAdd: 'NodeAdd',
|
||||
NodeResize: 'NodeResize',
|
||||
NoteAdd: 'NoteAdd',
|
||||
NoteChange: 'NoteChange',
|
||||
NoteDelete: 'NoteDelete',
|
||||
LayoutOrganize: 'LayoutOrganize',
|
||||
} as const
|
||||
|
||||
export type WorkflowHistoryEventT = keyof typeof WorkflowHistoryEvent
|
||||
|
||||
export const useWorkflowHistory = () => {
|
||||
const store = useStoreApi()
|
||||
@ -65,7 +67,7 @@ export const useWorkflowHistory = () => {
|
||||
// Some events may be triggered multiple times in a short period of time.
|
||||
// We debounce the history state update to avoid creating multiple history states
|
||||
// with minimal changes.
|
||||
const saveStateToHistoryRef = useRef(debounce((event: WorkflowHistoryEvent, meta?: WorkflowHistoryEventMeta) => {
|
||||
const saveStateToHistoryRef = useRef(debounce((event: WorkflowHistoryEventT, meta?: WorkflowHistoryEventMeta) => {
|
||||
workflowHistoryStore.setState({
|
||||
workflowHistoryEvent: event,
|
||||
workflowHistoryEventMeta: meta,
|
||||
@ -74,7 +76,7 @@ export const useWorkflowHistory = () => {
|
||||
})
|
||||
}, 500))
|
||||
|
||||
const saveStateToHistory = useCallback((event: WorkflowHistoryEvent, meta?: WorkflowHistoryEventMeta) => {
|
||||
const saveStateToHistory = useCallback((event: WorkflowHistoryEventT, meta?: WorkflowHistoryEventMeta) => {
|
||||
switch (event) {
|
||||
case WorkflowHistoryEvent.NoteChange:
|
||||
// Hint: Note change does not trigger when note text changes,
|
||||
@ -105,7 +107,7 @@ export const useWorkflowHistory = () => {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const getHistoryLabel = useCallback((event: WorkflowHistoryEvent) => {
|
||||
const getHistoryLabel = useCallback((event: WorkflowHistoryEventT) => {
|
||||
switch (event) {
|
||||
case WorkflowHistoryEvent.NodeTitleChange:
|
||||
return t('workflow.changeHistory.nodeTitleChange')
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
NODE_LAYOUT_VERTICAL_PADDING,
|
||||
WORKFLOW_DATA_UPDATE,
|
||||
} from '../constants'
|
||||
import type { Node, WorkflowDataUpdater } from '../types'
|
||||
import type { WorkflowDataUpdater } from '../types'
|
||||
import { BlockEnum, ControlMode } from '../types'
|
||||
import {
|
||||
getLayoutByDagre,
|
||||
@ -18,6 +18,7 @@ import {
|
||||
initialEdges,
|
||||
initialNodes,
|
||||
} from '../utils'
|
||||
import type { LayoutResult } from '../utils'
|
||||
import {
|
||||
useNodesReadOnly,
|
||||
useSelectionInteractions,
|
||||
@ -102,10 +103,17 @@ export const useWorkflowOrganize = () => {
|
||||
&& node.type === CUSTOM_NODE,
|
||||
)
|
||||
|
||||
const childLayoutsMap: Record<string, any> = {}
|
||||
loopAndIterationNodes.forEach((node) => {
|
||||
childLayoutsMap[node.id] = getLayoutForChildNodes(node.id, nodes, edges)
|
||||
})
|
||||
const childLayoutEntries = await Promise.all(
|
||||
loopAndIterationNodes.map(async node => [
|
||||
node.id,
|
||||
await getLayoutForChildNodes(node.id, nodes, edges),
|
||||
] as const),
|
||||
)
|
||||
const childLayoutsMap = childLayoutEntries.reduce((acc, [nodeId, layout]) => {
|
||||
if (layout)
|
||||
acc[nodeId] = layout
|
||||
return acc
|
||||
}, {} as Record<string, LayoutResult>)
|
||||
|
||||
const containerSizeChanges: Record<string, { width: number, height: number }> = {}
|
||||
|
||||
@ -113,37 +121,20 @@ export const useWorkflowOrganize = () => {
|
||||
const childLayout = childLayoutsMap[parentNode.id]
|
||||
if (!childLayout) return
|
||||
|
||||
let minX = Infinity
|
||||
let minY = Infinity
|
||||
let maxX = -Infinity
|
||||
let maxY = -Infinity
|
||||
let hasChildren = false
|
||||
const {
|
||||
bounds,
|
||||
nodes: layoutNodes,
|
||||
} = childLayout
|
||||
|
||||
const childNodes = nodes.filter(node => node.parentId === parentNode.id)
|
||||
if (!layoutNodes.size)
|
||||
return
|
||||
|
||||
childNodes.forEach((node) => {
|
||||
if (childLayout.node(node.id)) {
|
||||
hasChildren = true
|
||||
const childNodeWithPosition = childLayout.node(node.id)
|
||||
const requiredWidth = (bounds.maxX - bounds.minX) + NODE_LAYOUT_HORIZONTAL_PADDING * 2
|
||||
const requiredHeight = (bounds.maxY - bounds.minY) + NODE_LAYOUT_VERTICAL_PADDING * 2
|
||||
|
||||
const nodeX = childNodeWithPosition.x - node.width! / 2
|
||||
const nodeY = childNodeWithPosition.y - node.height! / 2
|
||||
|
||||
minX = Math.min(minX, nodeX)
|
||||
minY = Math.min(minY, nodeY)
|
||||
maxX = Math.max(maxX, nodeX + node.width!)
|
||||
maxY = Math.max(maxY, nodeY + node.height!)
|
||||
}
|
||||
})
|
||||
|
||||
if (hasChildren) {
|
||||
const requiredWidth = maxX - minX + NODE_LAYOUT_HORIZONTAL_PADDING * 2
|
||||
const requiredHeight = maxY - minY + NODE_LAYOUT_VERTICAL_PADDING * 2
|
||||
|
||||
containerSizeChanges[parentNode.id] = {
|
||||
width: Math.max(parentNode.width || 0, requiredWidth),
|
||||
height: Math.max(parentNode.height || 0, requiredHeight),
|
||||
}
|
||||
containerSizeChanges[parentNode.id] = {
|
||||
width: Math.max(parentNode.width || 0, requiredWidth),
|
||||
height: Math.max(parentNode.height || 0, requiredHeight),
|
||||
}
|
||||
})
|
||||
|
||||
@ -166,63 +157,65 @@ export const useWorkflowOrganize = () => {
|
||||
})
|
||||
})
|
||||
|
||||
const layout = getLayoutByDagre(nodesWithUpdatedSizes, edges)
|
||||
const layout = await getLayoutByDagre(nodesWithUpdatedSizes, edges)
|
||||
|
||||
const rankMap = {} as Record<string, Node>
|
||||
nodesWithUpdatedSizes.forEach((node) => {
|
||||
if (!node.parentId && node.type === CUSTOM_NODE) {
|
||||
const rank = layout.node(node.id).rank!
|
||||
|
||||
if (!rankMap[rank]) {
|
||||
rankMap[rank] = node
|
||||
}
|
||||
else {
|
||||
if (rankMap[rank].position.y > node.position.y)
|
||||
rankMap[rank] = node
|
||||
// Build layer map for vertical alignment - nodes in the same layer should align
|
||||
const layerMap = new Map<number, { minY: number; maxHeight: number }>()
|
||||
layout.nodes.forEach((layoutInfo) => {
|
||||
if (layoutInfo.layer !== undefined) {
|
||||
const existing = layerMap.get(layoutInfo.layer)
|
||||
const newLayerInfo = {
|
||||
minY: existing ? Math.min(existing.minY, layoutInfo.y) : layoutInfo.y,
|
||||
maxHeight: existing ? Math.max(existing.maxHeight, layoutInfo.height) : layoutInfo.height,
|
||||
}
|
||||
layerMap.set(layoutInfo.layer, newLayerInfo)
|
||||
}
|
||||
})
|
||||
|
||||
const newNodes = produce(nodesWithUpdatedSizes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
if (!node.parentId && node.type === CUSTOM_NODE) {
|
||||
const nodeWithPosition = layout.node(node.id)
|
||||
const layoutInfo = layout.nodes.get(node.id)
|
||||
if (!layoutInfo)
|
||||
return
|
||||
|
||||
// Calculate vertical position with layer alignment
|
||||
let yPosition = layoutInfo.y
|
||||
if (layoutInfo.layer !== undefined) {
|
||||
const layerInfo = layerMap.get(layoutInfo.layer)
|
||||
if (layerInfo) {
|
||||
// Align to the center of the tallest node in this layer
|
||||
const layerCenterY = layerInfo.minY + layerInfo.maxHeight / 2
|
||||
yPosition = layerCenterY - layoutInfo.height / 2
|
||||
}
|
||||
}
|
||||
|
||||
node.position = {
|
||||
x: nodeWithPosition.x - node.width! / 2,
|
||||
y: nodeWithPosition.y - node.height! / 2 + rankMap[nodeWithPosition.rank!].height! / 2,
|
||||
x: layoutInfo.x,
|
||||
y: yPosition,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
loopAndIterationNodes.forEach((parentNode) => {
|
||||
const childLayout = childLayoutsMap[parentNode.id]
|
||||
if (!childLayout) return
|
||||
if (!childLayout)
|
||||
return
|
||||
|
||||
const childNodes = draft.filter(node => node.parentId === parentNode.id)
|
||||
const {
|
||||
bounds,
|
||||
nodes: layoutNodes,
|
||||
} = childLayout
|
||||
|
||||
let minX = Infinity
|
||||
let minY = Infinity
|
||||
childNodes.forEach((childNode) => {
|
||||
const layoutInfo = layoutNodes.get(childNode.id)
|
||||
if (!layoutInfo)
|
||||
return
|
||||
|
||||
childNodes.forEach((node) => {
|
||||
if (childLayout.node(node.id)) {
|
||||
const childNodeWithPosition = childLayout.node(node.id)
|
||||
const nodeX = childNodeWithPosition.x - node.width! / 2
|
||||
const nodeY = childNodeWithPosition.y - node.height! / 2
|
||||
|
||||
minX = Math.min(minX, nodeX)
|
||||
minY = Math.min(minY, nodeY)
|
||||
}
|
||||
})
|
||||
|
||||
childNodes.forEach((node) => {
|
||||
if (childLayout.node(node.id)) {
|
||||
const childNodeWithPosition = childLayout.node(node.id)
|
||||
|
||||
node.position = {
|
||||
x: NODE_LAYOUT_HORIZONTAL_PADDING + (childNodeWithPosition.x - node.width! / 2 - minX),
|
||||
y: NODE_LAYOUT_VERTICAL_PADDING + (childNodeWithPosition.y - node.height! / 2 - minY),
|
||||
}
|
||||
childNode.position = {
|
||||
x: NODE_LAYOUT_HORIZONTAL_PADDING + (layoutInfo.x - bounds.minX),
|
||||
y: NODE_LAYOUT_VERTICAL_PADDING + (layoutInfo.y - bounds.minY),
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@ -354,7 +354,7 @@ export const useWorkflow = () => {
|
||||
return startNodes
|
||||
}, [nodesMap, getRootNodesById])
|
||||
|
||||
const isValidConnection = useCallback(({ source, sourceHandle, target }: Connection) => {
|
||||
const isValidConnection = useCallback(({ source, sourceHandle: _sourceHandle, target }: Connection) => {
|
||||
const {
|
||||
edges,
|
||||
getNodes,
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export {}
|
||||
|
||||
@ -407,7 +407,10 @@ const VarReferencePicker: FC<Props> = ({
|
||||
<WrapElem onClick={() => {
|
||||
if (readonly)
|
||||
return
|
||||
!isConstant ? setOpen(!open) : setControlFocus(Date.now())
|
||||
if (!isConstant)
|
||||
setOpen(!open)
|
||||
else
|
||||
setControlFocus(Date.now())
|
||||
}} className='group/picker-trigger-wrap relative !flex'>
|
||||
<>
|
||||
{isAddBtnTrigger
|
||||
@ -457,7 +460,10 @@ const VarReferencePicker: FC<Props> = ({
|
||||
onClick={() => {
|
||||
if (readonly)
|
||||
return
|
||||
!isConstant ? setOpen(!open) : setControlFocus(Date.now())
|
||||
if (!isConstant)
|
||||
setOpen(!open)
|
||||
else
|
||||
setControlFocus(Date.now())
|
||||
}}
|
||||
className='h-full grow'
|
||||
>
|
||||
|
||||
@ -137,7 +137,7 @@ const Item: FC<ItemProps> = ({
|
||||
const isHovering = isItemHovering || isChildrenHovering
|
||||
const open = (isObj || isStructureOutput) && isHovering
|
||||
useEffect(() => {
|
||||
onHovering && onHovering(isHovering)
|
||||
onHovering?.(isHovering)
|
||||
}, [isHovering])
|
||||
const handleChosen = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
|
||||
@ -25,12 +25,12 @@ type Props = {
|
||||
} & Partial<ResultPanelProps>
|
||||
|
||||
const LastRun: FC<Props> = ({
|
||||
appId,
|
||||
appId: _appId,
|
||||
nodeId,
|
||||
canSingleRun,
|
||||
isRunAfterSingleRun,
|
||||
updateNodeRunningStatus,
|
||||
nodeInfo,
|
||||
nodeInfo: _nodeInfo,
|
||||
runningStatus: oneStepRunRunningStatus,
|
||||
onSingleRunClicked,
|
||||
singleRunResult,
|
||||
|
||||
@ -33,7 +33,7 @@ export const useResizePanel = (params?: UseResizePanelParams) => {
|
||||
const initContainerWidthRef = useRef(0)
|
||||
const initContainerHeightRef = useRef(0)
|
||||
const isResizingRef = useRef(false)
|
||||
const [prevUserSelectStyle, setPrevUserSelectStyle] = useState(getComputedStyle(document.body).userSelect)
|
||||
const [prevUserSelectStyle, setPrevUserSelectStyle] = useState(() => getComputedStyle(document.body).userSelect)
|
||||
|
||||
const handleStartResize = useCallback((e: MouseEvent) => {
|
||||
initXRef.current = e.clientX
|
||||
|
||||
@ -16,7 +16,7 @@ const strToKeyValueList = (value: string) => {
|
||||
}
|
||||
|
||||
const useKeyValueList = (value: string, onChange: (value: string) => void, noFilter?: boolean) => {
|
||||
const [list, doSetList] = useState<KeyValue[]>(value ? strToKeyValueList(value) : [])
|
||||
const [list, doSetList] = useState<KeyValue[]>(() => value ? strToKeyValueList(value) : [])
|
||||
const setList = (l: KeyValue[]) => {
|
||||
doSetList(l.map((item) => {
|
||||
return {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user