Files
genshin-assistant/src/features/scan/components/modals/hooks/useScanDiagnosticsModalModel.ts
T
2026-07-09 08:44:50 +02:00

242 lines
11 KiB
TypeScript

import { detailFingerprint } from "../../../../../lib/autoScanLoop";
import { dataGeneratedAt, sourceVersion } from "../../../../../lib/genshinData";
import { dataPackageStatus } from "../../../../../lib/dataPackageStatus";
import { validateLookupPackage } from "../../../../../lib/genshinLookup";
import { useCallback, useMemo, type MouseEvent } from "react";
import type { ScanDiagnosticsModalProps } from "../types";
import type { ScanDiagnosticEvent } from "../../../../../lib/scanDiagnosticsLog";
export interface ScanDiagnosticsModelProgress {
width: number;
show: boolean;
}
export interface ScanDiagnosticsModalModel {
closeDiagnostics: () => void;
openDetails: () => void;
handleSaveReviewSample: () => void;
stopPropagation: (event: MouseEvent<HTMLDivElement>) => void;
canOpenDetails: boolean;
statusTitle: string;
rightsClassName: string;
rightsValue: string;
genshinClassName: string;
genshinValue: string;
shouldShowAdminBanner: boolean;
gridSourceClass: string;
gridMainValue: string;
gridMetaValue: string;
learningRulesText: string;
learningRulesSubtext: string;
autoScanModeLabel: string;
fingerprintText: string;
runtimeRows: string[];
scanLimitText: string;
scanTipText: string;
autoScanStatsLines: Array<{ label: string; value: number }>;
playerProgress: ScanDiagnosticsModelProgress;
autoScanRunning: boolean;
showDevRows: boolean;
reviewStatus: string;
automationLogLines: string[];
diagnosticEvents: ScanDiagnosticEvent[];
canSaveReviewSample: boolean;
}
type ScanDiagnosticsController = ScanDiagnosticsModalProps["controller"];
interface UseScanDiagnosticsModalModelInput extends Pick<
ScanDiagnosticsModalProps,
| "setDetailsOpen"
| "setDiagnosticsOpen"
| "latestCapture"
| "controller"
> {
captureStatus: string;
// saveReviewSample / canSaveReviewSample live on the controller, not the modal
// props; the component wires them through from controller.* .
saveReviewSample: ScanDiagnosticsController["saveReviewSample"];
canSaveReviewSample: ScanDiagnosticsController["canSaveReviewSample"];
}
export function useScanDiagnosticsModalModel({
setDetailsOpen,
setDiagnosticsOpen,
saveReviewSample,
canSaveReviewSample,
latestCapture,
controller,
captureStatus,
}: UseScanDiagnosticsModalModelInput): ScanDiagnosticsModalModel {
const closeDiagnostics = useCallback(() => setDiagnosticsOpen(false), [setDiagnosticsOpen]);
const openDetails = useCallback(() => setDetailsOpen(true), [setDetailsOpen]);
const handleSaveReviewSample = useCallback(() => {
if (canSaveReviewSample && controller.parsedArtifact) {
void saveReviewSample();
}
}, [canSaveReviewSample, controller.parsedArtifact, saveReviewSample]);
const stopPropagation = useCallback((event: MouseEvent<HTMLDivElement>) => {
event.stopPropagation();
}, []);
const canOpenDetails = Boolean(latestCapture?.crops?.length || latestCapture?.ocr?.length);
const shouldShowAdminBanner = !controller.runtimeInfo?.isElevated;
const rightsClassName = controller.runtimeInfo?.isElevated ? "ok" : "blocked";
const rightsValue = controller.runtimeInfo?.isElevated ? "Admin" : "Standard";
const genshinClassName = controller.runtimeInfo?.genshinFound ? "ok" : "neutral";
const genshinValue = controller.runtimeInfo?.genshinFound ? "Gefunden" : "Nicht gefunden";
const statusTitle = controller.devMode ? "Dev-Ausgabe kompakt" : "Dev-Ausgabe erweitern";
const autoScanModeLabel = controller.autoScanRunning ? "Scan active" : "Last scan";
const inventoryGrid = latestCapture?.inventoryGrid;
const gridSourceClass = inventoryGrid?.source ?? "missing";
const gridMainValue = inventoryGrid
? inventoryGrid?.source === "missing"
? "not detected"
: `${inventoryGrid?.cols} x ${inventoryGrid?.rows}`
: "waiting";
const gridMetaValue = inventoryGrid
? `${inventoryGrid?.confidence}% confidence - ${inventoryGrid?.centers.length} click targets`
: "Run a capture once while the artifact inventory is visible.";
const learningRulesText = controller.learningRulesLoaded ? `${controller.learningRuleCount} local rules` : "loading";
const dataStaleness = dataPackageStatus(dataGeneratedAt, sourceVersion);
const lookupStatus = validateLookupPackage();
const lookupText = lookupStatus.valid
? `Lookup OK: ${lookupStatus.summary.artifactSets} sets / ${lookupStatus.summary.artifactPieces} pieces`
: `Lookup invalid: ${lookupStatus.errors[0] ?? "unknown error"}`;
const learningRulesSubtext = `Review samples feed deterministic OCR fixes before each new parse. ${lookupText}. Data package: ${sourceVersion}${dataStaleness.warning ? ` - ${dataStaleness.warning}` : ""}`;
const playerProgress = useMemo(() => {
const width = Math.min(
100,
controller.autoScanStats.attempted > 0
? Math.round((controller.autoScanStats.verified / Math.max(1, controller.autoScanStats.attempted)) * 100)
: 0,
);
const show = controller.autoScanRunning || controller.autoScanStats.clicked > 0 || controller.autoScanStats.parsed > 0;
return { width, show };
}, [
controller.autoScanRunning,
controller.autoScanStats.attempted,
controller.autoScanStats.clicked,
controller.autoScanStats.parsed,
controller.autoScanStats.verified,
]);
const autoScanStatsLines = useMemo(
() => [
{ label: "clicked", value: controller.autoScanStats.clicked },
{ label: "attempted", value: controller.autoScanStats.attempted },
{ label: "verified", value: controller.autoScanStats.verified },
{ label: "parsed", value: controller.autoScanStats.parsed },
{ label: "stored", value: controller.autoScanStats.stored },
{ label: "review", value: controller.autoScanStats.review },
{ label: "duplicates", value: controller.autoScanStats.duplicates },
{ label: "misses", value: controller.autoScanStats.misses },
{ label: "pages", value: controller.autoScanStats.pages },
{ label: "elapsedMs", value: controller.autoScanStats.elapsedMs },
{ label: "activeScanMs", value: controller.autoScanStats.activeScanMs },
{ label: "writeFlushMs", value: controller.autoScanStats.writeFlushMs },
{ label: "avgMs", value: controller.autoScanStats.averageMsPerParsed },
{ label: "activeAvgMs", value: controller.autoScanStats.activeAverageMsPerParsed },
{ label: "avgCaptureMs", value: controller.autoScanStats.averageCaptureMs },
{ label: "avgCaptureRoundTripMs", value: controller.autoScanStats.averageCaptureRoundTripMs },
{ label: "avgCaptureRoundTripOverheadMs", value: controller.autoScanStats.averageCaptureRoundTripOverheadMs },
{ label: "captureP50Ms", value: controller.autoScanStats.captureP50Ms },
{ label: "captureP90Ms", value: controller.autoScanStats.captureP90Ms },
{ label: "avgOcrMs", value: controller.autoScanStats.averageOcrMs },
{ label: "ocrP50Ms", value: controller.autoScanStats.ocrP50Ms },
{ label: "ocrP90Ms", value: controller.autoScanStats.ocrP90Ms },
{ label: "cardReadyAvgMs", value: controller.autoScanStats.averageCardReadyMs },
{ label: "cardReadyCount", value: controller.autoScanStats.cardReadyCount },
{ label: "scrollReadyAvgMs", value: controller.autoScanStats.averageScrollReadyMs },
{ label: "scrollReadyCount", value: controller.autoScanStats.scrollReadyCount },
{ label: "perMin x10", value: Math.round(controller.autoScanStats.artifactsPerMinute * 10) },
{ label: "activePerMin x10", value: Math.round(controller.autoScanStats.activeArtifactsPerMinute * 10) },
{ label: "projected100Ms", value: controller.autoScanStats.projectedMsFor100 },
{ label: "activeProjected100Ms", value: controller.autoScanStats.activeProjectedMsFor100 },
],
[
controller.autoScanStats.clicked,
controller.autoScanStats.attempted,
controller.autoScanStats.verified,
controller.autoScanStats.parsed,
controller.autoScanStats.stored,
controller.autoScanStats.review,
controller.autoScanStats.duplicates,
controller.autoScanStats.misses,
controller.autoScanStats.pages,
controller.autoScanStats.elapsedMs,
controller.autoScanStats.activeScanMs,
controller.autoScanStats.writeFlushMs,
controller.autoScanStats.averageMsPerParsed,
controller.autoScanStats.activeAverageMsPerParsed,
controller.autoScanStats.averageCaptureMs,
controller.autoScanStats.averageCaptureRoundTripMs,
controller.autoScanStats.averageCaptureRoundTripOverheadMs,
controller.autoScanStats.captureP50Ms,
controller.autoScanStats.captureP90Ms,
controller.autoScanStats.averageOcrMs,
controller.autoScanStats.ocrP50Ms,
controller.autoScanStats.ocrP90Ms,
controller.autoScanStats.averageCardReadyMs,
controller.autoScanStats.cardReadyCount,
controller.autoScanStats.averageScrollReadyMs,
controller.autoScanStats.scrollReadyCount,
controller.autoScanStats.artifactsPerMinute,
controller.autoScanStats.activeArtifactsPerMinute,
controller.autoScanStats.projectedMsFor100,
controller.autoScanStats.activeProjectedMsFor100,
],
);
const runtimeRows = useMemo(() => {
const selectedSourceLabel = controller.selectedSource
? `Selected: ${controller.selectedSource.name}`
: "No source selected";
const storedTotalLabel = controller.storedTotal !== null ? `DB: ${controller.storedTotal} Artifacts` : "DB: -";
const runtime = controller.runtimeInfo
? `${controller.runtimeInfo?.isElevated ? "admin" : "standard"} - target ${controller.runtimeInfo?.genshinFound ? controller.runtimeInfo?.targetProcess || "Genshin" : "missing"} - fg ${controller.runtimeInfo?.foregroundProcess || "unknown"}`
: "unknown";
return [selectedSourceLabel, storedTotalLabel, runtime, captureStatus, controller.reviewStatus].filter(Boolean);
}, [controller.selectedSource, controller.storedTotal, controller.runtimeInfo, captureStatus, controller.reviewStatus]);
const fingerprintText = latestCapture ? detailFingerprint(latestCapture) : "--";
const scanLimitText = controller.scanLimit ? `Scan-Limit: ${controller.scanLimit}` : "Keine Limitinfo.";
const scanTipText = "Tip: Die Auto-Scan Logik bleibt aktivierbar, aber Dev-Ansicht ist rein informativ.";
return {
closeDiagnostics,
openDetails,
handleSaveReviewSample,
stopPropagation,
canOpenDetails,
statusTitle,
rightsClassName,
rightsValue,
genshinClassName,
genshinValue,
shouldShowAdminBanner,
gridSourceClass,
gridMainValue,
gridMetaValue,
learningRulesText,
learningRulesSubtext,
autoScanModeLabel,
fingerprintText,
runtimeRows,
scanLimitText,
scanTipText,
autoScanStatsLines,
playerProgress,
autoScanRunning: controller.autoScanRunning,
showDevRows: controller.devMode,
reviewStatus: controller.reviewStatus,
automationLogLines: controller.automationLog,
diagnosticEvents: controller.diagnosticEvents,
canSaveReviewSample: canSaveReviewSample && Boolean(controller.parsedArtifact),
};
}