Evolve summarizer into AI service with OCR support
This commit is contained in:
@@ -14,12 +14,14 @@ import {
|
||||
import { api, getApiErrorMessage } from "../api";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
|
||||
type SummarizerMetrics = {
|
||||
type AiServiceMetrics = {
|
||||
healthy: boolean;
|
||||
model?: string | null;
|
||||
device?: string | null;
|
||||
gpuAvailable?: boolean;
|
||||
gpuName?: string | null;
|
||||
ocrAvailable?: boolean | null;
|
||||
ocrLanguages?: string | null;
|
||||
healthLatencyMs?: number | null;
|
||||
probeLatencyMs?: number | null;
|
||||
lastProbeAt?: string | null;
|
||||
@@ -31,6 +33,11 @@ type SummarizerMetrics = {
|
||||
cacheMisses: number;
|
||||
failures: number;
|
||||
averageLatencyMs?: number | null;
|
||||
ocrRequests: number;
|
||||
ocrFailures: number;
|
||||
averageOcrLatencyMs?: number | null;
|
||||
lastOcrSuccessAt?: string | null;
|
||||
lastOcrFailureAt?: string | null;
|
||||
lastSuccessAt?: string | null;
|
||||
lastFailureAt?: string | null;
|
||||
lastError?: string | null;
|
||||
@@ -79,7 +86,7 @@ type SystemStatus = {
|
||||
googleConfigured: boolean;
|
||||
gmailConfigured: boolean;
|
||||
};
|
||||
summarizer: SummarizerMetrics;
|
||||
ai: AiServiceMetrics;
|
||||
};
|
||||
|
||||
function formatBytes(bytes?: number | null) {
|
||||
@@ -148,10 +155,10 @@ export default function AdminSystemPage() {
|
||||
return "success" as const;
|
||||
}, [status]);
|
||||
|
||||
const summarizerTone = useMemo(() => {
|
||||
const aiTone = useMemo(() => {
|
||||
if (!status) return "default" as const;
|
||||
if (!status.summarizer.healthy) return "error" as const;
|
||||
if (status.summarizer.probeFailures > 0 || status.summarizer.failures > 0) return "warning" as const;
|
||||
if (!status.ai.healthy) return "error" as const;
|
||||
if (status.ai.probeFailures > 0 || status.ai.failures > 0 || (status.ai.ocrFailures ?? 0) > 0) return "warning" as const;
|
||||
return "success" as const;
|
||||
}, [status]);
|
||||
|
||||
@@ -184,10 +191,10 @@ export default function AdminSystemPage() {
|
||||
setRunningProbe(true);
|
||||
setError(null);
|
||||
try {
|
||||
await api.post("/admin/system/summarizer/probe");
|
||||
await api.post("/admin/system/ai/probe");
|
||||
await load();
|
||||
} catch (e: any) {
|
||||
setError(getApiErrorMessage(e, "Failed to run summarizer probe."));
|
||||
setError(getApiErrorMessage(e, "Failed to run AI service probe."));
|
||||
} finally {
|
||||
setRunningProbe(false);
|
||||
}
|
||||
@@ -204,7 +211,7 @@ export default function AdminSystemPage() {
|
||||
|
||||
{error ? <Alert severity="error">{error}</Alert> : null}
|
||||
{status?.database.warning ? <Alert severity={status.database.canConnect ? "warning" : "error"}>{status.database.warning}</Alert> : null}
|
||||
{status?.summarizer.lastError ? <Alert severity={status.summarizer.healthy ? "warning" : "error"}>{status.summarizer.lastError}</Alert> : null}
|
||||
{status?.ai.lastError ? <Alert severity={status.ai.healthy ? "warning" : "error"}>{status.ai.lastError}</Alert> : null}
|
||||
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "repeat(4, 1fr)" }, gap: 2 }}>
|
||||
<SummaryCard
|
||||
@@ -226,13 +233,13 @@ export default function AdminSystemPage() {
|
||||
/>
|
||||
<SummaryCard
|
||||
title={t("adminSystemSummarizer")}
|
||||
value={status?.summarizer.healthy ? t("adminSystemHealthy") : t("adminSystemOffline")}
|
||||
subtitle={status?.summarizer.probeLatencyMs != null
|
||||
? `${status.summarizer.probeLatencyMs} ms probe · ${status.summarizer.device || "unknown device"}`
|
||||
: status?.summarizer.healthLatencyMs != null
|
||||
? `${status.summarizer.healthLatencyMs} ms health · ${status.summarizer.device || "unknown device"}`
|
||||
value={status?.ai.healthy ? t("adminSystemHealthy") : t("adminSystemOffline")}
|
||||
subtitle={status?.ai.probeLatencyMs != null
|
||||
? `${status.ai.probeLatencyMs} ms probe · ${status.ai.device || "unknown device"}`
|
||||
: status?.ai.healthLatencyMs != null
|
||||
? `${status.ai.healthLatencyMs} ms health · ${status.ai.device || "unknown device"}`
|
||||
: t("adminSystemNoLatencyData")}
|
||||
tone={summarizerTone}
|
||||
tone={aiTone}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -288,15 +295,15 @@ export default function AdminSystemPage() {
|
||||
<Paper sx={{ p: 2, borderRadius: 3 }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 900, mb: 1 }}>{t("adminSystemSummarizerRuntime")}</Typography>
|
||||
<Stack spacing={0.75}>
|
||||
<DetailRow label="Model" value={status?.summarizer.model || "-"} />
|
||||
<DetailRow label="Device" value={status?.summarizer.device || "-"} />
|
||||
<DetailRow label="GPU available" value={status?.summarizer.gpuAvailable ? "Yes" : "No"} />
|
||||
<DetailRow label="GPU name" value={status?.summarizer.gpuName || "-"} />
|
||||
<DetailRow label="Health latency" value={status?.summarizer.healthLatencyMs != null ? `${status.summarizer.healthLatencyMs} ms` : "-"} />
|
||||
<DetailRow label="Probe latency" value={status?.summarizer.probeLatencyMs != null ? `${status.summarizer.probeLatencyMs} ms` : "-"} />
|
||||
<DetailRow label="Last probe" value={formatDate(status?.summarizer.lastProbeAt)} />
|
||||
<DetailRow label="Last successful probe" value={formatDate(status?.summarizer.lastProbeSuccessAt)} />
|
||||
<DetailRow label="Last summarization success" value={formatDate(status?.summarizer.lastSuccessAt)} />
|
||||
<DetailRow label="Model" value={status?.ai.model || "-"} />
|
||||
<DetailRow label="Device" value={status?.ai.device || "-"} />
|
||||
<DetailRow label="GPU available" value={status?.ai.gpuAvailable ? "Yes" : "No"} />
|
||||
<DetailRow label="GPU name" value={status?.ai.gpuName || "-"} />
|
||||
<DetailRow label="Health latency" value={status?.ai.healthLatencyMs != null ? `${status.ai.healthLatencyMs} ms` : "-"} />
|
||||
<DetailRow label="Probe latency" value={status?.ai.probeLatencyMs != null ? `${status.ai.probeLatencyMs} ms` : "-"} />
|
||||
<DetailRow label="Last probe" value={formatDate(status?.ai.lastProbeAt)} />
|
||||
<DetailRow label="Last successful probe" value={formatDate(status?.ai.lastProbeSuccessAt)} />
|
||||
<DetailRow label="Last summarization success" value={formatDate(status?.ai.lastSuccessAt)} />
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
@@ -320,20 +327,23 @@ export default function AdminSystemPage() {
|
||||
|
||||
<Paper sx={{ p: 2, borderRadius: 3 }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 900, mb: 1 }}>{t("adminSystemSummarizerTelemetry")}</Typography>
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr 1fr", md: "repeat(6, 1fr)" }, gap: 2 }}>
|
||||
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>Requests</Typography><Typography variant="h6" sx={{ fontWeight: 900 }}>{status?.summarizer.requests ?? 0}</Typography></Box>
|
||||
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>Cache hits</Typography><Typography variant="h6" sx={{ fontWeight: 900 }}>{status?.summarizer.cacheHits ?? 0}</Typography></Box>
|
||||
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>Cache misses</Typography><Typography variant="h6" sx={{ fontWeight: 900 }}>{status?.summarizer.cacheMisses ?? 0}</Typography></Box>
|
||||
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>Failures</Typography><Typography variant="h6" sx={{ fontWeight: 900 }}>{status?.summarizer.failures ?? 0}</Typography></Box>
|
||||
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>Probe failures</Typography><Typography variant="h6" sx={{ fontWeight: 900 }}>{status?.summarizer.probeFailures ?? 0}</Typography></Box>
|
||||
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>Avg latency</Typography><Typography variant="h6" sx={{ fontWeight: 900 }}>{status?.summarizer.averageLatencyMs != null ? `${status.summarizer.averageLatencyMs} ms` : "-"}</Typography></Box>
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr 1fr", md: "repeat(8, 1fr)" }, gap: 2 }}>
|
||||
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>Requests</Typography><Typography variant="h6" sx={{ fontWeight: 900 }}>{status?.ai.requests ?? 0}</Typography></Box>
|
||||
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>Cache hits</Typography><Typography variant="h6" sx={{ fontWeight: 900 }}>{status?.ai.cacheHits ?? 0}</Typography></Box>
|
||||
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>Cache misses</Typography><Typography variant="h6" sx={{ fontWeight: 900 }}>{status?.ai.cacheMisses ?? 0}</Typography></Box>
|
||||
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>Failures</Typography><Typography variant="h6" sx={{ fontWeight: 900 }}>{status?.ai.failures ?? 0}</Typography></Box>
|
||||
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>Probe failures</Typography><Typography variant="h6" sx={{ fontWeight: 900 }}>{status?.ai.probeFailures ?? 0}</Typography></Box>
|
||||
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>Avg latency</Typography><Typography variant="h6" sx={{ fontWeight: 900 }}>{status?.ai.averageLatencyMs != null ? `${status.ai.averageLatencyMs} ms` : "-"}</Typography></Box>
|
||||
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>OCR requests</Typography><Typography variant="h6" sx={{ fontWeight: 900 }}>{status?.ai.ocrRequests ?? 0}</Typography></Box>
|
||||
<Box><Typography variant="overline" sx={{ color: "text.secondary" }}>OCR avg latency</Typography><Typography variant="h6" sx={{ fontWeight: 900 }}>{status?.ai.averageOcrLatencyMs != null ? `${status.ai.averageOcrLatencyMs} ms` : "-"}</Typography></Box>
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", mt: 2 }}>
|
||||
<Chip label={status?.database.canConnect ? t("adminSystemDatabaseConnected") : t("adminSystemDatabaseIssue")} color={status?.database.canConnect ? "success" : "error"} size="small" />
|
||||
<Chip label={status?.auth.required ? t("adminSystemAuthEnforced") : t("adminSystemAuthOptional")} color={status?.auth.required ? "success" : "warning"} size="small" />
|
||||
<Chip label={status?.auth.googleConfigured ? t("adminSystemGoogleReady") : t("adminSystemGoogleOff")} variant="outlined" size="small" />
|
||||
<Chip label={status?.auth.gmailConfigured ? t("adminSystemGmailReady") : t("adminSystemGmailIncomplete")} variant="outlined" size="small" />
|
||||
<Chip label={status?.summarizer.gpuAvailable ? t("adminSystemGpuVisible") : t("adminSystemCpuMode")} color={status?.summarizer.gpuAvailable ? "success" : "default"} size="small" />
|
||||
<Chip label={status?.ai.gpuAvailable ? t("adminSystemGpuVisible") : t("adminSystemCpuMode")} color={status?.ai.gpuAvailable ? "success" : "default"} size="small" />
|
||||
<Chip label={status?.ai.ocrAvailable ? `OCR ${status.ai.ocrLanguages || "enabled"}` : "OCR unavailable"} variant="outlined" size="small" />
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
@@ -29,7 +29,7 @@ type MeResponse = {
|
||||
} | null;
|
||||
};
|
||||
|
||||
const CV_UPLOAD_ACCEPT = ".pdf,.docx,.txt,.md,application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document,text/plain,text/markdown";
|
||||
const CV_UPLOAD_ACCEPT = ".pdf,.docx,.txt,.md,image/png,image/jpeg,image/webp,application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document,text/plain,text/markdown";
|
||||
const AVATAR_UPLOAD_ACCEPT = "image/png,image/jpeg,image/webp";
|
||||
|
||||
function initialsFrom(values: Array<string | undefined>) {
|
||||
|
||||
Reference in New Issue
Block a user