fix(knowledge base): selector infinite render

This commit is contained in:
Vikhyath Mondreti
2025-06-30 17:53:51 -07:00
parent f4e627a9f7
commit 7a5aeadbb7
6 changed files with 76 additions and 170 deletions
@@ -1,6 +1,6 @@
'use client'
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { Check, ChevronDown, FileText } from 'lucide-react'
import { Button } from '@/components/ui/button'
import {
@@ -13,8 +13,8 @@ import {
} from '@/components/ui/command'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import type { SubBlockConfig } from '@/blocks/types'
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { useSubBlockValue } from '../../hooks/use-sub-block-value'
interface DocumentData {
id: string
@@ -52,38 +52,23 @@ export function DocumentSelector({
previewValue,
}: DocumentSelectorProps) {
const { getValue } = useSubBlockStore()
const { collaborativeSetSubblockValue } = useCollaborativeWorkflow()
const [documents, setDocuments] = useState<DocumentData[]>([])
const [error, setError] = useState<string | null>(null)
const [open, setOpen] = useState(false)
const [selectedDocument, setSelectedDocument] = useState<DocumentData | null>(null)
const [initialFetchDone, setInitialFetchDone] = useState(false)
const [selectedId, setSelectedId] = useState('')
// Get the current value from the store
const storeValue = getValue(blockId, subBlock.id)
// Use the proper hook to get the current value and setter
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id)
// Get the knowledge base ID from the same block's knowledgeBaseId subblock
const knowledgeBaseId = getValue(blockId, 'knowledgeBaseId')
// Get the knowledge base ID from the same block's knowledgeBaseId subblock - memoize to prevent re-renders
const knowledgeBaseId = useMemo(() => getValue(blockId, 'knowledgeBaseId'), [getValue, blockId])
// Use preview value when in preview mode, otherwise use store value
const value = isPreview ? previewValue : storeValue
// Initialize selectedId with the effective value
useEffect(() => {
if (isPreview && previewValue !== undefined) {
setSelectedId(previewValue || '')
} else {
setSelectedId(value || '')
}
}, [value, isPreview, previewValue])
// Update local state when external value changes
useEffect(() => {
const currentValue = isPreview ? previewValue : value
setSelectedId(currentValue || '')
}, [value, isPreview, previewValue])
// Fetch documents for the selected knowledge base
const fetchDocuments = useCallback(async () => {
@@ -112,46 +97,12 @@ export function DocumentSelector({
const fetchedDocuments = result.data || []
setDocuments(fetchedDocuments)
setInitialFetchDone(true)
// Auto-selection logic: if we have a valid selection, keep it
// If there's only one document, select it
// If we have a value but it's not in the documents, reset it
if (selectedId && !fetchedDocuments.some((doc: DocumentData) => doc.id === selectedId)) {
setSelectedId('')
if (!isPreview) {
collaborativeSetSubblockValue(blockId, subBlock.id, '')
}
}
if (
(!selectedId || !fetchedDocuments.some((doc: DocumentData) => doc.id === selectedId)) &&
fetchedDocuments.length > 0
) {
if (fetchedDocuments.length === 1) {
// If only one document, auto-select it
const singleDoc = fetchedDocuments[0]
setSelectedId(singleDoc.id)
setSelectedDocument(singleDoc)
if (!isPreview) {
collaborativeSetSubblockValue(blockId, subBlock.id, singleDoc.id)
}
onDocumentSelect?.(singleDoc.id)
}
}
} catch (err) {
if ((err as Error).name === 'AbortError') return
setError((err as Error).message)
setDocuments([])
}
}, [
knowledgeBaseId,
selectedId,
collaborativeSetSubblockValue,
blockId,
subBlock.id,
isPreview,
onDocumentSelect,
])
}, [knowledgeBaseId])
// Handle dropdown open/close - fetch documents when opening
const handleOpenChange = (isOpen: boolean) => {
@@ -170,50 +121,35 @@ export function DocumentSelector({
if (isPreview) return
setSelectedDocument(document)
setSelectedId(document.id)
if (!isPreview) {
collaborativeSetSubblockValue(blockId, subBlock.id, document.id)
}
setStoreValue(document.id)
onDocumentSelect?.(document.id)
setOpen(false)
}
// Sync selected document with value prop
useEffect(() => {
if (selectedId && documents.length > 0) {
const docInfo = documents.find((doc) => doc.id === selectedId)
if (docInfo) {
setSelectedDocument(docInfo)
} else {
setSelectedDocument(null)
}
} else if (!selectedId) {
if (value && documents.length > 0) {
const docInfo = documents.find((doc) => doc.id === value)
setSelectedDocument(docInfo || null)
} else {
setSelectedDocument(null)
}
}, [selectedId, documents])
}, [value, documents])
// Reset documents when knowledge base changes
useEffect(() => {
if (knowledgeBaseId) {
setDocuments([])
setSelectedDocument(null)
setSelectedId('')
setInitialFetchDone(false)
setError(null)
if (!isPreview) {
collaborativeSetSubblockValue(blockId, subBlock.id, '')
}
}
}, [knowledgeBaseId, blockId, subBlock.id, collaborativeSetSubblockValue, isPreview])
setDocuments([])
setSelectedDocument(null)
setInitialFetchDone(false)
setError(null)
}, [knowledgeBaseId])
// Fetch documents when knowledge base is available and we haven't fetched yet
useEffect(() => {
if (knowledgeBaseId && !initialFetchDone && !isPreview) {
fetchDocuments()
}
}, [knowledgeBaseId, initialFetchDone, fetchDocuments, isPreview])
}, [knowledgeBaseId, initialFetchDone, isPreview])
const formatDocumentName = (document: DocumentData) => {
return document.filename
@@ -307,7 +243,7 @@ export function DocumentSelector({
</div>
</div>
</div>
{document.id === selectedId && <Check className='ml-auto h-4 w-4' />}
{document.id === value && <Check className='ml-auto h-4 w-4' />}
</CommandItem>
))}
</CommandGroup>
@@ -7,6 +7,7 @@ import type { SubBlockConfig } from '@/blocks/types'
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { useSubBlockValue } from '../../hooks/use-sub-block-value'
import type { ConfluenceFileInfo } from './components/confluence-file-selector'
import { ConfluenceFileSelector } from './components/confluence-file-selector'
import type { DiscordChannelInfo } from './components/discord-channel-selector'
@@ -40,6 +41,9 @@ export function FileSelectorInput({
const { getValue } = useSubBlockStore()
const { collaborativeSetSubblockValue } = useCollaborativeWorkflow()
const { activeWorkflowId } = useWorkflowRegistry()
// Use the proper hook to get the current value and setter
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id)
const [selectedFileId, setSelectedFileId] = useState<string>('')
const [_fileInfo, setFileInfo] = useState<FileInfo | ConfluenceFileInfo | null>(null)
const [selectedIssueId, setSelectedIssueId] = useState<string>('')
@@ -66,7 +70,7 @@ export function FileSelectorInput({
const serverId = isDiscord ? (getValue(blockId, 'serverId') as string) || '' : ''
// Use preview value when in preview mode, otherwise use store value
const value = isPreview ? previewValue : getValue(blockId, subBlock.id)
const value = isPreview ? previewValue : storeValue
// Get the current value from the store or prop value if in preview mode
useEffect(() => {
@@ -117,14 +121,14 @@ export function FileSelectorInput({
const handleFileChange = (fileId: string, info?: any) => {
setSelectedFileId(fileId)
setFileInfo(info || null)
collaborativeSetSubblockValue(blockId, subBlock.id, fileId)
setStoreValue(fileId)
}
// Handle issue selection
const handleIssueChange = (issueKey: string, info?: JiraIssueInfo) => {
setSelectedIssueId(issueKey)
setIssueInfo(info || null)
collaborativeSetSubblockValue(blockId, subBlock.id, issueKey)
setStoreValue(issueKey)
// Clear the fields when a new issue is selected
if (isJira) {
@@ -137,14 +141,14 @@ export function FileSelectorInput({
const handleChannelChange = (channelId: string, info?: DiscordChannelInfo) => {
setSelectedChannelId(channelId)
setChannelInfo(info || null)
collaborativeSetSubblockValue(blockId, subBlock.id, channelId)
setStoreValue(channelId)
}
// Handle calendar selection
const handleCalendarChange = (calendarId: string, info?: GoogleCalendarInfo) => {
setSelectedCalendarId(calendarId)
setCalendarInfo(info || null)
collaborativeSetSubblockValue(blockId, subBlock.id, calendarId)
setStoreValue(calendarId)
}
// For Google Drive
@@ -4,7 +4,6 @@ import { useRef, useState } from 'react'
import { X } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Progress } from '@/components/ui/progress'
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
import { useNotificationStore } from '@/stores/notifications/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
@@ -58,7 +57,6 @@ export function FileUpload({
// Stores
const { addNotification } = useNotificationStore()
const { activeWorkflowId } = useWorkflowRegistry()
const { collaborativeSetSubblockValue } = useCollaborativeWorkflow()
// Use preview value when in preview mode, otherwise use store value
const value = isPreview ? previewValue : storeValue
@@ -298,16 +296,10 @@ export function FileUpload({
const newFiles = Array.from(uniqueFiles.values())
setStoreValue(newFiles)
// Use collaborative update for persistence
collaborativeSetSubblockValue(blockId, subBlockId, newFiles)
useWorkflowStore.getState().triggerUpdate()
} else {
// For single file: Replace with last uploaded file
setStoreValue(uploadedFiles[0] || null)
// Use collaborative update for persistence
collaborativeSetSubblockValue(blockId, subBlockId, uploadedFiles[0] || null)
useWorkflowStore.getState().triggerUpdate()
}
} catch (error) {
@@ -363,19 +355,9 @@ export function FileUpload({
const filesArray = Array.isArray(value) ? value : value ? [value] : []
const updatedFiles = filesArray.filter((f) => f.path !== file.path)
setStoreValue(updatedFiles.length > 0 ? updatedFiles : null)
// Use collaborative update for persistence
collaborativeSetSubblockValue(
blockId,
subBlockId,
updatedFiles.length > 0 ? updatedFiles : null
)
} else {
// For single file: Clear the value
setStoreValue(null)
// Use collaborative update for persistence
collaborativeSetSubblockValue(blockId, subBlockId, null)
}
useWorkflowStore.getState().triggerUpdate()
@@ -416,7 +398,6 @@ export function FileUpload({
// Clear input state immediately for better UX
setStoreValue(null)
collaborativeSetSubblockValue(blockId, subBlockId, null)
useWorkflowStore.getState().triggerUpdate()
if (fileInputRef.current) {
@@ -1,6 +1,6 @@
'use client'
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { Check, ChevronDown, RefreshCw, X } from 'lucide-react'
import { PackageSearchIcon } from '@/components/icons'
import { Button } from '@/components/ui/button'
@@ -14,9 +14,8 @@ import {
} from '@/components/ui/command'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import type { SubBlockConfig } from '@/blocks/types'
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
import { type KnowledgeBaseData, useKnowledgeStore } from '@/stores/knowledge/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { useSubBlockValue } from '../../../sub-block/hooks/use-sub-block-value'
interface KnowledgeBaseSelectorProps {
blockId: string
@@ -37,24 +36,39 @@ export function KnowledgeBaseSelector({
}: KnowledgeBaseSelectorProps) {
const { getKnowledgeBasesList, knowledgeBasesList, loadingKnowledgeBasesList } =
useKnowledgeStore()
const { getValue } = useSubBlockStore()
const { collaborativeSetSubblockValue } = useCollaborativeWorkflow()
const [knowledgeBases, setKnowledgeBases] = useState<KnowledgeBaseData[]>([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [open, setOpen] = useState(false)
const [selectedKnowledgeBases, setSelectedKnowledgeBases] = useState<KnowledgeBaseData[]>([])
const [initialFetchDone, setInitialFetchDone] = useState(false)
// Get the current value from the store
const storeValue = getValue(blockId, subBlock.id)
// Use the proper hook to get the current value and setter - this prevents infinite loops
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id)
// Use preview value when in preview mode, otherwise use store value
const value = isPreview ? previewValue : storeValue
const isMultiSelect = subBlock.multiSelect === true
// Compute selected knowledge bases directly from value - no local state to avoid loops
const selectedKnowledgeBases = useMemo(() => {
if (value && knowledgeBases.length > 0) {
const selectedIds =
typeof value === 'string'
? value.includes(',')
? value
.split(',')
.map((id) => id.trim())
.filter((id) => id.length > 0)
: [value]
: []
return knowledgeBases.filter((kb) => selectedIds.includes(kb.id))
}
return []
}, [value, knowledgeBases])
// Fetch knowledge bases
const fetchKnowledgeBases = useCallback(async () => {
setLoading(true)
@@ -89,12 +103,8 @@ export function KnowledgeBaseSelector({
const handleSelectSingleKnowledgeBase = (knowledgeBase: KnowledgeBaseData) => {
if (isPreview) return
setSelectedKnowledgeBases([knowledgeBase])
if (!isPreview) {
// Use collaborative update for both local store and persistence
collaborativeSetSubblockValue(blockId, subBlock.id, knowledgeBase.id)
}
// Use the hook's setter which handles collaborative updates
setStoreValue(knowledgeBase.id)
onKnowledgeBaseSelect?.(knowledgeBase.id)
setOpen(false)
@@ -115,16 +125,13 @@ export function KnowledgeBaseSelector({
newSelected = [...selectedKnowledgeBases, knowledgeBase]
}
setSelectedKnowledgeBases(newSelected)
const selectedIds = newSelected.map((kb) => kb.id)
const valueToStore = selectedIds.length === 1 ? selectedIds[0] : selectedIds.join(',')
if (!isPreview) {
const selectedIds = newSelected.map((kb) => kb.id)
const valueToStore = selectedIds.length === 1 ? selectedIds[0] : selectedIds.join(',')
// Use collaborative update for both local store and persistence
collaborativeSetSubblockValue(blockId, subBlock.id, valueToStore)
}
// Use the hook's setter which handles collaborative updates
setStoreValue(valueToStore)
onKnowledgeBaseSelect?.(newSelected.map((kb) => kb.id))
onKnowledgeBaseSelect?.(selectedIds)
}
// Remove selected knowledge base (for multi-select tags)
@@ -132,37 +139,16 @@ export function KnowledgeBaseSelector({
if (isPreview) return
const newSelected = selectedKnowledgeBases.filter((kb) => kb.id !== knowledgeBaseId)
setSelectedKnowledgeBases(newSelected)
const selectedIds = newSelected.map((kb) => kb.id)
const valueToStore = selectedIds.length === 1 ? selectedIds[0] : selectedIds.join(',')
if (!isPreview) {
const selectedIds = newSelected.map((kb) => kb.id)
const valueToStore = selectedIds.length === 1 ? selectedIds[0] : selectedIds.join(',')
// Use collaborative update for both local store and persistence
collaborativeSetSubblockValue(blockId, subBlock.id, valueToStore)
}
// Use the hook's setter which handles collaborative updates
setStoreValue(valueToStore)
onKnowledgeBaseSelect?.(newSelected.map((kb) => kb.id))
onKnowledgeBaseSelect?.(selectedIds)
}
// Sync selected knowledge bases with value prop
useEffect(() => {
if (value && knowledgeBases.length > 0) {
const selectedIds =
typeof value === 'string'
? value.includes(',')
? value
.split(',')
.map((id) => id.trim())
.filter((id) => id.length > 0)
: [value]
: []
const selectedKbs = knowledgeBases.filter((kb) => selectedIds.includes(kb.id))
setSelectedKnowledgeBases(selectedKbs)
} else if (!value) {
setSelectedKnowledgeBases([])
}
}, [value, knowledgeBases])
// Use cached data if available
useEffect(() => {
@@ -177,6 +163,7 @@ export function KnowledgeBaseSelector({
if (
value &&
selectedKnowledgeBases.length === 0 &&
knowledgeBases.length === 0 &&
!loading &&
!initialFetchDone &&
!isPreview
@@ -186,6 +173,7 @@ export function KnowledgeBaseSelector({
}, [
value,
selectedKnowledgeBases.length,
knowledgeBases.length,
loading,
initialFetchDone,
fetchKnowledgeBases,
@@ -5,6 +5,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/comp
import type { SubBlockConfig } from '@/blocks/types'
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { useSubBlockValue } from '../../hooks/use-sub-block-value'
import { type DiscordServerInfo, DiscordServerSelector } from './components/discord-server-selector'
import { type JiraProjectInfo, JiraProjectSelector } from './components/jira-project-selector'
import { type LinearProjectInfo, LinearProjectSelector } from './components/linear-project-selector'
@@ -32,6 +33,9 @@ export function ProjectSelectorInput({
const [selectedProjectId, setSelectedProjectId] = useState<string>('')
const [_projectInfo, setProjectInfo] = useState<JiraProjectInfo | DiscordServerInfo | null>(null)
// Use the proper hook to get the current value and setter
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id)
// Get provider-specific values
const provider = subBlock.provider || 'jira'
const isDiscord = provider === 'discord'
@@ -60,7 +64,7 @@ export function ProjectSelectorInput({
) => {
setSelectedProjectId(projectId)
setProjectInfo(info || null)
collaborativeSetSubblockValue(blockId, subBlock.id, projectId)
setStoreValue(projectId)
// Clear the issue-related fields when a new project is selected
if (provider === 'jira') {
@@ -4,6 +4,7 @@ import { getProviderFromModel } from '@/providers/utils'
import { useGeneralStore } from '@/stores/settings/general/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
// Helper function to dispatch collaborative subblock updates
const dispatchSubblockUpdate = (blockId: string, subBlockId: string, value: any) => {
@@ -167,6 +168,8 @@ export function useSubBlockValue<T = any>(
subBlockId: string,
triggerWorkflowUpdate = false
): readonly [T | null, (value: T) => void] {
const { collaborativeSetSubblockValue } = useCollaborativeWorkflow()
const blockType = useWorkflowStore(
useCallback((state) => state.blocks?.[blockId]?.type, [blockId])
)
@@ -228,25 +231,15 @@ export function useSubBlockValue<T = any>(
storeApiKeyValue(blockId, blockType, modelValue, newValue, storeValue)
}
// Update the subblock store directly
useSubBlockStore.getState().setValue(blockId, subBlockId, valueCopy)
// Dispatch event to trigger socket emission only (not store update)
const event = new CustomEvent('update-subblock-value', {
detail: {
blockId,
subBlockId,
value: valueCopy,
},
})
window.dispatchEvent(event)
// Use collaborative function which handles both local store update and socket emission
collaborativeSetSubblockValue(blockId, subBlockId, valueCopy)
if (triggerWorkflowUpdate) {
useWorkflowStore.getState().triggerUpdate()
}
}
},
[blockId, subBlockId, blockType, isApiKey, storeValue, triggerWorkflowUpdate, modelValue]
[blockId, subBlockId, blockType, isApiKey, storeValue, triggerWorkflowUpdate, modelValue, collaborativeSetSubblockValue]
)
// Initialize valueRef on first render