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
8 changes: 7 additions & 1 deletion backend/src/handlers/fileHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,13 @@ const chatWithFileStream = async (req: AuthenticatedRequest, res: Response): Pro
return;
}
if (fileRecord.extractionStatus !== 'done') {
res.status(409).json({ message: 'Not ready for chat yet.' });
const statusMsg = fileRecord.extractionStatus === 'failed'
? 'Extraction failed for this file. Re-run extraction from the file menu.'
: 'This file is still being processed. It usually takes 30-60 seconds — try again in a moment.';
res.status(409).json({
message: statusMsg,
extraction_status: fileRecord.extractionStatus,
});
return;
}

Expand Down
14 changes: 11 additions & 3 deletions backend/src/services/searchPipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,11 +478,19 @@ export const runSearchPipeline = async (
};
});

results.sort((a, b) => b.score - a.score);
logger.info(`search: ${results.length} results for "${rawQuery}" (top score=${results[0]?.score.toFixed(4)})`);
// Drop nameless results. These come from file_ids that the parent fetch
// couldn't find a matching summary row for (race condition between delete
// and re-extract, or orphaned chunk rows). The UI can't render them and
// they'd crash on .split() / .localeCompare().
const cleaned = results.filter((r) => typeof r.filename === "string" && r.filename.length > 0);
cleaned.sort((a, b) => b.score - a.score);
if (cleaned.length < results.length) {
logger.warn(`search: dropped ${results.length - cleaned.length} orphan result(s) without filename`);
}
logger.info(`search: ${cleaned.length} results for "${rawQuery}" (top score=${cleaned[0]?.score.toFixed(4)})`);

// Cap to a reasonable top-K — UI doesn't need 200 results.
return results.slice(0, 30);
return cleaned.slice(0, 30);
};

// ---------- 7. Optional LLM rerank for the top results ----------
Expand Down
31 changes: 28 additions & 3 deletions smartdrive-frontend/src/components/FileChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -349,9 +349,34 @@ export function FileChat({ fileId, fileName, ready }: Props) {
</div>

{error && (
<div className="text-xs text-red-600/90 flex items-start gap-1.5">
<IconAlertTriangle size={12} className="mt-0.5 shrink-0" /> {error}
</div>
(() => {
const isNotReady = /still being processed|not ready/i.test(error);
const isFailed = /extraction failed/i.test(error);
const isPrivate = /private/i.test(error);
const tone = isNotReady
? "border-amber-300/60 bg-amber-50/70 text-amber-900 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-100"
: isPrivate
? "border-emerald-300/60 bg-emerald-50/70 text-emerald-900 dark:border-emerald-500/30 dark:bg-emerald-500/10 dark:text-emerald-100"
: "border-red-300/60 bg-red-50/70 text-red-900 dark:border-red-500/30 dark:bg-red-500/10 dark:text-red-100";
return (
<div className={`text-xs flex items-start gap-1.5 rounded-md border px-3 py-2 ${tone}`}>
<IconAlertTriangle size={12} className="mt-0.5 shrink-0" />
<div className="min-w-0">
<div>{error}</div>
{isNotReady && (
<div className="mt-1 opacity-80">
The file is being indexed in the background. This usually takes 30-60 seconds.
</div>
)}
{isFailed && (
<div className="mt-1 opacity-80">
Open the file menu and click &ldquo;Re-run extraction&rdquo; to try again.
</div>
)}
</div>
</div>
);
})()
)}

<div className="flex gap-2 items-end">
Expand Down
8 changes: 4 additions & 4 deletions smartdrive-frontend/src/components/FileListWithDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,7 @@ function FileCard({
</div>
</div>
{(() => {
const ext = file.filename.split(".").pop()?.toUpperCase();
const ext = (file.filename ?? "").split(".").pop()?.toUpperCase();
if (!ext || ext.length > 5) return null;
return (
<div className="text-[11px] font-semibold tracking-wider px-2 py-1 rounded-md bg-background/70 text-foreground/80 ring-1 ring-border">
Expand Down Expand Up @@ -697,8 +697,8 @@ function sortFiles(arr: UploadItem[], kind: SortKind): UploadItem[] {
switch (kind) {
case "newest": return copy.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
case "oldest": return copy.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
case "name": return copy.sort((a, b) => a.filename.localeCompare(b.filename));
case "type": return copy.sort((a, b) => a.filetype.localeCompare(b.filetype));
case "name": return copy.sort((a, b) => (a.filename ?? "").localeCompare(b.filename ?? ""));
case "type": return copy.sort((a, b) => (a.filetype ?? "").localeCompare(b.filetype ?? ""));
case "status": {
const order: Record<string, number> = { failed: 0, processing: 1, pending: 2, done: 3 };
return copy.sort((a, b) => (order[a.extraction_status ?? "done"] ?? 4) - (order[b.extraction_status ?? "done"] ?? 4));
Expand Down Expand Up @@ -805,7 +805,7 @@ export function FileListWithDrawer({
const visibleFiles = useMemo(() => {
const filtered = files.filter((f) => matchesFilter(f, filter));
const searched = search.trim()
? filtered.filter((f) => f.filename.toLowerCase().includes(search.toLowerCase().trim()))
? filtered.filter((f) => (f.filename ?? "").toLowerCase().includes(search.toLowerCase().trim()))
: filtered;
return sortFiles(searched, sort);
}, [files, filter, sort, search]);
Expand Down
Loading