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"
| "hidden"
| "sync";
error?: string;
chunks?: ChunkResult[];
}

View file

@ -4,6 +4,24 @@ import {
useQueryClient,
} 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 {
task_id: string;
status:
@ -24,7 +42,7 @@ export interface Task {
duration_seconds?: number;
result?: Record<string, unknown>;
error?: string;
files?: Record<string, Record<string, unknown>>;
files?: Record<string, TaskFileEntry>;
}
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 SharePointIcon from "../settings/icons/share-point-icon";
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 getSourceIcon(connectorType?: string) {
@ -77,6 +85,7 @@ function SearchPage() {
size: taskFile.size,
connector_type: taskFile.connector_type,
status: taskFile.status,
error: taskFile.error,
};
});
@ -128,7 +137,6 @@ function SearchPage() {
// Read status directly from data on each render
const status = data?.status || "active";
const isActive = status === "active";
console.log(data?.filename, status, "a");
return (
<div className="flex items-center overflow-hidden w-full">
<div
@ -196,9 +204,37 @@ function SearchPage() {
field: "status",
headerName: "Status",
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 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} />;
},
},

View file

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

View file

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