show file processing errors in UI

This commit is contained in:
phact 2025-10-10 12:52:37 -04:00
parent c8c719c7eb
commit 77f558e690
5 changed files with 204 additions and 131 deletions

View file

@ -50,6 +50,7 @@ export interface File {
| "failed" | "failed"
| "hidden" | "hidden"
| "sync"; | "sync";
error?: string;
chunks?: ChunkResult[]; chunks?: ChunkResult[];
} }

View file

@ -4,6 +4,24 @@ import {
useQueryClient, useQueryClient,
} from "@tanstack/react-query"; } from "@tanstack/react-query";
export interface TaskFileEntry {
status?:
| "pending"
| "running"
| "processing"
| "completed"
| "failed"
| "error";
result?: unknown;
error?: string;
retry_count?: number;
created_at?: string;
updated_at?: string;
duration_seconds?: number;
filename?: string;
[key: string]: unknown;
}
export interface Task { export interface Task {
task_id: string; task_id: string;
status: status:
@ -24,7 +42,7 @@ export interface Task {
duration_seconds?: number; duration_seconds?: number;
result?: Record<string, unknown>; result?: Record<string, unknown>;
error?: string; error?: string;
files?: Record<string, Record<string, unknown>>; files?: Record<string, TaskFileEntry>;
} }
export interface TasksResponse { export interface TasksResponse {

View file

@ -26,6 +26,14 @@ import GoogleDriveIcon from "../settings/icons/google-drive-icon";
import OneDriveIcon from "../settings/icons/one-drive-icon"; import OneDriveIcon from "../settings/icons/one-drive-icon";
import SharePointIcon from "../settings/icons/share-point-icon"; import SharePointIcon from "../settings/icons/share-point-icon";
import { KnowledgeSearchInput } from "@/components/knowledge-search-input"; import { KnowledgeSearchInput } from "@/components/knowledge-search-input";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
// Function to get the appropriate icon for a connector type // Function to get the appropriate icon for a connector type
function getSourceIcon(connectorType?: string) { function getSourceIcon(connectorType?: string) {
@ -77,6 +85,7 @@ function SearchPage() {
size: taskFile.size, size: taskFile.size,
connector_type: taskFile.connector_type, connector_type: taskFile.connector_type,
status: taskFile.status, status: taskFile.status,
error: taskFile.error,
}; };
}); });
@ -128,7 +137,6 @@ function SearchPage() {
// Read status directly from data on each render // Read status directly from data on each render
const status = data?.status || "active"; const status = data?.status || "active";
const isActive = status === "active"; const isActive = status === "active";
console.log(data?.filename, status, "a");
return ( return (
<div className="flex items-center overflow-hidden w-full"> <div className="flex items-center overflow-hidden w-full">
<div <div
@ -196,9 +204,37 @@ function SearchPage() {
field: "status", field: "status",
headerName: "Status", headerName: "Status",
cellRenderer: ({ data }: CustomCellRendererProps<File>) => { cellRenderer: ({ data }: CustomCellRendererProps<File>) => {
console.log(data?.filename, data?.status, "b");
// Default to 'active' status if no status is provided
const status = data?.status || "active"; const status = data?.status || "active";
const error =
typeof data?.error === "string" && data.error.trim().length > 0
? data.error.trim()
: undefined;
if (status === "failed" && error) {
return (
<Dialog>
<DialogTrigger asChild>
<button
type="button"
className="inline-flex items-center gap-1 text-red-500 transition hover:text-red-400"
aria-label="View ingestion error"
>
<StatusBadge status={status} className="pointer-events-none" />
</button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Ingestion failed</DialogTitle>
<DialogDescription className="text-sm text-muted-foreground">
{data?.filename || "Unknown file"}
</DialogDescription>
</DialogHeader>
<div className="rounded-md border border-destructive/20 bg-destructive/10 p-4 text-sm text-destructive">
{error}
</div>
</DialogContent>
</Dialog>
);
}
return <StatusBadge status={status} />; return <StatusBadge status={status} />;
}, },
}, },

View file

@ -169,95 +169,87 @@ export function TaskNotificationMenu() {
{activeTasks.length > 0 && ( {activeTasks.length > 0 && (
<div className="p-4 space-y-3"> <div className="p-4 space-y-3">
<h4 className="text-sm font-medium text-muted-foreground">Active Tasks</h4> <h4 className="text-sm font-medium text-muted-foreground">Active Tasks</h4>
{activeTasks.map((task) => ( {activeTasks.map((task) => {
<Card key={task.task_id} className="bg-card/50"> const progress = formatTaskProgress(task)
<CardHeader className="pb-2"> const showCancel =
<div className="flex items-center justify-between"> task.status === 'pending' ||
<CardTitle className="text-sm flex items-center gap-2"> task.status === 'running' ||
{getTaskIcon(task.status)} task.status === 'processing'
Task {task.task_id.substring(0, 8)}...
</CardTitle> return (
</div> <Card key={task.task_id} className="bg-card/50">
<CardDescription className="text-xs"> <CardHeader className="pb-2">
Started {formatRelativeTime(task.created_at)} <div className="flex items-center justify-between">
{formatDuration(task.duration_seconds) && ( <CardTitle className="text-sm flex items-center gap-2">
<span className="ml-2 text-muted-foreground"> {getTaskIcon(task.status)}
{formatDuration(task.duration_seconds)} Task {task.task_id.substring(0, 8)}...
</span> </CardTitle>
)} </div>
</CardDescription> <CardDescription className="text-xs">
</CardHeader> Started {formatRelativeTime(task.created_at)}
{formatTaskProgress(task) && ( {formatDuration(task.duration_seconds) && (
<CardContent className="pt-0"> <span className="ml-2 text-muted-foreground">
<div className="space-y-2"> {formatDuration(task.duration_seconds)}
<div className="text-xs text-muted-foreground"> </span>
Progress: {formatTaskProgress(task)?.basic} )}
</div> </CardDescription>
{formatTaskProgress(task)?.detailed && ( </CardHeader>
<div className="grid grid-cols-2 gap-2 text-xs"> {(progress || showCancel) && (
<div className="flex items-center gap-1"> <CardContent className="pt-0">
<div className="w-2 h-2 bg-green-500 rounded-full"></div> {progress && (
<span className="text-green-600"> <div className="space-y-2">
{formatTaskProgress(task)?.detailed.successful} success <div className="text-xs text-muted-foreground">
</span> Progress: {progress.basic}
</div>
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-red-500 rounded-full"></div>
<span className="text-red-600">
{formatTaskProgress(task)?.detailed.failed} failed
</span>
</div>
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
<span className="text-blue-600">
{formatTaskProgress(task)?.detailed.running} running
</span>
</div>
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-yellow-500 rounded-full"></div>
<span className="text-yellow-600">
{formatTaskProgress(task)?.detailed.pending} pending
</span>
</div> </div>
{progress.detailed && (
<div className="grid grid-cols-2 gap-2 text-xs">
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span className="text-green-600">
{progress.detailed.successful} success
</span>
</div>
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-red-500 rounded-full"></div>
<span className="text-red-600">
{progress.detailed.failed} failed
</span>
</div>
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
<span className="text-blue-600">
{progress.detailed.running} running
</span>
</div>
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-yellow-500 rounded-full"></div>
<span className="text-yellow-600">
{progress.detailed.pending} pending
</span>
</div>
</div>
)}
</div> </div>
)} )}
</div> {showCancel && (
{/* Cancel button in bottom right */} <div className={`flex justify-end ${progress ? 'mt-3' : ''}`}>
{(task.status === 'pending' || task.status === 'running' || task.status === 'processing') && ( <Button
<div className="flex justify-end mt-3"> variant="destructive"
<Button size="sm"
variant="destructive" onClick={() => cancelTask(task.task_id)}
size="sm" className="h-7 px-3 text-xs"
onClick={() => cancelTask(task.task_id)} title="Cancel task"
className="h-7 px-3 text-xs" >
title="Cancel task" <X className="h-3 w-3 mr-1" />
> Cancel
<X className="h-3 w-3 mr-1" /> </Button>
Cancel </div>
</Button> )}
</div> </CardContent>
)} )}
</CardContent> </Card>
)} )
{/* Cancel button for tasks without progress */} })}
{!formatTaskProgress(task) && (task.status === 'pending' || task.status === 'running' || task.status === 'processing') && (
<CardContent className="pt-0">
<div className="flex justify-end">
<Button
variant="destructive"
size="sm"
onClick={() => cancelTask(task.task_id)}
className="h-7 px-3 text-xs"
title="Cancel task"
>
<X className="h-3 w-3 mr-1" />
Cancel
</Button>
</div>
</CardContent>
)}
</Card>
))}
</div> </div>
)} )}
@ -282,43 +274,47 @@ export function TaskNotificationMenu() {
{isExpanded && ( {isExpanded && (
<div className="space-y-2 transition-all duration-200"> <div className="space-y-2 transition-all duration-200">
{recentTasks.map((task) => ( {recentTasks.map((task) => {
<div const progress = formatTaskProgress(task)
key={task.task_id}
className="flex items-center gap-3 p-2 rounded-lg hover:bg-muted/50 transition-colors" return (
> <div
{getTaskIcon(task.status)} key={task.task_id}
<div className="flex-1 min-w-0"> className="flex items-center gap-3 p-2 rounded-lg hover:bg-muted/50 transition-colors"
<div className="text-xs font-medium truncate"> >
Task {task.task_id.substring(0, 8)}... {getTaskIcon(task.status)}
</div> <div className="flex-1 min-w-0">
<div className="text-xs text-muted-foreground"> <div className="text-xs font-medium truncate">
{formatRelativeTime(task.updated_at)} Task {task.task_id.substring(0, 8)}...
{formatDuration(task.duration_seconds) && ( </div>
<span className="ml-2"> <div className="text-xs text-muted-foreground">
{formatDuration(task.duration_seconds)} {formatRelativeTime(task.updated_at)}
</span> {formatDuration(task.duration_seconds) && (
)} <span className="ml-2">
</div> {formatDuration(task.duration_seconds)}
{/* Show final results for completed tasks */} </span>
{task.status === 'completed' && formatTaskProgress(task)?.detailed && (
<div className="text-xs text-muted-foreground mt-1">
{formatTaskProgress(task)?.detailed.successful} success, {' '}
{formatTaskProgress(task)?.detailed.failed} failed
{(formatTaskProgress(task)?.detailed.running || 0) > 0 && (
<span>, {formatTaskProgress(task)?.detailed.running} running</span>
)} )}
</div> </div>
)} {/* Show final results for completed tasks */}
{task.status === 'failed' && task.error && ( {task.status === 'completed' && progress?.detailed && (
<div className="text-xs text-red-600 mt-1 truncate"> <div className="text-xs text-muted-foreground mt-1">
{task.error} {progress.detailed.successful} success,{' '}
</div> {progress.detailed.failed} failed
)} {(progress.detailed.running || 0) > 0 && (
<span>, {progress.detailed.running} running</span>
)}
</div>
)}
{task.status === 'failed' && task.error && (
<div className="text-xs text-red-600 mt-1 truncate">
{task.error}
</div>
)}
</div>
{getStatusBadge(task.status)}
</div> </div>
{getStatusBadge(task.status)} )
</div> })}
))}
</div> </div>
)} )}
</div> </div>
@ -338,4 +334,4 @@ export function TaskNotificationMenu() {
</div> </div>
</div> </div>
) )
} }

View file

@ -14,6 +14,7 @@ import { toast } from "sonner";
import { useCancelTaskMutation } from "@/app/api/mutations/useCancelTaskMutation"; import { useCancelTaskMutation } from "@/app/api/mutations/useCancelTaskMutation";
import { import {
type Task, type Task,
type TaskFileEntry,
useGetTasksQuery, useGetTasksQuery,
} from "@/app/api/queries/useGetTasksQuery"; } from "@/app/api/queries/useGetTasksQuery";
import { useAuth } from "@/contexts/auth-context"; import { useAuth } from "@/contexts/auth-context";
@ -31,6 +32,7 @@ export interface TaskFile {
task_id: string; task_id: string;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
error?: string;
} }
interface TaskContextType { interface TaskContextType {
tasks: Task[]; tasks: Task[];
@ -105,6 +107,7 @@ export function TaskProvider({ children }: { children: React.ReactNode }) {
task_id: taskId, task_id: taskId,
created_at: now, created_at: now,
updated_at: now, updated_at: now,
error: file.error,
})); }));
setFiles((prevFiles) => [...prevFiles, ...filesToAdd]); setFiles((prevFiles) => [...prevFiles, ...filesToAdd]);
@ -138,12 +141,13 @@ export function TaskProvider({ children }: { children: React.ReactNode }) {
taskFileEntries.forEach(([filePath, fileInfo]) => { taskFileEntries.forEach(([filePath, fileInfo]) => {
if (typeof fileInfo === "object" && fileInfo) { if (typeof fileInfo === "object" && fileInfo) {
const fileInfoEntry = fileInfo as TaskFileEntry;
// Use the filename from backend if available, otherwise extract from path // Use the filename from backend if available, otherwise extract from path
const fileName = const fileName =
(fileInfo as any).filename || fileInfoEntry.filename ||
filePath.split("/").pop() || filePath.split("/").pop() ||
filePath; filePath;
const fileStatus = fileInfo.status as string; const fileStatus = fileInfoEntry.status ?? "processing";
// Map backend file status to our TaskFile status // Map backend file status to our TaskFile status
let mappedStatus: TaskFile["status"]; let mappedStatus: TaskFile["status"];
@ -162,6 +166,23 @@ export function TaskProvider({ children }: { children: React.ReactNode }) {
mappedStatus = "processing"; mappedStatus = "processing";
} }
const fileError = (() => {
if (
typeof fileInfoEntry.error === "string" &&
fileInfoEntry.error.trim().length > 0
) {
return fileInfoEntry.error.trim();
}
if (
mappedStatus === "failed" &&
typeof currentTask.error === "string" &&
currentTask.error.trim().length > 0
) {
return currentTask.error.trim();
}
return undefined;
})();
setFiles((prevFiles) => { setFiles((prevFiles) => {
const existingFileIndex = prevFiles.findIndex( const existingFileIndex = prevFiles.findIndex(
(f) => (f) =>
@ -185,13 +206,14 @@ export function TaskProvider({ children }: { children: React.ReactNode }) {
status: mappedStatus, status: mappedStatus,
task_id: currentTask.task_id, task_id: currentTask.task_id,
created_at: created_at:
typeof fileInfo.created_at === "string" typeof fileInfoEntry.created_at === "string"
? fileInfo.created_at ? fileInfoEntry.created_at
: now, : now,
updated_at: updated_at:
typeof fileInfo.updated_at === "string" typeof fileInfoEntry.updated_at === "string"
? fileInfo.updated_at ? fileInfoEntry.updated_at
: now, : now,
error: fileError,
}; };
if (existingFileIndex >= 0) { if (existingFileIndex >= 0) {