11'use client'
22
33import { useCallback , useEffect , useState } from 'react'
4- import {
5- ChevronLeft ,
6- ChevronRight ,
7- Circle ,
8- CircleOff ,
9- FileText ,
10- Plus ,
11- Search ,
12- Trash2 ,
13- X ,
14- } from 'lucide-react'
15- import { useParams } from 'next/navigation'
4+ import { ChevronLeft , ChevronRight , Circle , CircleOff , FileText , Plus , Trash2 } from 'lucide-react'
5+ import { useParams , useRouter , useSearchParams } from 'next/navigation'
166import { Button } from '@/components/ui/button'
177import { Checkbox } from '@/components/ui/checkbox'
8+ import { SearchHighlight } from '@/components/ui/search-highlight'
189import { Tooltip , TooltipContent , TooltipTrigger } from '@/components/ui/tooltip'
1910import { createLogger } from '@/lib/logs/console-logger'
2011import { ActionBar } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/action-bar/action-bar'
12+ import { SearchInput } from '@/app/workspace/[workspaceId]/knowledge/components/search-input/search-input'
2113import { useDocumentChunks } from '@/hooks/use-knowledge'
2214import { type ChunkData , type DocumentData , useKnowledgeStore } from '@/stores/knowledge/store'
2315import { useSidebarStore } from '@/stores/sidebar/store'
@@ -56,78 +48,74 @@ export function Document({
5648 const { mode, isExpanded } = useSidebarStore ( )
5749 const { getCachedKnowledgeBase, getCachedDocuments } = useKnowledgeStore ( )
5850 const { workspaceId } = useParams ( )
51+ const router = useRouter ( )
52+ const searchParams = useSearchParams ( )
5953
6054 const isSidebarCollapsed =
6155 mode === 'expanded' ? ! isExpanded : mode === 'collapsed' || mode === 'hover'
6256
63- const [ searchQuery , setSearchQuery ] = useState ( '' )
64- const [ selectedChunks , setSelectedChunks ] = useState < Set < string > > ( new Set ( ) )
65- const [ selectedChunk , setSelectedChunk ] = useState < ChunkData | null > ( null )
66- const [ isModalOpen , setIsModalOpen ] = useState ( false )
67- const [ isCreateChunkModalOpen , setIsCreateChunkModalOpen ] = useState ( false )
68- const [ chunkToDelete , setChunkToDelete ] = useState < ChunkData | null > ( null )
69- const [ isDeleteModalOpen , setIsDeleteModalOpen ] = useState ( false )
70- const [ isBulkOperating , setIsBulkOperating ] = useState ( false )
57+ const currentPageFromURL = Number . parseInt ( searchParams . get ( 'page' ) || '1' , 10 )
7158
72- const [ document , setDocument ] = useState < DocumentData | null > ( null )
73- const [ isLoadingDocument , setIsLoadingDocument ] = useState ( true )
74- const [ error , setError ] = useState < string | null > ( null )
75-
76- // Use the updated chunks hook with pagination
7759 const {
78- chunks,
79- isLoading : isLoadingChunks ,
80- error : chunksError ,
60+ chunks : paginatedChunks ,
61+ allChunks,
62+ filteredChunks,
63+ searchQuery,
64+ setSearchQuery,
8165 currentPage,
8266 totalPages,
8367 hasNextPage,
8468 hasPrevPage,
8569 goToPage,
8670 nextPage,
8771 prevPage,
72+ isLoading : isLoadingAllChunks ,
73+ error : chunksError ,
8874 refreshChunks,
8975 updateChunk,
90- } = useDocumentChunks ( knowledgeBaseId , documentId )
76+ } = useDocumentChunks ( knowledgeBaseId , documentId , currentPageFromURL , '' , {
77+ enableClientSearch : true ,
78+ } )
9179
92- // Combine errors
93- const combinedError = error || chunksError
80+ const [ selectedChunks , setSelectedChunks ] = useState < Set < string > > ( new Set ( ) )
81+ const [ selectedChunk , setSelectedChunk ] = useState < ChunkData | null > ( null )
82+ const [ isModalOpen , setIsModalOpen ] = useState ( false )
83+ const [ isCreateChunkModalOpen , setIsCreateChunkModalOpen ] = useState ( false )
84+ const [ chunkToDelete , setChunkToDelete ] = useState < ChunkData | null > ( null )
85+ const [ isDeleteModalOpen , setIsDeleteModalOpen ] = useState ( false )
86+ const [ isBulkOperating , setIsBulkOperating ] = useState ( false )
9487
95- // Handle pagination navigation
96- const handlePrevPage = useCallback ( ( ) => {
97- if ( hasPrevPage && ! isLoadingChunks ) {
98- prevPage ( ) ?. catch ( ( err ) => {
99- logger . error ( 'Previous page failed:' , err )
100- } )
101- }
102- } , [ hasPrevPage , isLoadingChunks , prevPage ] )
88+ const [ document , setDocument ] = useState < DocumentData | null > ( null )
89+ const [ isLoadingDocument , setIsLoadingDocument ] = useState ( true )
90+ const [ error , setError ] = useState < string | null > ( null )
10391
104- const handleNextPage = useCallback ( ( ) => {
105- if ( hasNextPage && ! isLoadingChunks ) {
106- nextPage ( ) ?. catch ( ( err ) => {
107- logger . error ( 'Next page failed:' , err )
108- } )
109- }
110- } , [ hasNextPage , isLoadingChunks , nextPage ] )
111-
112- const handleGoToPage = useCallback (
113- ( page : number ) => {
114- if ( page !== currentPage && ! isLoadingChunks ) {
115- goToPage ( page ) ?. catch ( ( err ) => {
116- logger . error ( 'Go to page failed:' , err )
117- } )
92+ const combinedError = error || chunksError
93+
94+ // URL synchronization for pagination
95+ const updatePageInURL = useCallback (
96+ ( newPage : number ) => {
97+ const params = new URLSearchParams ( searchParams )
98+ if ( newPage > 1 ) {
99+ params . set ( 'page' , newPage . toString ( ) )
100+ } else {
101+ params . delete ( 'page' )
118102 }
103+ router . replace ( `?${ params . toString ( ) } ` , { scroll : false } )
119104 } ,
120- [ currentPage , isLoadingChunks , goToPage ]
105+ [ router , searchParams ]
121106 )
122107
123- // Try to get document from store cache first, then fetch if needed
108+ // Sync URL when page changes
109+ useEffect ( ( ) => {
110+ updatePageInURL ( currentPage )
111+ } , [ currentPage , updatePageInURL ] )
112+
124113 useEffect ( ( ) => {
125114 const fetchDocument = async ( ) => {
126115 try {
127116 setIsLoadingDocument ( true )
128117 setError ( null )
129118
130- // First try to get from cached documents in the store
131119 const cachedDocuments = getCachedDocuments ( knowledgeBaseId )
132120 const cachedDoc = cachedDocuments ?. find ( ( d ) => d . id === documentId )
133121
@@ -137,7 +125,6 @@ export function Document({
137125 return
138126 }
139127
140- // If not in cache, fetch from API
141128 const response = await fetch ( `/api/knowledge/${ knowledgeBaseId } /documents/${ documentId } ` )
142129
143130 if ( ! response . ok ) {
@@ -191,7 +178,7 @@ export function Document({
191178 }
192179
193180 const handleToggleEnabled = async ( chunkId : string ) => {
194- const chunk = chunks . find ( ( c ) => c . id === chunkId )
181+ const chunk = allChunks . find ( ( c ) => c . id === chunkId )
195182 if ( ! chunk ) return
196183
197184 try {
@@ -223,7 +210,7 @@ export function Document({
223210 }
224211
225212 const handleDeleteChunk = ( chunkId : string ) => {
226- const chunk = chunks . find ( ( c ) => c . id === chunkId )
213+ const chunk = allChunks . find ( ( c ) => c . id === chunkId )
227214 if ( chunk ) {
228215 setChunkToDelete ( chunk )
229216 setIsDeleteModalOpen ( true )
@@ -260,7 +247,7 @@ export function Document({
260247
261248 const handleSelectAll = ( checked : boolean ) => {
262249 if ( checked ) {
263- setSelectedChunks ( new Set ( chunks . map ( ( chunk ) => chunk . id ) ) )
250+ setSelectedChunks ( new Set ( paginatedChunks . map ( ( chunk ) => chunk . id ) ) )
264251 } else {
265252 setSelectedChunks ( new Set ( ) )
266253 }
@@ -329,28 +316,32 @@ export function Document({
329316 }
330317
331318 const handleBulkEnable = async ( ) => {
332- const chunksToEnable = chunks . filter ( ( chunk ) => selectedChunks . has ( chunk . id ) && ! chunk . enabled )
319+ const chunksToEnable = allChunks . filter (
320+ ( chunk ) => selectedChunks . has ( chunk . id ) && ! chunk . enabled
321+ )
333322 await performBulkChunkOperation ( 'enable' , chunksToEnable )
334323 }
335324
336325 const handleBulkDisable = async ( ) => {
337- const chunksToDisable = chunks . filter ( ( chunk ) => selectedChunks . has ( chunk . id ) && chunk . enabled )
326+ const chunksToDisable = allChunks . filter (
327+ ( chunk ) => selectedChunks . has ( chunk . id ) && chunk . enabled
328+ )
338329 await performBulkChunkOperation ( 'disable' , chunksToDisable )
339330 }
340331
341332 const handleBulkDelete = async ( ) => {
342- const chunksToDelete = chunks . filter ( ( chunk ) => selectedChunks . has ( chunk . id ) )
333+ const chunksToDelete = allChunks . filter ( ( chunk ) => selectedChunks . has ( chunk . id ) )
343334 await performBulkChunkOperation ( 'delete' , chunksToDelete )
344335 }
345336
346337 // Calculate bulk operation counts
347- const selectedChunksList = chunks . filter ( ( chunk ) => selectedChunks . has ( chunk . id ) )
338+ const selectedChunksList = allChunks . filter ( ( chunk ) => selectedChunks . has ( chunk . id ) )
348339 const enabledCount = selectedChunksList . filter ( ( chunk ) => chunk . enabled ) . length
349340 const disabledCount = selectedChunksList . filter ( ( chunk ) => ! chunk . enabled ) . length
350341
351- const isAllSelected = chunks . length > 0 && selectedChunks . size === chunks . length
342+ const isAllSelected = paginatedChunks . length > 0 && selectedChunks . size === paginatedChunks . length
352343
353- if ( isLoadingDocument || isLoadingChunks ) {
344+ if ( isLoadingDocument || isLoadingAllChunks ) {
354345 return (
355346 < DocumentLoading
356347 knowledgeBaseId = { knowledgeBaseId }
@@ -360,7 +351,7 @@ export function Document({
360351 )
361352 }
362353
363- if ( combinedError && ! isLoadingChunks ) {
354+ if ( combinedError && ! isLoadingAllChunks ) {
364355 const errorBreadcrumbs = [
365356 { label : 'Knowledge' , href : `/workspace/${ workspaceId } /knowledge` } ,
366357 {
@@ -404,31 +395,16 @@ export function Document({
404395 < div className = 'px-6 pb-6' >
405396 { /* Search Section */ }
406397 < div className = 'mb-4 flex items-center justify-between pt-1' >
407- < div className = 'relative max-w-md' >
408- < div className = 'relative flex items-center' >
409- < Search className = '-translate-y-1/2 pointer-events-none absolute top-1/2 left-3 h-[18px] w-[18px] transform text-muted-foreground' />
410- < input
411- type = 'text'
412- value = { searchQuery }
413- onChange = { ( e ) => setSearchQuery ( e . target . value ) }
414- placeholder = {
415- document ?. processingStatus === 'completed'
416- ? 'Search chunks...'
417- : 'Document processing...'
418- }
419- disabled = { document ?. processingStatus !== 'completed' }
420- className = 'h-10 w-full rounded-md border bg-background px-9 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:font-medium file:text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50'
421- />
422- { searchQuery && document ?. processingStatus === 'completed' && (
423- < button
424- onClick = { ( ) => setSearchQuery ( '' ) }
425- className = '-translate-y-1/2 absolute top-1/2 right-3 transform text-muted-foreground hover:text-foreground'
426- >
427- < X className = 'h-[18px] w-[18px]' />
428- </ button >
429- ) }
430- </ div >
431- </ div >
398+ < SearchInput
399+ value = { searchQuery }
400+ onChange = { setSearchQuery }
401+ placeholder = {
402+ document ?. processingStatus === 'completed'
403+ ? 'Search chunks...'
404+ : 'Document processing...'
405+ }
406+ disabled = { document ?. processingStatus !== 'completed' }
407+ />
432408
433409 < Button
434410 onClick = { ( ) => setIsCreateChunkModalOpen ( true ) }
@@ -442,7 +418,7 @@ export function Document({
442418 </ div >
443419
444420 { /* Error State for chunks */ }
445- { combinedError && ! isLoadingChunks && (
421+ { combinedError && ! isLoadingAllChunks && (
446422 < div className = 'mb-4 rounded-md border border-red-200 bg-red-50 p-4' >
447423 < p className = 'text-red-800 text-sm' > Error loading chunks: { combinedError } </ p >
448424 </ div >
@@ -540,7 +516,7 @@ export function Document({
540516 < div className = 'text-muted-foreground text-xs' > —</ div >
541517 </ td >
542518 </ tr >
543- ) : chunks . length === 0 && ! isLoadingChunks ? (
519+ ) : paginatedChunks . length === 0 && ! isLoadingAllChunks ? (
544520 < tr className = 'border-b transition-colors hover:bg-accent/30' >
545521 < td className = 'px-4 py-3' >
546522 < div className = 'h-3.5 w-3.5' />
@@ -553,7 +529,9 @@ export function Document({
553529 < FileText className = 'h-5 w-5 text-muted-foreground' />
554530 < span className = 'text-muted-foreground text-sm italic' >
555531 { document ?. processingStatus === 'completed'
556- ? 'No chunks found'
532+ ? searchQuery . trim ( )
533+ ? 'No chunks match your search'
534+ : 'No chunks found'
557535 : 'Document is still processing...' }
558536 </ span >
559537 </ div >
@@ -568,7 +546,7 @@ export function Document({
568546 < div className = 'text-muted-foreground text-xs' > —</ div >
569547 </ td >
570548 </ tr >
571- ) : isLoadingChunks ? (
549+ ) : isLoadingAllChunks ? (
572550 // Show loading skeleton rows when chunks are loading
573551 Array . from ( { length : 5 } ) . map ( ( _ , index ) => (
574552 < tr key = { `loading-${ index } ` } className = 'border-b transition-colors' >
@@ -593,7 +571,7 @@ export function Document({
593571 </ tr >
594572 ) )
595573 ) : (
596- chunks . map ( ( chunk ) => (
574+ paginatedChunks . map ( ( chunk ) => (
597575 < tr
598576 key = { chunk . id }
599577 className = 'cursor-pointer border-b transition-colors hover:bg-accent/30'
@@ -620,7 +598,10 @@ export function Document({
620598 { /* Content column */ }
621599 < td className = 'px-4 py-3' >
622600 < div className = 'text-sm' title = { chunk . content } >
623- { truncateContent ( chunk . content ) }
601+ < SearchHighlight
602+ text = { truncateContent ( chunk . content ) }
603+ searchQuery = { searchQuery }
604+ />
624605 </ div >
625606 </ td >
626607
@@ -700,8 +681,8 @@ export function Document({
700681 < Button
701682 variant = 'ghost'
702683 size = 'sm'
703- onClick = { handlePrevPage }
704- disabled = { ! hasPrevPage || isLoadingChunks }
684+ onClick = { prevPage }
685+ disabled = { ! hasPrevPage || isLoadingAllChunks }
705686 className = 'h-8 w-8 p-0'
706687 >
707688 < ChevronLeft className = 'h-4 w-4' />
@@ -726,8 +707,8 @@ export function Document({
726707 return (
727708 < button
728709 key = { page }
729- onClick = { ( ) => handleGoToPage ( page ) }
730- disabled = { isLoadingChunks }
710+ onClick = { ( ) => goToPage ( page ) }
711+ disabled = { isLoadingAllChunks }
731712 className = { `font-medium text-sm transition-colors hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50 ${
732713 page === currentPage ? 'text-foreground' : 'text-muted-foreground'
733714 } `}
@@ -741,8 +722,8 @@ export function Document({
741722 < Button
742723 variant = 'ghost'
743724 size = 'sm'
744- onClick = { handleNextPage }
745- disabled = { ! hasNextPage || isLoadingChunks }
725+ onClick = { nextPage }
726+ disabled = { ! hasNextPage || isLoadingAllChunks }
746727 className = 'h-8 w-8 p-0'
747728 >
748729 < ChevronRight className = 'h-4 w-4' />
@@ -767,7 +748,7 @@ export function Document({
767748 updateChunk ( updatedChunk . id , updatedChunk )
768749 setSelectedChunk ( updatedChunk )
769750 } }
770- allChunks = { chunks }
751+ allChunks = { allChunks }
771752 currentPage = { currentPage }
772753 totalPages = { totalPages }
773754 onNavigateToChunk = { ( chunk : ChunkData ) => {
@@ -777,11 +758,11 @@ export function Document({
777758 await goToPage ( page )
778759
779760 const checkAndSelectChunk = ( ) => {
780- if ( ! isLoadingChunks && chunks . length > 0 ) {
761+ if ( ! isLoadingAllChunks && paginatedChunks . length > 0 ) {
781762 if ( selectChunk === 'first' ) {
782- setSelectedChunk ( chunks [ 0 ] )
763+ setSelectedChunk ( paginatedChunks [ 0 ] )
783764 } else {
784- setSelectedChunk ( chunks [ chunks . length - 1 ] )
765+ setSelectedChunk ( paginatedChunks [ paginatedChunks . length - 1 ] )
785766 }
786767 } else {
787768 // Retry after a short delay if chunks aren't loaded yet
0 commit comments