11import { FileTextIcon , UploadIcon } from "lucide-react" ;
2- import { useRef , useState } from "react" ;
2+ import { useCallback , useRef , useState } from "react" ;
3+ import { toast } from "sonner" ;
34import {
45 ImportDialogFooter ,
56 ImportDialogShell ,
@@ -10,13 +11,15 @@ import {
1011} from "@/components/features/import/import-dialog-shared" ;
1112import { Button } from "@/components/ui/button" ;
1213import { useImportDialog } from "@/hooks/use-import-dialog" ;
14+ import { useMemories } from "@/hooks/use-memories" ;
1315import {
1416 convertToImportItems ,
1517 type DocumentImportItem ,
1618 type DocumentParserStatus ,
1719 parseDocument ,
1820} from "@/lib/document/document-parser" ;
1921import { createLogger } from "@/lib/logger" ;
22+ import { findDuplicates } from "@/lib/storage/memories" ;
2023
2124const logger = createLogger ( "component:document-import-dialog" ) ;
2225
@@ -43,6 +46,7 @@ function getDescription(status: DocumentParserStatus): string {
4346 case "success" :
4447 return "Select the information you want to import." ;
4548 case "reading" :
49+ return "Reading your document..." ;
4650 case "parsing" :
4751 return "AI is extracting your information..." ;
4852 case "error" :
@@ -63,6 +67,11 @@ export function DocumentImportDialog({
6367} : DocumentImportDialogProps ) {
6468 const fileInputRef = useRef < HTMLInputElement > ( null ) ;
6569 const [ fileName , setFileName ] = useState < string | null > ( null ) ;
70+ const lastImportKeyRef = useRef < string | null > ( null ) ;
71+ const lastImportTimeRef = useRef < number > ( 0 ) ;
72+ const abortControllerRef = useRef < AbortController | null > ( null ) ;
73+
74+ const { entries : existingMemories } = useMemories ( ) ;
6675
6776 const {
6877 status,
@@ -91,65 +100,111 @@ export function DocumentImportDialog({
91100
92101 const progress = PROGRESS_BY_STATUS [ status ] ;
93102
94- const handleFileSelect = async (
95- event : React . ChangeEvent < HTMLInputElement > ,
96- ) => {
97- const file = event . target . files ?. [ 0 ] ;
98- if ( ! file ) return ;
99-
100- const isPdf =
101- file . type === "application/pdf" ||
102- file . name . toLowerCase ( ) . endsWith ( ".pdf" ) ;
103- const isTxt =
104- file . type === "text/plain" || file . name . toLowerCase ( ) . endsWith ( ".txt" ) ;
105-
106- if ( ! isPdf && ! isTxt ) {
107- setError ( "Please select a PDF or text file" ) ;
108- setStatus ( "error" ) ;
109- event . target . value = "" ;
110- return ;
111- }
112-
113- setFileName ( file . name ) ;
114- setStatus ( "parsing" ) ;
115- setError ( null ) ;
116- setImportItems ( [ ] ) ;
103+ const handleFileSelect = useCallback (
104+ async ( event : React . ChangeEvent < HTMLInputElement > ) => {
105+ const file = event . target . files ?. [ 0 ] ;
106+ if ( ! file ) return ;
117107
118- const currentRequestId = ++ requestIdRef . current ;
108+ const isPdf =
109+ file . type === "application/pdf" ||
110+ file . name . toLowerCase ( ) . endsWith ( ".pdf" ) ;
111+ const isTxt =
112+ file . type === "text/plain" || file . name . toLowerCase ( ) . endsWith ( ".txt" ) ;
119113
120- try {
121- const result = await parseDocument ( file ) ;
114+ if ( ! isPdf && ! isTxt ) {
115+ setError ( "Please select a PDF or text file" ) ;
116+ setStatus ( "error" ) ;
117+ event . target . value = "" ;
118+ return ;
119+ }
122120
123- if ( requestIdRef . current !== currentRequestId ) return ;
121+ const importKey = `${ file . name } :${ file . size } ` ;
122+ const now = Date . now ( ) ;
124123
125- if ( ! result . success || ! result . items ) {
126- setStatus ( "error" ) ;
127- setError ( result . error || "Failed to extract data from document" ) ;
124+ if (
125+ importKey === lastImportKeyRef . current &&
126+ now - lastImportTimeRef . current < 5_000
127+ ) {
128+ logger . debug ( "Skipping duplicate import for:" , file . name ) ;
129+ event . target . value = "" ;
128130 return ;
129131 }
130132
131- const items = convertToImportItems ( result . items ) ;
132- setImportItems ( items ) ;
133- setStatus ( "success" ) ;
133+ lastImportKeyRef . current = importKey ;
134+ lastImportTimeRef . current = now ;
135+ abortControllerRef . current ?. abort ( ) ;
136+ const controller = new AbortController ( ) ;
137+ abortControllerRef . current = controller ;
134138
135- logger . debug ( "Successfully extracted document data:" , items . length ) ;
136- } catch ( err ) {
137- if ( requestIdRef . current !== currentRequestId ) return ;
139+ setFileName ( file . name ) ;
140+ setStatus ( "reading" ) ;
141+ setError ( null ) ;
142+ setImportItems ( [ ] ) ;
138143
139- logger . error ( "Import error:" , err ) ;
140- setStatus ( "error" ) ;
141- setError (
142- err instanceof Error ? err . message : "An unexpected error occurred" ,
143- ) ;
144- }
144+ const currentRequestId = ++ requestIdRef . current ;
145+ const requestId = String ( currentRequestId ) ;
145146
146- if ( fileInputRef . current ) {
147- fileInputRef . current . value = "" ;
148- }
149- } ;
147+ try {
148+ const result = await parseDocument ( file , {
149+ requestId,
150+ signal : controller . signal ,
151+ onStageChange : ( stage ) => {
152+ if ( requestIdRef . current !== currentRequestId ) return ;
153+ setStatus ( stage ) ;
154+ } ,
155+ } ) ;
156+
157+ if ( requestIdRef . current !== currentRequestId ) return ;
158+
159+ if ( ! result . success || ! result . items ) {
160+ if ( result . error === "cancelled" ) return ;
161+
162+ const errorMsg =
163+ result . error || "Failed to extract data from document" ;
164+ setStatus ( "error" ) ;
165+ setError ( errorMsg ) ;
166+ toast . error ( errorMsg ) ;
167+ return ;
168+ }
169+
170+ const items = convertToImportItems ( result . items ) ;
171+ const duplicatesMap = await findDuplicates ( items , existingMemories ) ;
172+ const enrichedItems = items . map ( ( item , i ) => {
173+ const duplicate = duplicatesMap . get ( i ) ;
174+ return duplicate ? { ...item , existingDuplicate : duplicate } : item ;
175+ } ) ;
176+
177+ setImportItems ( enrichedItems ) ;
178+ setStatus ( "success" ) ;
179+
180+ logger . debug (
181+ `[req:${ requestId } ] Successfully extracted document data:` ,
182+ items . length ,
183+ "items" ,
184+ ) ;
185+ } catch ( err ) {
186+ if ( requestIdRef . current !== currentRequestId ) return ;
187+ if ( err instanceof Error && err . name === "AbortError" ) return ;
188+
189+ const errMsg =
190+ err instanceof Error ? err . message : "An unexpected error occurred" ;
191+ logger . error ( `[req:${ requestId } ] Import error:` , err ) ;
192+ setStatus ( "error" ) ;
193+ setError ( errMsg ) ;
194+ toast . error ( errMsg ) ;
195+ }
196+
197+ if ( fileInputRef . current ) {
198+ fileInputRef . current . value = "" ;
199+ }
200+ } ,
201+ [ requestIdRef , setStatus , setError , setImportItems , existingMemories ] ,
202+ ) ;
150203
151204 const handleCloseWrapper = ( open : boolean ) => {
152205 if ( ! open ) {
206+ abortControllerRef . current ?. abort ( ) ;
207+ abortControllerRef . current = null ;
153208 setFileName ( null ) ;
154209 }
155210 handleClose ( open ) ;
0 commit comments