Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useState } from 'react'
import { Check, ChevronDown, Hash, Lock, RefreshCw } from 'lucide-react'
import { SlackIcon } from '@/components/icons'
import { Button } from '@/components/ui/button'
Expand All @@ -11,6 +11,7 @@ import {
CommandList,
} from '@/components/ui/command'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { useDisplayNamesStore } from '@/stores/display-names/store'

export interface SlackChannelInfo {
id: string
Expand Down Expand Up @@ -41,9 +42,19 @@ export function SlackChannelSelector({
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [open, setOpen] = useState(false)
const [selectedChannel, setSelectedChannel] = useState<SlackChannelInfo | null>(null)
const [initialFetchDone, setInitialFetchDone] = useState(false)

// Get cached display name
const cachedChannelName = useDisplayNamesStore(
useCallback(
(state) => {
if (!credential || !value) return null
return state.cache.channels[credential]?.[value] || null
},
[credential, value]
)
)

// Fetch channels from Slack API
const fetchChannels = useCallback(async () => {
if (!credential) return
Expand Down Expand Up @@ -76,6 +87,18 @@ export function SlackChannelSelector({
} else {
setChannels(data.channels)
setInitialFetchDone(true)

// Cache channel names in display names store
if (credential) {
const channelMap = data.channels.reduce(
(acc: Record<string, string>, ch: SlackChannelInfo) => {
acc[ch.id] = `#${ch.name}`
return acc
},
{}
)
useDisplayNamesStore.getState().setDisplayNames('channels', credential, channelMap)
}
}
} catch (err) {
if ((err as Error).name === 'AbortError') return
Expand All @@ -97,27 +120,7 @@ export function SlackChannelSelector({
}
}

// Sync selected channel with value prop
useEffect(() => {
if (value && channels.length > 0) {
const channelInfo = channels.find((c) => c.id === value)
setSelectedChannel(channelInfo || null)
} else if (!value) {
setSelectedChannel(null)
}
}, [value, channels])

// If we have a value but no channel info and haven't fetched yet, get just that channel
useEffect(() => {
if (value && !selectedChannel && !loading && !initialFetchDone && credential) {
// For now, we'll fetch all channels when needed
// In the future, we could optimize to fetch just the selected channel
fetchChannels()
}
}, [value, selectedChannel, loading, initialFetchDone, credential, fetchChannels])

const handleSelectChannel = (channel: SlackChannelInfo) => {
setSelectedChannel(channel)
onChange(channel.id, channel)
setOpen(false)
}
Expand All @@ -143,15 +146,10 @@ export function SlackChannelSelector({
>
<div className='flex max-w-[calc(100%-20px)] items-center gap-2 overflow-hidden'>
<SlackIcon className='h-4 w-4 text-[#611f69]' />
{selectedChannel ? (
<>
{getChannelIcon(selectedChannel)}
<span className='truncate font-normal'>{formatChannelName(selectedChannel)}</span>
</>
) : value ? (
{cachedChannelName ? (
<>
<Hash className='h-1.5 w-1.5' />
<span className='truncate font-normal'>{value}</span>
<span className='truncate font-normal'>{cachedChannelName}</span>
</>
) : (
<span className='truncate text-muted-foreground'>{label}</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/c
import type { SubBlockConfig } from '@/blocks/types'
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
import { getMissingRequiredScopes } from '@/hooks/use-oauth-scope-status'
import { useDisplayNamesStore } from '@/stores/display-names/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'

const logger = createLogger('CredentialSelector')
Expand Down Expand Up @@ -116,6 +117,17 @@ export function CredentialSelector({
setHasForeignMeta(foreignMetaFound)
setCredentials(creds)

// Cache credential names in display names store
if (effectiveProviderId) {
const credentialMap = creds.reduce((acc: Record<string, string>, cred: Credential) => {
acc[cred.id] = cred.name
return acc
}, {})
useDisplayNamesStore
.getState()
.setDisplayNames('credentials', effectiveProviderId, credentialMap)
}

// Do not auto-select or reset. We only show what's persisted.
}
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { Check, ChevronDown, FileText, RefreshCw } from 'lucide-react'
import { Button } from '@/components/ui/button'
import {
Expand All @@ -15,24 +15,8 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-depends-on-gate'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-sub-block-value'
import type { SubBlockConfig } from '@/blocks/types'

interface DocumentData {
id: string
knowledgeBaseId: string
filename: string
fileUrl: string
fileSize: number
mimeType: string
chunkCount: number
tokenCount: number
characterCount: number
processingStatus: string
processingStartedAt: Date | null
processingCompletedAt: Date | null
processingError: string | null
enabled: boolean
uploadedAt: Date
}
import { useDisplayNamesStore } from '@/stores/display-names/store'
import { type DocumentData, useKnowledgeStore } from '@/stores/knowledge/store'

interface DocumentSelectorProps {
blockId: string
Expand All @@ -51,110 +35,107 @@ export function DocumentSelector({
isPreview = false,
previewValue,
}: DocumentSelectorProps) {
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 [loading, setLoading] = useState(false)

// 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] = useSubBlockValue(blockId, 'knowledgeBaseId')
const normalizedKnowledgeBaseId =
typeof knowledgeBaseId === 'string' && knowledgeBaseId.trim().length > 0
? knowledgeBaseId
: null

const documentsCache = useKnowledgeStore(
useCallback(
(state) =>
normalizedKnowledgeBaseId ? state.documents[normalizedKnowledgeBaseId] : undefined,
[normalizedKnowledgeBaseId]
)
)

const isDocumentsLoading = useKnowledgeStore(
useCallback(
(state) =>
normalizedKnowledgeBaseId ? state.isDocumentsLoading(normalizedKnowledgeBaseId) : false,
[normalizedKnowledgeBaseId]
)
)

const getDocuments = useKnowledgeStore((state) => state.getDocuments)

// Use preview value when in preview mode, otherwise use store value
const value = isPreview ? previewValue : storeValue

const { finalDisabled } = useDependsOnGate(blockId, subBlock, { disabled, isPreview })
const isDisabled = finalDisabled

// Fetch documents for the selected knowledge base
const fetchDocuments = useCallback(async () => {
if (!knowledgeBaseId) {
setDocuments([])
const documents = useMemo<DocumentData[]>(() => {
if (!documentsCache) return []
return documentsCache.documents ?? []
}, [documentsCache])

const loadDocuments = useCallback(async () => {
if (!normalizedKnowledgeBaseId) {
setError('No knowledge base selected')
return
}

setLoading(true)
setError(null)

try {
const response = await fetch(`/api/knowledge/${knowledgeBaseId}/documents`)

if (!response.ok) {
throw new Error(`Failed to fetch documents: ${response.statusText}`)
}
const fetchedDocuments = await getDocuments(normalizedKnowledgeBaseId)

const result = await response.json()
if (fetchedDocuments.length > 0) {
const documentMap = fetchedDocuments.reduce<Record<string, string>>((acc, doc) => {
acc[doc.id] = doc.filename
return acc
}, {})

if (!result.success) {
throw new Error(result.error || 'Failed to fetch documents')
useDisplayNamesStore
.getState()
.setDisplayNames('documents', normalizedKnowledgeBaseId, documentMap)
}

const fetchedDocuments = result.data.documents || result.data || []
setDocuments(fetchedDocuments)
} catch (err) {
if ((err as Error).name === 'AbortError') return
setError((err as Error).message)
setDocuments([])
} finally {
setLoading(false)
if (err instanceof Error && err.name === 'AbortError') return
setError(err instanceof Error ? err.message : 'Failed to fetch documents')
}
}, [knowledgeBaseId])
}, [normalizedKnowledgeBaseId, getDocuments])

// Handle dropdown open/close - fetch documents when opening
const handleOpenChange = (isOpen: boolean) => {
if (isPreview) return
if (isDisabled) return
if (isPreview || isDisabled) return

setOpen(isOpen)

// Fetch fresh documents when opening the dropdown
if (isOpen) {
fetchDocuments()
if (isOpen && (!documentsCache || !documentsCache.documents.length)) {
void loadDocuments()
}
}

// Handle document selection
const handleSelectDocument = (document: DocumentData) => {
if (isPreview) return

setSelectedDocument(document)
setStoreValue(document.id)
onDocumentSelect?.(document.id)
setOpen(false)
}

// Sync selected document with value prop
useEffect(() => {
if (isDisabled) return
if (value && documents.length > 0) {
const docInfo = documents.find((doc) => doc.id === value)
setSelectedDocument(docInfo || null)
} else {
setSelectedDocument(null)
}
}, [value, documents, isDisabled])

// Reset documents when knowledge base changes
useEffect(() => {
setDocuments([])
setSelectedDocument(null)
setError(null)
}, [knowledgeBaseId])
}, [normalizedKnowledgeBaseId])

// Fetch documents when knowledge base is available
useEffect(() => {
if (knowledgeBaseId && !isPreview && !isDisabled) {
fetchDocuments()
}
}, [knowledgeBaseId, isPreview, isDisabled, fetchDocuments])
if (!normalizedKnowledgeBaseId || documents.length === 0) return

const formatDocumentName = (document: DocumentData) => {
return document.filename
}
const documentMap = documents.reduce<Record<string, string>>((acc, doc) => {
acc[doc.id] = doc.filename
return acc
}, {})

useDisplayNamesStore
.getState()
.setDisplayNames('documents', normalizedKnowledgeBaseId as string, documentMap)
}, [documents, normalizedKnowledgeBaseId])

const formatDocumentName = (document: DocumentData) => document.filename

const getDocumentDescription = (document: DocumentData) => {
const statusMap: Record<string, string> = {
Expand All @@ -171,6 +152,18 @@ export function DocumentSelector({
}

const label = subBlock.placeholder || 'Select document'
const isLoading = isDocumentsLoading && !error

// Always use cached display name
const displayName = useDisplayNamesStore(
useCallback(
(state) => {
if (!normalizedKnowledgeBaseId || !value || typeof value !== 'string') return null
return state.cache.documents[normalizedKnowledgeBaseId]?.[value] || null
},
[normalizedKnowledgeBaseId, value]
)
)

return (
<div className='w-full'>
Expand All @@ -185,8 +178,8 @@ export function DocumentSelector({
>
<div className='flex max-w-[calc(100%-20px)] items-center gap-2 overflow-hidden'>
<FileText className='h-4 w-4 text-muted-foreground' />
{selectedDocument ? (
<span className='truncate font-normal'>{formatDocumentName(selectedDocument)}</span>
{displayName ? (
<span className='truncate font-normal'>{displayName}</span>
) : (
<span className='truncate text-muted-foreground'>{label}</span>
)}
Expand All @@ -199,7 +192,7 @@ export function DocumentSelector({
<CommandInput placeholder='Search documents...' />
<CommandList>
<CommandEmpty>
{loading ? (
{isLoading ? (
<div className='flex items-center justify-center p-4'>
<RefreshCw className='h-4 w-4 animate-spin' />
<span className='ml-2'>Loading documents...</span>
Expand All @@ -208,7 +201,7 @@ export function DocumentSelector({
<div className='p-4 text-center'>
<p className='text-destructive text-sm'>{error}</p>
</div>
) : !knowledgeBaseId ? (
) : !normalizedKnowledgeBaseId ? (
<div className='p-4 text-center'>
<p className='font-medium text-sm'>No knowledge base selected</p>
<p className='text-muted-foreground text-xs'>
Expand Down
Loading