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"
|
| "failed"
|
||||||
| "hidden"
|
| "hidden"
|
||||||
| "sync";
|
| "sync";
|
||||||
|
error?: string;
|
||||||
chunks?: ChunkResult[];
|
chunks?: ChunkResult[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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} />;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue