Add AI review workflow for Paperless documents

This commit is contained in:
2026-05-07 20:04:30 +02:00
parent 210b77876d
commit f913bc0ba6
24 changed files with 2169 additions and 15 deletions

View File

@@ -0,0 +1,154 @@
import AutoAwesomeIcon from "@mui/icons-material/AutoAwesome";
import RefreshIcon from "@mui/icons-material/Refresh";
import {
Alert,
Box,
Button,
Chip,
Paper,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Typography
} from "@mui/material";
import { useQuery } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { fetchAiReviews } from "../api/aiReviews";
import { fetchServerConfig } from "../api/config";
import PageHeader from "../components/PageHeader";
import { formatDate } from "../utils/date";
function statusColor(status: string): "default" | "primary" | "success" | "warning" | "error" {
if (status === "approved") return "success";
if (status === "needs_review") return "primary";
if (status === "failed") return "error";
if (status === "analyzing" || status === "pending") return "warning";
return "default";
}
export default function AiReviewList() {
const { t } = useTranslation();
const navigate = useNavigate();
const {
data: reviews,
isLoading,
isError,
refetch,
isFetching
} = useQuery({
queryKey: ["ai-reviews"],
queryFn: () => fetchAiReviews()
});
const { data: serverConfig } = useQuery({
queryKey: ["server-config"],
queryFn: fetchServerConfig
});
return (
<>
<PageHeader
title={t("aiReviews.title")}
subtitle={t("aiReviews.subtitle")}
action={
<Button
variant="outlined"
startIcon={<RefreshIcon />}
onClick={() => refetch()}
disabled={isFetching}
>
{t("aiReviews.refresh")}
</Button>
}
/>
{!serverConfig?.aiConfigured && (
<Alert severity="warning" sx={{ mb: 2 }}>
{t("aiReviews.aiNotConfigured")}
</Alert>
)}
{!serverConfig?.paperlessWebhookConfigured && (
<Alert severity="info" sx={{ mb: 2 }}>
{t("aiReviews.webhookNotConfigured")}
</Alert>
)}
<Paper variant="outlined" sx={{ borderRadius: 3, p: 2.5 }}>
<Table>
<TableHead>
<TableRow>
<TableCell>{t("aiReviews.columns.document")}</TableCell>
<TableCell>{t("aiReviews.columns.status")}</TableCell>
<TableCell>{t("aiReviews.columns.confidence")}</TableCell>
<TableCell>{t("aiReviews.columns.updated")}</TableCell>
<TableCell align="right">{t("aiReviews.columns.action")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{isLoading && (
<TableRow>
<TableCell colSpan={5}>{t("messages.loading")}</TableCell>
</TableRow>
)}
{isError && (
<TableRow>
<TableCell colSpan={5}>
<Typography color="error">{t("aiReviews.loadError")}</Typography>
</TableCell>
</TableRow>
)}
{!isLoading && !isError && (reviews ?? []).length === 0 && (
<TableRow>
<TableCell colSpan={5}>
<Box display="flex" alignItems="center" gap={1}>
<AutoAwesomeIcon color="disabled" />
<Typography color="text.secondary">{t("aiReviews.empty")}</Typography>
</Box>
</TableCell>
</TableRow>
)}
{(reviews ?? []).map((review) => (
<TableRow
hover
key={review.id}
sx={{ cursor: "pointer" }}
onClick={() => navigate(`/ai-reviews/${review.id}`)}
>
<TableCell>
<Typography fontWeight={600}>
{review.documentTitle ?? `${t("aiReviews.document")} #${review.paperlessDocumentId}`}
</Typography>
<Typography variant="caption" color="text.secondary">
Paperless #{review.paperlessDocumentId}
</Typography>
</TableCell>
<TableCell>
<Chip
size="small"
color={statusColor(review.status)}
label={t(`aiReviews.status.${review.status}`)}
/>
</TableCell>
<TableCell>
{review.confidence !== null ? `${Math.round(review.confidence * 100)}%` : "-"}
</TableCell>
<TableCell>{formatDate(review.updatedAt, "dd.MM.yyyy HH:mm")}</TableCell>
<TableCell align="right">
<Button size="small" onClick={(event) => {
event.stopPropagation();
navigate(`/ai-reviews/${review.id}`);
}}>
{t("aiReviews.review")}
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Paper>
</>
);
}