Skip to content

Commit 4587cba

Browse files
committed
Merge branch 'main' of https://github.com/mikr13/superfill.ai into feat/cdp-based-form-filling
1 parent 0751e4c commit 4587cba

10 files changed

Lines changed: 539 additions & 102 deletions

File tree

.changeset/fifty-words-lick.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"superfill.ai": patch
3+
---
4+
5+
add observability in the document parser

.changeset/huge-suns-prove.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"superfill.ai": minor
3+
---
4+
5+
Updated doc parser prompt to stop redundant memory creation and added logic for deduplication of memory

.changeset/tender-months-wish.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"superfill.ai": major
3+
---
4+
5+
Updated the deduplication logic to give the preview of the new and existing duplicate memory in the UI

src/components/features/document/document-import-dialog.tsx

Lines changed: 102 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { FileTextIcon, UploadIcon } from "lucide-react";
2-
import { useRef, useState } from "react";
2+
import { useCallback, useRef, useState } from "react";
3+
import { toast } from "sonner";
34
import {
45
ImportDialogFooter,
56
ImportDialogShell,
@@ -10,13 +11,15 @@ import {
1011
} from "@/components/features/import/import-dialog-shared";
1112
import { Button } from "@/components/ui/button";
1213
import { useImportDialog } from "@/hooks/use-import-dialog";
14+
import { useMemories } from "@/hooks/use-memories";
1315
import {
1416
convertToImportItems,
1517
type DocumentImportItem,
1618
type DocumentParserStatus,
1719
parseDocument,
1820
} from "@/lib/document/document-parser";
1921
import { createLogger } from "@/lib/logger";
22+
import { findDuplicates } from "@/lib/storage/memories";
2023

2124
const 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);

src/components/features/import/import-dialog-shared.tsx

Lines changed: 96 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
} from "@/components/ui/dialog";
2525
import { Progress } from "@/components/ui/progress";
2626
import { ScrollArea } from "@/components/ui/scroll-area";
27+
import { cn } from "@/lib/cn";
2728
import type { BaseImportItem } from "@/types/import";
2829
import type { AllowedCategory } from "@/types/memory";
2930

@@ -79,6 +80,7 @@ export function ImportItemsList<T extends BaseImportItem>({
7980
);
8081

8182
const allSelected = items.every((item) => item.selected);
83+
const duplicateCount = items.filter((item) => item.existingDuplicate).length;
8284

8385
return (
8486
<div className="space-y-4">
@@ -88,6 +90,15 @@ export function ImportItemsList<T extends BaseImportItem>({
8890
<span className="text-sm font-medium">
8991
Found {items.length} items
9092
</span>
93+
{duplicateCount > 0 && (
94+
<Badge
95+
variant="outline"
96+
size="sm"
97+
className="border-amber-400 text-amber-600 bg-amber-50 dark:bg-amber-950/30"
98+
>
99+
{duplicateCount} duplicate{duplicateCount > 1 ? "s" : ""}
100+
</Badge>
101+
)}
91102
{headerExtra}
92103
</div>
93104
<Button
@@ -122,7 +133,11 @@ export function ImportItemsList<T extends BaseImportItem>({
122133
{categoryItems?.map((item) => (
123134
<div
124135
key={item.id}
125-
className="flex items-start gap-3 p-2 rounded-lg hover:bg-muted/50 transition-colors w-full"
136+
className={cn(
137+
"flex items-start gap-3 p-2 rounded-lg hover:bg-muted/50 transition-colors w-full",
138+
item.existingDuplicate &&
139+
"border border-amber-200 dark:border-amber-800/50 bg-amber-50/50 dark:bg-amber-950/10",
140+
)}
126141
>
127142
<Checkbox
128143
id={`${itemIdPrefix}-${item.id}`}
@@ -134,12 +149,86 @@ export function ImportItemsList<T extends BaseImportItem>({
134149
htmlFor={`${itemIdPrefix}-${item.id}`}
135150
className="flex-1 min-w-0 cursor-pointer"
136151
>
137-
<p className="text-sm font-medium truncate">
138-
{item.label}
139-
</p>
140-
<p className="text-xs text-muted-foreground line-clamp-2">
141-
{item.answer}
142-
</p>
152+
<div className="flex items-center gap-1.5 flex-wrap">
153+
<p className="text-sm font-medium truncate">
154+
{item.label}
155+
</p>
156+
{item.existingDuplicate && (
157+
<Badge
158+
variant="outline"
159+
size="sm"
160+
className="shrink-0 border-amber-400 text-amber-600 bg-amber-50 dark:bg-amber-950/30"
161+
>
162+
duplicate
163+
</Badge>
164+
)}
165+
</div>
166+
{item.question &&
167+
item.question.toLowerCase() !==
168+
item.label.toLowerCase() && (
169+
<p className="text-xs text-muted-foreground/70 italic truncate">
170+
{item.question}
171+
</p>
172+
)}
173+
<div
174+
className={cn(
175+
"mt-0.5",
176+
item.existingDuplicate && "space-y-1.5",
177+
)}
178+
>
179+
{item.existingDuplicate && (
180+
<p className="text-[11px] font-medium text-muted-foreground/60 uppercase tracking-wide">
181+
New
182+
</p>
183+
)}
184+
<p className="text-xs text-muted-foreground line-clamp-2">
185+
{item.answer}
186+
</p>
187+
{item.existingDuplicate && (
188+
<>
189+
<p className="text-[11px] font-medium text-muted-foreground/60 uppercase tracking-wide mt-1">
190+
Currently saved
191+
</p>
192+
<p className="text-xs text-muted-foreground/70 line-clamp-2 italic">
193+
{item.existingDuplicate.answer}
194+
</p>
195+
<div className="flex gap-1.5 mt-2">
196+
<button
197+
type="button"
198+
onClick={(e) => {
199+
e.preventDefault();
200+
e.stopPropagation();
201+
if (!item.selected) onToggleItem(item.id);
202+
}}
203+
className={cn(
204+
"text-[11px] px-2 py-0.5 rounded border font-medium transition-colors",
205+
item.selected
206+
? "bg-primary text-primary-foreground border-primary"
207+
: "bg-background text-muted-foreground border-border hover:border-primary hover:text-primary",
208+
)}
209+
>
210+
Use new
211+
</button>
212+
<button
213+
type="button"
214+
onClick={(e) => {
215+
e.preventDefault();
216+
e.stopPropagation();
217+
if (item.selected) onToggleItem(item.id);
218+
}}
219+
className={cn(
220+
"text-[11px] px-2 py-0.5 rounded border font-medium transition-colors",
221+
!item.selected
222+
? "bg-primary text-primary-foreground border-primary"
223+
: "bg-background text-muted-foreground border-border hover:border-primary hover:text-primary",
224+
)}
225+
>
226+
Keep existing
227+
</button>
228+
</div>
229+
</>
230+
)}
231+
</div>
143232
</label>
144233
</div>
145234
))}

src/hooks/use-import-dialog.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,18 @@ export function useImportDialog<
108108
description: successDescription,
109109
});
110110

111-
logger.debug("Successfully imported memories:", entries.length);
111+
logger.debug(
112+
`Import success — saved: ${entries.length}, total selected: ${selectedItems.length}`,
113+
);
112114

113115
resetState();
114116
onOpenChange(false);
115117
onSuccess?.();
116118
} catch (err) {
117-
logger.error("Failed to save memories:", err);
119+
logger.error(
120+
`Import error — attempted: ${importItems.filter((i) => i.selected).length} items —`,
121+
err,
122+
);
118123
toast.error(
119124
err instanceof Error ? err.message : "Failed to save memories",
120125
);

0 commit comments

Comments
 (0)