@@ -11,7 +11,7 @@ type FileEntry = {
1111 filename : string ;
1212 hash ?: string ;
1313 docId ?: number ;
14- status : "ready" | "queued" | "extracting" | "deleting" | "error" ;
14+ status : "ready" | "queued" | "extracting" | "indexing" | " deleting" | "error" ;
1515 detail ?: string ;
1616} ;
1717
@@ -26,40 +26,83 @@ async function fileHash(file: File): Promise<string> {
2626type Props = {
2727 activeDocId : number | null ;
2828 onSelectDoc : ( doc : DocumentInfo ) => void ;
29+ onReadyChange : ( hasReady : boolean ) => void ;
2930} ;
3031
3132let entrySeq = 0 ;
3233
33- export function FileSidebar ( { activeDocId, onSelectDoc } : Props ) {
34+ export function FileSidebar ( {
35+ activeDocId,
36+ onSelectDoc,
37+ onReadyChange,
38+ } : Props ) {
3439 const [ entries , setEntries ] = useState < FileEntry [ ] > ( [ ] ) ;
3540 const [ documents , setDocuments ] = useState < DocumentInfo [ ] > ( [ ] ) ;
41+ const [ vectorCount , setVectorCount ] = useState ( 0 ) ;
42+ const [ chunkCount , setChunkCount ] = useState ( 0 ) ;
3643 const fileRef = useRef < HTMLInputElement > ( null ) ;
3744
3845 const refreshDocs = useCallback ( async ( ) => {
39- const docs = await listDocuments ( ) ;
40- setDocuments ( docs ) ;
46+ const resp = await listDocuments ( ) ;
47+ setDocuments ( resp . documents ) ;
48+ setVectorCount ( resp . vectorCount ) ;
49+ setChunkCount ( resp . chunkCount ) ;
4150 setEntries ( ( prev ) => {
4251 const uploading = prev . filter (
43- ( e ) => e . status === "queued" && ! docs . some ( ( d ) => d . id === e . docId ) ,
52+ ( e ) =>
53+ e . status === "queued" &&
54+ ! resp . documents . some ( ( d ) => d . id === e . docId ) ,
4455 ) ;
45- const fromApi : FileEntry [ ] = docs . map ( ( d ) => ( {
46- key : `doc-${ d . id } ` ,
47- filename : d . filename ,
48- docId : d . id ,
49- status : d . status === "ready" ? "ready" : "extracting" ,
50- } ) ) ;
51- return [ ...uploading , ...fromApi ] ;
56+ const deleting = prev . filter ( ( e ) => e . status === "deleting" ) ;
57+ const deletingIds = new Set ( deleting . map ( ( e ) => e . docId ) ) ;
58+ const fromApi : FileEntry [ ] = resp . documents
59+ . filter ( ( d ) => ! deletingIds . has ( d . id ) )
60+ . map ( ( d ) => {
61+ let status : FileEntry [ "status" ] ;
62+ let detail : string | undefined ;
63+ if ( d . status === "error" ) {
64+ status = "error" ;
65+ detail = "Processing failed" ;
66+ } else if (
67+ d . status === "ready" &&
68+ resp . chunkCount > 0 &&
69+ resp . vectorCount < resp . chunkCount
70+ ) {
71+ status = "indexing" ;
72+ } else if ( d . status === "ready" ) {
73+ status = "ready" ;
74+ } else {
75+ status = "extracting" ;
76+ }
77+ return {
78+ key : `doc-${ d . id } ` ,
79+ filename : d . filename ,
80+ docId : d . id ,
81+ status,
82+ detail,
83+ } ;
84+ } ) ;
85+ return [ ...uploading , ...deleting , ...fromApi ] ;
5286 } ) ;
5387 } , [ ] ) ;
5488
5589 useEffect ( ( ) => {
5690 refreshDocs ( ) ;
5791 } , [ refreshDocs ] ) ;
5892
59- // Auto-poll while any documents are still processing
93+ // Notify parent when documents are ready AND vectors are fully indexed
94+ const indexed = chunkCount > 0 && vectorCount >= chunkCount ;
6095 useEffect ( ( ) => {
61- const hasProcessing = entries . some ( ( e ) => e . status === "extracting" ) ;
62- if ( ! hasProcessing ) return ;
96+ const hasReady = entries . some ( ( e ) => e . status === "ready" ) ;
97+ onReadyChange ( hasReady && indexed ) ;
98+ } , [ entries , indexed , onReadyChange ] ) ;
99+
100+ // Auto-poll while documents are processing or vectors are not yet indexed
101+ useEffect ( ( ) => {
102+ const needsPoll = entries . some (
103+ ( e ) => e . status === "extracting" || e . status === "indexing" ,
104+ ) ;
105+ if ( ! needsPoll ) return ;
63106 const interval = setInterval ( refreshDocs , 3000 ) ;
64107 return ( ) => clearInterval ( interval ) ;
65108 } , [ entries , refreshDocs ] ) ;
@@ -134,7 +177,11 @@ export function FileSidebar({ activeDocId, onSelectDoc }: Props) {
134177 }
135178
136179 function handleClick ( entry : FileEntry ) {
137- if ( entry . status !== "ready" || ! entry . docId ) return ;
180+ if (
181+ ( entry . status !== "ready" && entry . status !== "indexing" ) ||
182+ ! entry . docId
183+ )
184+ return ;
138185 const doc = documents . find ( ( d ) => d . id === entry . docId ) ;
139186 if ( doc ) onSelectDoc ( doc ) ;
140187 }
@@ -156,6 +203,7 @@ export function FileSidebar({ activeDocId, onSelectDoc }: Props) {
156203 const STATUS_LABEL : Record < string , string > = {
157204 queued : "Queued" ,
158205 extracting : "Processing..." ,
206+ indexing : "Indexing..." ,
159207 deleting : "Removing..." ,
160208 error : "Error" ,
161209 } ;
@@ -187,20 +235,20 @@ export function FileSidebar({ activeDocId, onSelectDoc }: Props) {
187235 { entries . map ( ( entry ) => (
188236 < div
189237 key = { entry . key }
190- className = { `file-item ${ entry . docId === activeDocId && entry . status === "ready" ? "active" : "" } ${ entry . status !== "ready" ? "file-item--processing" : "" } ` }
238+ className = { `file-item ${ entry . docId === activeDocId && ( entry . status === "ready" || entry . status === "indexing" ) ? "active" : "" } ${ entry . status === "error" ? "file-item--error" : entry . status === "indexing" ? "file-item--indexing" : entry . status !== "ready" ? "file-item--processing" : "" } ` }
191239 >
192240 < button
193241 type = "button"
194242 className = "file-item-btn"
195243 onClick = { ( ) => handleClick ( entry ) }
196- disabled = { entry . status !== "ready" }
244+ disabled = { entry . status !== "ready" && entry . status !== "indexing" }
197245 >
198246 < span
199247 className = { `file-item-dot file-item-dot--${ entry . status } ` }
200248 />
201249 < span className = "file-item-label" > { entry . filename } </ span >
202250 </ button >
203- { entry . status === "ready " && entry . docId && (
251+ { entry . status !== "deleting " && entry . docId && (
204252 < button
205253 type = "button"
206254 className = "file-item-delete"
0 commit comments