mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 02:18:08 +08:00
Merge branch 'main' into feat/rag-2
This commit is contained in:
@ -8,7 +8,6 @@ import {
|
||||
} from '@heroicons/react/24/outline'
|
||||
import { RiCloseLine, RiEditFill } from '@remixicon/react'
|
||||
import { get } from 'lodash-es'
|
||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||
import dayjs from 'dayjs'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import timezone from 'dayjs/plugin/timezone'
|
||||
@ -111,7 +110,8 @@ const statusTdRender = (statusCount: StatusCount) => {
|
||||
|
||||
const getFormattedChatList = (messages: ChatMessage[], conversationId: string, timezone: string, format: string) => {
|
||||
const newChatList: IChatItem[] = []
|
||||
messages.forEach((item: ChatMessage) => {
|
||||
try {
|
||||
messages.forEach((item: ChatMessage) => {
|
||||
const questionFiles = item.message_files?.filter((file: any) => file.belongs_to === 'user') || []
|
||||
newChatList.push({
|
||||
id: `question-${item.id}`,
|
||||
@ -178,7 +178,13 @@ const getFormattedChatList = (messages: ChatMessage[], conversationId: string, t
|
||||
parentMessageId: `question-${item.id}`,
|
||||
})
|
||||
})
|
||||
return newChatList
|
||||
|
||||
return newChatList
|
||||
}
|
||||
catch (error) {
|
||||
console.error('getFormattedChatList processing failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
type IDetailPanel = {
|
||||
@ -188,6 +194,9 @@ type IDetailPanel = {
|
||||
}
|
||||
|
||||
function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
const MIN_ITEMS_FOR_SCROLL_LOADING = 8
|
||||
const SCROLL_THRESHOLD_PX = 50
|
||||
const SCROLL_DEBOUNCE_MS = 200
|
||||
const { userProfile: { timezone } } = useAppContext()
|
||||
const { formatTime } = useTimestamp()
|
||||
const { onClose, appDetail } = useContext(DrawerContext)
|
||||
@ -204,13 +213,19 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
const { t } = useTranslation()
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [varValues, setVarValues] = useState<Record<string, string>>({})
|
||||
const isLoadingRef = useRef(false)
|
||||
|
||||
const [allChatItems, setAllChatItems] = useState<IChatItem[]>([])
|
||||
const [chatItemTree, setChatItemTree] = useState<ChatItemInTree[]>([])
|
||||
const [threadChatItems, setThreadChatItems] = useState<IChatItem[]>([])
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
if (isLoadingRef.current)
|
||||
return
|
||||
|
||||
try {
|
||||
isLoadingRef.current = true
|
||||
|
||||
if (!hasMore)
|
||||
return
|
||||
|
||||
@ -218,8 +233,11 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
conversation_id: detail.id,
|
||||
limit: 10,
|
||||
}
|
||||
if (allChatItems[0]?.id)
|
||||
params.first_id = allChatItems[0]?.id.replace('question-', '')
|
||||
// Use the oldest answer item ID for pagination
|
||||
const answerItems = allChatItems.filter(item => item.isAnswer)
|
||||
const oldestAnswerItem = answerItems[answerItems.length - 1]
|
||||
if (oldestAnswerItem?.id)
|
||||
params.first_id = oldestAnswerItem.id
|
||||
const messageRes = await fetchChatMessages({
|
||||
url: `/apps/${appDetail?.id}/chat-messages`,
|
||||
params,
|
||||
@ -249,15 +267,20 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
}
|
||||
setChatItemTree(tree)
|
||||
|
||||
setThreadChatItems(getThreadMessages(tree, newAllChatItems.at(-1)?.id))
|
||||
const lastMessageId = newAllChatItems.length > 0 ? newAllChatItems[newAllChatItems.length - 1].id : undefined
|
||||
setThreadChatItems(getThreadMessages(tree, lastMessageId))
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
console.error('fetchData execution failed:', err)
|
||||
}
|
||||
finally {
|
||||
isLoadingRef.current = false
|
||||
}
|
||||
}, [allChatItems, detail.id, hasMore, timezone, t, appDetail, detail?.model_config?.configs?.introduction])
|
||||
|
||||
const switchSibling = useCallback((siblingMessageId: string) => {
|
||||
setThreadChatItems(getThreadMessages(chatItemTree, siblingMessageId))
|
||||
const newThreadChatItems = getThreadMessages(chatItemTree, siblingMessageId)
|
||||
setThreadChatItems(newThreadChatItems)
|
||||
}, [chatItemTree])
|
||||
|
||||
const handleAnnotationEdited = useCallback((query: string, answer: string, index: number) => {
|
||||
@ -344,13 +367,217 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
|
||||
const fetchInitiated = useRef(false)
|
||||
|
||||
// Only load initial messages, don't auto-load more
|
||||
useEffect(() => {
|
||||
if (appDetail?.id && detail.id && appDetail?.mode !== 'completion' && !fetchInitiated.current) {
|
||||
// Mark as initialized, but don't auto-load more messages
|
||||
fetchInitiated.current = true
|
||||
// Still call fetchData to get initial messages
|
||||
fetchData()
|
||||
}
|
||||
}, [appDetail?.id, detail.id, appDetail?.mode, fetchData])
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const loadMoreMessages = useCallback(async () => {
|
||||
if (isLoading || !hasMore || !appDetail?.id || !detail.id)
|
||||
return
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
const params: ChatMessagesRequest = {
|
||||
conversation_id: detail.id,
|
||||
limit: 10,
|
||||
}
|
||||
|
||||
// Use the earliest response item as the first_id
|
||||
const answerItems = allChatItems.filter(item => item.isAnswer)
|
||||
const oldestAnswerItem = answerItems[answerItems.length - 1]
|
||||
if (oldestAnswerItem?.id) {
|
||||
params.first_id = oldestAnswerItem.id
|
||||
}
|
||||
else if (allChatItems.length > 0 && allChatItems[0]?.id) {
|
||||
const firstId = allChatItems[0].id.replace('question-', '').replace('answer-', '')
|
||||
params.first_id = firstId
|
||||
}
|
||||
|
||||
const messageRes = await fetchChatMessages({
|
||||
url: `/apps/${appDetail.id}/chat-messages`,
|
||||
params,
|
||||
})
|
||||
|
||||
if (!messageRes.data || messageRes.data.length === 0) {
|
||||
setHasMore(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (messageRes.data.length > 0) {
|
||||
const varValues = messageRes.data.at(-1)!.inputs
|
||||
setVarValues(varValues)
|
||||
}
|
||||
|
||||
setHasMore(messageRes.has_more)
|
||||
|
||||
const newItems = getFormattedChatList(
|
||||
messageRes.data,
|
||||
detail.id,
|
||||
timezone!,
|
||||
t('appLog.dateTimeFormat') as string,
|
||||
)
|
||||
|
||||
// Check for duplicate messages
|
||||
const existingIds = new Set(allChatItems.map(item => item.id))
|
||||
const uniqueNewItems = newItems.filter(item => !existingIds.has(item.id))
|
||||
|
||||
if (uniqueNewItems.length === 0) {
|
||||
if (allChatItems.length > 1) {
|
||||
const nextId = allChatItems[1].id.replace('question-', '').replace('answer-', '')
|
||||
|
||||
const retryParams = {
|
||||
...params,
|
||||
first_id: nextId,
|
||||
}
|
||||
|
||||
const retryRes = await fetchChatMessages({
|
||||
url: `/apps/${appDetail.id}/chat-messages`,
|
||||
params: retryParams,
|
||||
})
|
||||
|
||||
if (retryRes.data && retryRes.data.length > 0) {
|
||||
const retryItems = getFormattedChatList(
|
||||
retryRes.data,
|
||||
detail.id,
|
||||
timezone!,
|
||||
t('appLog.dateTimeFormat') as string,
|
||||
)
|
||||
|
||||
const retryUniqueItems = retryItems.filter(item => !existingIds.has(item.id))
|
||||
if (retryUniqueItems.length > 0) {
|
||||
const newAllChatItems = [
|
||||
...retryUniqueItems,
|
||||
...allChatItems,
|
||||
]
|
||||
|
||||
setAllChatItems(newAllChatItems)
|
||||
|
||||
let tree = buildChatItemTree(newAllChatItems)
|
||||
if (retryRes.has_more === false && detail?.model_config?.configs?.introduction) {
|
||||
tree = [{
|
||||
id: 'introduction',
|
||||
isAnswer: true,
|
||||
isOpeningStatement: true,
|
||||
content: detail?.model_config?.configs?.introduction ?? 'hello',
|
||||
feedbackDisabled: true,
|
||||
children: tree,
|
||||
}]
|
||||
}
|
||||
setChatItemTree(tree)
|
||||
setHasMore(retryRes.has_more)
|
||||
setThreadChatItems(getThreadMessages(tree, newAllChatItems.at(-1)?.id))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newAllChatItems = [
|
||||
...uniqueNewItems,
|
||||
...allChatItems,
|
||||
]
|
||||
|
||||
setAllChatItems(newAllChatItems)
|
||||
|
||||
let tree = buildChatItemTree(newAllChatItems)
|
||||
if (messageRes.has_more === false && detail?.model_config?.configs?.introduction) {
|
||||
tree = [{
|
||||
id: 'introduction',
|
||||
isAnswer: true,
|
||||
isOpeningStatement: true,
|
||||
content: detail?.model_config?.configs?.introduction ?? 'hello',
|
||||
feedbackDisabled: true,
|
||||
children: tree,
|
||||
}]
|
||||
}
|
||||
setChatItemTree(tree)
|
||||
|
||||
setThreadChatItems(getThreadMessages(tree, newAllChatItems.at(-1)?.id))
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
setHasMore(false)
|
||||
}
|
||||
finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [allChatItems, detail.id, hasMore, isLoading, timezone, t, appDetail])
|
||||
|
||||
useEffect(() => {
|
||||
const scrollableDiv = document.getElementById('scrollableDiv')
|
||||
const outerDiv = scrollableDiv?.parentElement
|
||||
const chatContainer = document.querySelector('.mx-1.mb-1.grow.overflow-auto') as HTMLElement
|
||||
|
||||
let scrollContainer: HTMLElement | null = null
|
||||
|
||||
if (outerDiv && outerDiv.scrollHeight > outerDiv.clientHeight) {
|
||||
scrollContainer = outerDiv
|
||||
}
|
||||
else if (scrollableDiv && scrollableDiv.scrollHeight > scrollableDiv.clientHeight) {
|
||||
scrollContainer = scrollableDiv
|
||||
}
|
||||
else if (chatContainer && chatContainer.scrollHeight > chatContainer.clientHeight) {
|
||||
scrollContainer = chatContainer
|
||||
}
|
||||
else {
|
||||
const possibleContainers = document.querySelectorAll('.overflow-auto, .overflow-y-auto')
|
||||
for (let i = 0; i < possibleContainers.length; i++) {
|
||||
const container = possibleContainers[i] as HTMLElement
|
||||
if (container.scrollHeight > container.clientHeight) {
|
||||
scrollContainer = container
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!scrollContainer)
|
||||
return
|
||||
|
||||
let lastLoadTime = 0
|
||||
const throttleDelay = 200
|
||||
|
||||
const handleScroll = () => {
|
||||
const currentScrollTop = scrollContainer!.scrollTop
|
||||
const scrollHeight = scrollContainer!.scrollHeight
|
||||
const clientHeight = scrollContainer!.clientHeight
|
||||
|
||||
const distanceFromTop = currentScrollTop
|
||||
const distanceFromBottom = scrollHeight - currentScrollTop - clientHeight
|
||||
|
||||
const now = Date.now()
|
||||
|
||||
const isNearTop = distanceFromTop < 30
|
||||
// eslint-disable-next-line sonarjs/no-unused-vars
|
||||
const _distanceFromBottom = distanceFromBottom < 30
|
||||
if (isNearTop && hasMore && !isLoading && (now - lastLoadTime > throttleDelay)) {
|
||||
lastLoadTime = now
|
||||
loadMoreMessages()
|
||||
}
|
||||
}
|
||||
|
||||
scrollContainer.addEventListener('scroll', handleScroll, { passive: true })
|
||||
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
if (e.deltaY < 0)
|
||||
handleScroll()
|
||||
}
|
||||
scrollContainer.addEventListener('wheel', handleWheel, { passive: true })
|
||||
|
||||
return () => {
|
||||
scrollContainer!.removeEventListener('scroll', handleScroll)
|
||||
scrollContainer!.removeEventListener('wheel', handleWheel)
|
||||
}
|
||||
}, [hasMore, isLoading, loadMoreMessages])
|
||||
|
||||
const isChatMode = appDetail?.mode !== 'completion'
|
||||
const isAdvanced = appDetail?.mode === 'advanced-chat'
|
||||
|
||||
@ -378,6 +605,36 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
return () => cancelAnimationFrame(raf)
|
||||
}, [])
|
||||
|
||||
// Add scroll listener to ensure loading is triggered
|
||||
useEffect(() => {
|
||||
if (threadChatItems.length >= MIN_ITEMS_FOR_SCROLL_LOADING && hasMore) {
|
||||
const scrollableDiv = document.getElementById('scrollableDiv')
|
||||
|
||||
if (scrollableDiv) {
|
||||
let loadingTimeout: NodeJS.Timeout | null = null
|
||||
|
||||
const handleScroll = () => {
|
||||
const { scrollTop } = scrollableDiv
|
||||
|
||||
// Trigger loading when scrolling near the top
|
||||
if (scrollTop < SCROLL_THRESHOLD_PX && !isLoadingRef.current) {
|
||||
if (loadingTimeout)
|
||||
clearTimeout(loadingTimeout)
|
||||
|
||||
loadingTimeout = setTimeout(fetchData, SCROLL_DEBOUNCE_MS) // 200ms debounce
|
||||
}
|
||||
}
|
||||
|
||||
scrollableDiv.addEventListener('scroll', handleScroll)
|
||||
return () => {
|
||||
scrollableDiv.removeEventListener('scroll', handleScroll)
|
||||
if (loadingTimeout)
|
||||
clearTimeout(loadingTimeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [threadChatItems.length, hasMore, fetchData])
|
||||
|
||||
return (
|
||||
<div ref={ref} className='flex h-full flex-col rounded-xl border-[0.5px] border-components-panel-border'>
|
||||
{/* Panel Header */}
|
||||
@ -439,8 +696,8 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
siteInfo={null}
|
||||
/>
|
||||
</div>
|
||||
: threadChatItems.length < 8
|
||||
? <div className="mb-4 pt-4">
|
||||
: threadChatItems.length < MIN_ITEMS_FOR_SCROLL_LOADING ? (
|
||||
<div className="mb-4 pt-4">
|
||||
<Chat
|
||||
config={{
|
||||
appId: appDetail?.id,
|
||||
@ -466,35 +723,27 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
switchSibling={switchSibling}
|
||||
/>
|
||||
</div>
|
||||
: <div
|
||||
) : (
|
||||
<div
|
||||
className="py-4"
|
||||
id="scrollableDiv"
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column-reverse',
|
||||
height: '100%',
|
||||
overflow: 'auto',
|
||||
}}>
|
||||
{/* Put the scroll bar always on the bottom */}
|
||||
<InfiniteScroll
|
||||
scrollableTarget="scrollableDiv"
|
||||
dataLength={threadChatItems.length}
|
||||
next={fetchData}
|
||||
hasMore={hasMore}
|
||||
loader={<div className='system-xs-regular text-center text-text-tertiary'>{t('appLog.detail.loading')}...</div>}
|
||||
// endMessage={<div className='text-center'>Nothing more to show</div>}
|
||||
// below props only if you need pull down functionality
|
||||
refreshFunction={fetchData}
|
||||
pullDownToRefresh
|
||||
pullDownToRefreshThreshold={50}
|
||||
// pullDownToRefreshContent={
|
||||
// <div className='text-center'>Pull down to refresh</div>
|
||||
// }
|
||||
// releaseToRefreshContent={
|
||||
// <div className='text-center'>Release to refresh</div>
|
||||
// }
|
||||
// To put endMessage and loader to the top.
|
||||
style={{ display: 'flex', flexDirection: 'column-reverse' }}
|
||||
inverse={true}
|
||||
>
|
||||
<div className="flex w-full flex-col-reverse" style={{ position: 'relative' }}>
|
||||
{/* Loading state indicator - only shown when loading */}
|
||||
{hasMore && isLoading && (
|
||||
<div className="sticky left-0 right-0 top-0 z-10 bg-primary-50/40 py-3 text-center">
|
||||
<div className='system-xs-regular text-text-tertiary'>
|
||||
{t('appLog.detail.loading')}...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Chat
|
||||
config={{
|
||||
appId: appDetail?.id,
|
||||
@ -519,8 +768,9 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
chatContainerInnerClassName='px-3'
|
||||
switchSibling={switchSibling}
|
||||
/>
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{showMessageLogModal && (
|
||||
|
||||
@ -407,8 +407,8 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
}
|
||||
btnClassName={open =>
|
||||
cn(
|
||||
open ? '!bg-black/5 !shadow-none' : '!bg-transparent',
|
||||
'h-8 w-8 rounded-md border-none !p-2 hover:!bg-black/5',
|
||||
open ? '!bg-state-base-hover !shadow-none' : '!bg-transparent',
|
||||
'h-8 w-8 rounded-md border-none !p-2 hover:!bg-state-base-hover',
|
||||
)
|
||||
}
|
||||
popupClassName={
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import s from './style.module.css'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
export type ILoadingAnimProps = {
|
||||
type: 'text' | 'avatar'
|
||||
@ -11,7 +12,7 @@ const LoadingAnim: FC<ILoadingAnimProps> = ({
|
||||
type,
|
||||
}) => {
|
||||
return (
|
||||
<div className={`${s['dot-flashing']} ${s[type]}`}></div>
|
||||
<div className={cn(s['dot-flashing'], s[type])} />
|
||||
)
|
||||
}
|
||||
export default React.memo(LoadingAnim)
|
||||
|
||||
@ -8,6 +8,7 @@ import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import ConfirmAddVar from '@/app/components/app/configuration/config-prompt/confirm-add-var'
|
||||
import PromptEditor from '@/app/components/base/prompt-editor'
|
||||
import type { OpeningStatement } from '@/app/components/base/features/types'
|
||||
import { getInputKeys } from '@/app/components/base/block-input'
|
||||
import type { PromptVariable } from '@/models/debug'
|
||||
@ -101,7 +102,7 @@ const OpeningSettingModal = ({
|
||||
<div>·</div>
|
||||
<div>{tempSuggestedQuestions.length}/{MAX_QUESTION_NUM}</div>
|
||||
</div>
|
||||
<Divider bgStyle='gradient' className='ml-3 h-px w-0 grow'/>
|
||||
<Divider bgStyle='gradient' className='ml-3 h-px w-0 grow' />
|
||||
</div>
|
||||
<ReactSortable
|
||||
className="space-y-1"
|
||||
@ -178,19 +179,32 @@ const OpeningSettingModal = ({
|
||||
>
|
||||
<div className='mb-6 flex items-center justify-between'>
|
||||
<div className='title-2xl-semi-bold text-text-primary'>{t('appDebug.feature.conversationOpener.title')}</div>
|
||||
<div className='cursor-pointer p-1' onClick={onCancel}><RiCloseLine className='h-4 w-4 text-text-tertiary'/></div>
|
||||
<div className='cursor-pointer p-1' onClick={onCancel}><RiCloseLine className='h-4 w-4 text-text-tertiary' /></div>
|
||||
</div>
|
||||
<div className='mb-8 flex gap-2'>
|
||||
<div className='mt-1.5 h-8 w-8 shrink-0 rounded-lg border-components-panel-border bg-util-colors-orange-dark-orange-dark-500 p-1.5'>
|
||||
<RiAsterisk className='h-5 w-5 text-text-primary-on-surface' />
|
||||
</div>
|
||||
<div className='grow rounded-2xl border-t border-divider-subtle bg-chat-bubble-bg p-3 shadow-xs'>
|
||||
<textarea
|
||||
<PromptEditor
|
||||
value={tempValue}
|
||||
rows={3}
|
||||
onChange={e => setTempValue(e.target.value)}
|
||||
className="system-md-regular w-full border-0 bg-transparent px-0 text-text-secondary focus:outline-none"
|
||||
onChange={setTempValue}
|
||||
placeholder={t('appDebug.openingStatement.placeholder') as string}
|
||||
variableBlock={{
|
||||
show: true,
|
||||
variables: [
|
||||
// Prompt variables
|
||||
...promptVariables.map(item => ({
|
||||
name: item.name || item.key,
|
||||
value: item.key,
|
||||
})),
|
||||
// Workflow variables
|
||||
...workflowVariables.map(item => ({
|
||||
name: item.variable,
|
||||
value: item.variable,
|
||||
})),
|
||||
],
|
||||
}}
|
||||
/>
|
||||
{renderQuestions()}
|
||||
</div>
|
||||
|
||||
@ -137,7 +137,7 @@ const HitTestingPage: FC<Props> = ({ datasetId }: Props) => {
|
||||
<>
|
||||
<div className='grow overflow-y-auto'>
|
||||
<table className={'w-full border-collapse border-0 text-[13px] leading-4 text-text-secondary '}>
|
||||
<thead className='sticky top-0 h-7 text-xs font-medium uppercase leading-7 text-text-tertiary'>
|
||||
<thead className='sticky top-0 h-7 text-xs font-medium uppercase leading-7 text-text-tertiary backdrop-blur-[5px]'>
|
||||
<tr>
|
||||
<td className='w-[128px] rounded-l-lg bg-background-section-burn pl-3'>{t('datasetHitTesting.table.header.source')}</td>
|
||||
<td className='bg-background-section-burn'>{t('datasetHitTesting.table.header.text')}</td>
|
||||
|
||||
@ -5,7 +5,7 @@ import type { GithubRepo } from '@/models/common'
|
||||
import { RiLoader2Line } from '@remixicon/react'
|
||||
|
||||
const defaultData = {
|
||||
stargazers_count: 98570,
|
||||
stargazers_count: 110918,
|
||||
}
|
||||
|
||||
const getStar = async () => {
|
||||
|
||||
@ -79,6 +79,9 @@ const SwrInitializer = ({
|
||||
<SWRConfig value={{
|
||||
shouldRetryOnError: false,
|
||||
revalidateOnFocus: false,
|
||||
dedupingInterval: 60000,
|
||||
focusThrottleInterval: 5000,
|
||||
provider: () => new Map(),
|
||||
}}>
|
||||
{children}
|
||||
</SWRConfig>
|
||||
|
||||
15
web/app/components/tools/labels/store.ts
Normal file
15
web/app/components/tools/labels/store.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { create } from 'zustand'
|
||||
import type { Label } from './constant'
|
||||
|
||||
type State = {
|
||||
labelList: Label[]
|
||||
}
|
||||
|
||||
type Action = {
|
||||
setLabelList: (labelList?: Label[]) => void
|
||||
}
|
||||
|
||||
export const useStore = create<State & Action>(set => ({
|
||||
labelList: [],
|
||||
setLabelList: labelList => set(() => ({ labelList })),
|
||||
}))
|
||||
@ -246,11 +246,11 @@ export const useWorkflow = () => {
|
||||
|
||||
const handleOutVarRenameChange = useCallback((nodeId: string, oldValeSelector: ValueSelector, newVarSelector: ValueSelector) => {
|
||||
const { getNodes, setNodes } = store.getState()
|
||||
const afterNodes = getAfterNodesInSameBranch(nodeId)
|
||||
const effectNodes = findUsedVarNodes(oldValeSelector, afterNodes)
|
||||
if (effectNodes.length > 0) {
|
||||
const newNodes = getNodes().map((node) => {
|
||||
if (effectNodes.find(n => n.id === node.id))
|
||||
const allNodes = getNodes()
|
||||
const affectedNodes = findUsedVarNodes(oldValeSelector, allNodes)
|
||||
if (affectedNodes.length > 0) {
|
||||
const newNodes = allNodes.map((node) => {
|
||||
if (affectedNodes.find(n => n.id === node.id))
|
||||
return updateNodeVars(node, oldValeSelector, newVarSelector)
|
||||
|
||||
return node
|
||||
|
||||
@ -1101,7 +1101,15 @@ export const getNodeUsedVars = (node: Node): ValueSelector[] => {
|
||||
res = (data as IfElseNodeType).conditions?.map((c) => {
|
||||
return c.variable_selector || []
|
||||
}) || []
|
||||
res.push(...((data as IfElseNodeType).cases || []).flatMap(c => (c.conditions || [])).map(c => c.variable_selector || []))
|
||||
res.push(...((data as IfElseNodeType).cases || []).flatMap(c => (c.conditions || [])).flatMap((c) => {
|
||||
const selectors: ValueSelector[] = []
|
||||
if (c.variable_selector)
|
||||
selectors.push(c.variable_selector)
|
||||
// Handle sub-variable conditions
|
||||
if (c.sub_variable_condition && c.sub_variable_condition.conditions)
|
||||
selectors.push(...c.sub_variable_condition.conditions.map(subC => subC.variable_selector || []).filter(sel => sel.length > 0))
|
||||
return selectors
|
||||
}))
|
||||
break
|
||||
}
|
||||
case BlockEnum.Code: {
|
||||
@ -1345,6 +1353,26 @@ export const updateNodeVars = (oldNode: Node, oldVarSelector: ValueSelector, new
|
||||
return c
|
||||
})
|
||||
}
|
||||
if (payload.cases) {
|
||||
payload.cases = payload.cases.map((caseItem) => {
|
||||
if (caseItem.conditions) {
|
||||
caseItem.conditions = caseItem.conditions.map((c) => {
|
||||
if (c.variable_selector?.join('.') === oldVarSelector.join('.'))
|
||||
c.variable_selector = newVarSelector
|
||||
// Handle sub-variable conditions
|
||||
if (c.sub_variable_condition && c.sub_variable_condition.conditions) {
|
||||
c.sub_variable_condition.conditions = c.sub_variable_condition.conditions.map((subC) => {
|
||||
if (subC.variable_selector?.join('.') === oldVarSelector.join('.'))
|
||||
subC.variable_selector = newVarSelector
|
||||
return subC
|
||||
})
|
||||
}
|
||||
return c
|
||||
})
|
||||
}
|
||||
return caseItem
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case BlockEnum.Code: {
|
||||
|
||||
@ -90,7 +90,7 @@ const DebugAndPreview = () => {
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn(
|
||||
'relative flex h-full flex-col rounded-l-2xl border border-r-0 border-components-panel-border bg-components-panel-bg shadow-xl',
|
||||
'relative flex h-full flex-col rounded-l-2xl border border-r-0 border-components-panel-border bg-chatbot-bg shadow-xl',
|
||||
)}
|
||||
style={{ width: `${panelWidth}px` }}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user