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) => 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) => { 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), }; }