show file processing errors in UI
This commit is contained in:
parent
c8c719c7eb
commit
77f558e690
5 changed files with 204 additions and 131 deletions
|
|
@ -50,6 +50,7 @@ export interface File {
|
|||
| "failed"
|
||||
| "hidden"
|
||||
| "sync";
|
||||
error?: string;
|
||||
chunks?: ChunkResult[];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue