Improve IK-style artifact scanner pipeline
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { AlertTriangle, Download, Play, Upload, Wrench } from "lucide-react";
|
||||
import { AlertTriangle, BadgeCheck, Camera, ClipboardList, Download, Gauge, Play, Target, Upload, Wrench } from "lucide-react";
|
||||
import type { CaptureResult } from "../../../types/global";
|
||||
import type { ScanViewControllerResult } from "../types";
|
||||
import { FieldConfidenceList } from "./ScanResultCards";
|
||||
@@ -14,6 +14,53 @@ interface DiagnosticsViewProps {
|
||||
canDemoScan?: boolean;
|
||||
}
|
||||
|
||||
const appDiagnosisSections = [
|
||||
{
|
||||
title: "Was die App kann",
|
||||
tone: "ok",
|
||||
icon: BadgeCheck,
|
||||
items: [
|
||||
"Genshin-Fenster erkennen, Smart Capture ausfuehren und fokussierte Artifact-Crops erzeugen.",
|
||||
"Artifact-Felder deterministisch gegen das lokale Genshin-Datenpaket parsen.",
|
||||
"Auto-Scan read-only aus der sichtbaren Inventory-Seite starten, inklusive Grid, Verifikation, Dedupe und Store.",
|
||||
"Review-Samples, lokale Text-Lernregeln, GOOD Import/Export und Lock-Status im Store nutzen.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Was noch fehlt",
|
||||
tone: "warn",
|
||||
icon: ClipboardList,
|
||||
items: [
|
||||
"Paimon-Menue-Einstieg ist gebaut, aber live noch nicht mit 2/20/45 Limits validiert.",
|
||||
"Native/IK-Tesseract ist nur als Benchmark-Pfad vorbereitet, noch nicht Standard.",
|
||||
"Positive locked=true Probe an einem sicher gesperrten Artifact fehlt.",
|
||||
"Empfehlungen bleiben Nebenfunktion, bis Scanner-Vertrauen und Review-Rate stabil genug sind.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Wo es Probleme macht",
|
||||
tone: "risk",
|
||||
icon: AlertTriangle,
|
||||
items: [
|
||||
"OCR ist weiterhin der Haupt-Risikofaktor; einige Felder landen noch in Fallback, Ableitung oder Review.",
|
||||
"Bild-Preprocessing kann nur an echten Captures bewertet werden, nicht allein mit Text-Eval.",
|
||||
"Auto-Scan braucht bei erhoehtem Genshin auch eine erhoehte App-Laufzeit.",
|
||||
"Groessere Runs brauchen weiter Beobachtung auf Scroll-Uebergaenge, Wiederholseiten und Review-Quote.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Naechste Verbesserungen",
|
||||
tone: "next",
|
||||
icon: Target,
|
||||
items: [
|
||||
"Review-Corpus aus echten Samples vergroessern und mit `npm run eval` messbar halten.",
|
||||
"OCR-Benchmark gegen identische Crops fahren und erst danach Engine-Standard wechseln.",
|
||||
"Paimon-Menue-Pfad live pruefen und bei Blockade sichtbar auf visible-inventory zurueckfallen.",
|
||||
"Diagnose weiter als Operator-Cockpit halten: Live-Status, Evidenz und naechster sicherer Schritt.",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// All developer / diagnostic surfaces live here, separated from the Scan
|
||||
// workspace: runtime + rights, grid detection, learning + data-package status,
|
||||
// fingerprint, auto-scan counters, the automation log, and the raw crop/OCR/
|
||||
@@ -39,6 +86,7 @@ export function DiagnosticsView({ controller, latestCapture, captureStatus, onDe
|
||||
playerProgress,
|
||||
reviewStatus,
|
||||
automationLogLines,
|
||||
diagnosticEvents,
|
||||
canSaveReviewSample,
|
||||
handleSaveReviewSample,
|
||||
} = useScanDiagnosticsModalModel({
|
||||
@@ -106,6 +154,37 @@ export function DiagnosticsView({ controller, latestCapture, captureStatus, onDe
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="diagnose-card app-diagnosis-card">
|
||||
<div className="diagnose-card-heading">
|
||||
<div>
|
||||
<p className="eyebrow">Aktueller App-Stand</p>
|
||||
<h3>Scanner zuerst, Empfehlungen danach</h3>
|
||||
</div>
|
||||
<span className="diagnosis-source">
|
||||
<Gauge size={14} />
|
||||
Quelle: Docs + Live-Status
|
||||
</span>
|
||||
</div>
|
||||
<div className="app-diagnosis-grid">
|
||||
{appDiagnosisSections.map((section) => {
|
||||
const Icon = section.icon;
|
||||
return (
|
||||
<article className={`app-diagnosis-section ${section.tone}`} key={section.title}>
|
||||
<div className="app-diagnosis-title">
|
||||
<Icon size={16} />
|
||||
<strong>{section.title}</strong>
|
||||
</div>
|
||||
<ul>
|
||||
{section.items.map((item) => (
|
||||
<li key={item}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="diagnose-grid">
|
||||
<div className="diagnose-card">
|
||||
<p className="eyebrow">Status</p>
|
||||
@@ -196,6 +275,67 @@ export function DiagnosticsView({ controller, latestCapture, captureStatus, onDe
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="diagnose-card">
|
||||
<div className="diagnose-card-heading">
|
||||
<div>
|
||||
<p className="eyebrow">Evidence timeline</p>
|
||||
<h3>Scan-Flugschreiber</h3>
|
||||
</div>
|
||||
<span className="diagnosis-source">
|
||||
<Camera size={14} />
|
||||
letzte {diagnosticEvents.length}
|
||||
</span>
|
||||
</div>
|
||||
{diagnosticEvents.length > 0 ? (
|
||||
<div className="scan-evidence-timeline">
|
||||
{diagnosticEvents.slice().reverse().map((event) => (
|
||||
<article className={`scan-evidence-event ${event.severity}`} key={event.id}>
|
||||
<div className="scan-evidence-header">
|
||||
<span>{new Date(event.at).toLocaleTimeString()}</span>
|
||||
<strong>{event.phase}</strong>
|
||||
<em>{event.severity}</em>
|
||||
</div>
|
||||
<p>{event.message}</p>
|
||||
{event.details && (
|
||||
<div className="scan-evidence-details">
|
||||
{Object.entries(event.details).map(([key, value]) => (
|
||||
<span key={key}>{key}: {String(value ?? "-")}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{event.capture && (
|
||||
<div className="scan-evidence-capture">
|
||||
<div>
|
||||
<strong>{event.capture.name}</strong>
|
||||
<span>{event.capture.width}x{event.capture.height} · {event.capture.target ?? "capture"} · fp {event.capture.fingerprint}</span>
|
||||
{event.capture.grid && (
|
||||
<span>grid {event.capture.grid.cols}x{event.capture.grid.rows} · {event.capture.grid.targets} targets · {event.capture.grid.confidence}% · {event.capture.grid.source}</span>
|
||||
)}
|
||||
{event.capture.count && (
|
||||
<span>count {event.capture.count.current}/{event.capture.count.total || "?"} · {event.capture.count.confidence}% · {event.capture.count.text || "-"}</span>
|
||||
)}
|
||||
{event.capture.artifactDetail && (
|
||||
<span>detail {event.capture.artifactDetail.present ? "yes" : "no"} · {event.capture.artifactDetail.confidence}% · orange {event.capture.artifactDetail.orangeHits} · text {event.capture.artifactDetail.textHits}</span>
|
||||
)}
|
||||
{event.capture.paimonMenu && (
|
||||
<span>paimon {event.capture.paimonMenu.present ? "yes" : "no"} · {event.capture.paimonMenu.confidence}%</span>
|
||||
)}
|
||||
{event.capture.layoutWarning && <span className="evidence-warning">{event.capture.layoutWarning}</span>}
|
||||
</div>
|
||||
<div className="scan-evidence-images">
|
||||
{event.capture.screenshots?.inventory && <img src={event.capture.screenshots.inventory} alt={`${event.phase} inventory`} />}
|
||||
{event.capture.screenshots?.detail && <img src={event.capture.screenshots.detail} alt={`${event.phase} detail`} />}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="result-empty">Noch keine Evidence-Events. Starte einen Capture oder Auto-Scan, dann erscheinen hier Schritte mit Screenshots.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="diagnose-card">
|
||||
<div className="diagnose-card-heading">
|
||||
<p className="eyebrow">Crops, OCR & Confidence</p>
|
||||
|
||||
@@ -7,14 +7,9 @@ export function ScanMainSection({
|
||||
latestCapture,
|
||||
captureStatus,
|
||||
parsedArtifact,
|
||||
sourceLabel,
|
||||
gridLabel,
|
||||
inventoryLabel,
|
||||
activeTargetCount,
|
||||
storedTotal,
|
||||
reviewSampleTotal,
|
||||
learningRulesLoaded,
|
||||
learningRuleCount,
|
||||
setDetailsOpen,
|
||||
autoScanRunning,
|
||||
canOpenReviewQueue,
|
||||
@@ -27,29 +22,25 @@ export function ScanMainSection({
|
||||
captureImageSrc,
|
||||
captureImageAlt,
|
||||
hasCapture,
|
||||
captureModeText,
|
||||
resultHeading,
|
||||
noArtifactText,
|
||||
noCaptureMessage,
|
||||
targetLabel,
|
||||
dbLabel,
|
||||
reviewLabel,
|
||||
rulesLabel,
|
||||
} = useScanMainSectionModel({
|
||||
latestCapture,
|
||||
parsedArtifact,
|
||||
activeTargetCount,
|
||||
storedTotal,
|
||||
reviewSampleTotal,
|
||||
learningRulesLoaded,
|
||||
learningRuleCount,
|
||||
setDetailsOpen,
|
||||
openReviewQueue,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="scanner-main-grid">
|
||||
<div className="capture-stage-shell">
|
||||
<div className={`capture-stage-shell ${hasCapture ? "has-capture" : "is-empty"}`}>
|
||||
<div className="capture-stage">
|
||||
{hasCapture ? (
|
||||
<img src={captureImageSrc} alt={captureImageAlt} />
|
||||
@@ -61,10 +52,6 @@ export function ScanMainSection({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="capture-stage-meta">
|
||||
<div><span>Quelle</span><strong>{sourceLabel}</strong></div>
|
||||
<div><span>Inventar</span><strong>{inventoryLabel}</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside className="scanner-result-panel">
|
||||
@@ -81,9 +68,12 @@ export function ScanMainSection({
|
||||
<span>{reviewLabel}</span>
|
||||
</div>
|
||||
<div className="scanner-result-actions">
|
||||
<button className="ghost-button" onClick={handleOpenDetails} disabled={!canOpenDetails}>
|
||||
Details
|
||||
</button>
|
||||
<button className="ghost-button" onClick={handleOpenReviewQueue} disabled={autoScanRunning || !canOpenReviewQueue}>
|
||||
<AlertTriangle size={15} />
|
||||
Review Queue
|
||||
Review
|
||||
</button>
|
||||
</div>
|
||||
<p className="scanner-result-caption">{captureStatus}</p>
|
||||
|
||||
@@ -32,7 +32,7 @@ export function ScanTopControlsSection({
|
||||
openDiagnostics,
|
||||
captureSingleArtifact,
|
||||
stopScan,
|
||||
runVisibleGridScan,
|
||||
runGuidedAutoScan,
|
||||
runAutoReviewScan,
|
||||
bridgeStatusText,
|
||||
bridgePillClass,
|
||||
@@ -46,7 +46,6 @@ export function ScanTopControlsSection({
|
||||
refreshCaptureSourcesTitle,
|
||||
diagnosticsButtonTitle,
|
||||
scanSetupButtonTitle,
|
||||
showPlayerProgress,
|
||||
progressWidth,
|
||||
progressStats,
|
||||
} = useScanTopControlsModel({
|
||||
@@ -64,8 +63,7 @@ export function ScanTopControlsSection({
|
||||
<div className="scanner-header">
|
||||
<div>
|
||||
<p className="eyebrow">Scanner</p>
|
||||
<h2>Artifact capture workspace</h2>
|
||||
<p className="scanner-subcopy">Quelle waehlen, Auto-Scan starten, Ergebnis rechts pruefen. Dev-Details im Diagnose-Tab.</p>
|
||||
<h2>Artifact Scan</h2>
|
||||
</div>
|
||||
<div className="scanner-header-pills">
|
||||
<span className={`runtime-pill ${bridgePillClass}`}>{bridgeStatusText}</span>
|
||||
@@ -119,46 +117,42 @@ export function ScanTopControlsSection({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="player-scan-actions">
|
||||
<button
|
||||
className="primary-button scan-cta"
|
||||
onClick={runVisibleGridScan}
|
||||
disabled={!canStartAutoScan}
|
||||
title={autoScanButtonTitle}
|
||||
>
|
||||
<Play size={16} />
|
||||
{autoScanButtonLabel}
|
||||
</button>
|
||||
<button
|
||||
className="ghost-button"
|
||||
onClick={runAutoReviewScan}
|
||||
disabled={!canStartManualScan}
|
||||
title={manualScanButtonTitle}
|
||||
>
|
||||
<Radar size={15} />
|
||||
Manueller Scan
|
||||
</button>
|
||||
<button
|
||||
className="ghost-button"
|
||||
onClick={captureSingleArtifact}
|
||||
disabled={!canCaptureSingle}
|
||||
title={captureSingleButtonTitle}
|
||||
>
|
||||
<Camera size={15} />
|
||||
Einzelnes Artifact lesen
|
||||
</button>
|
||||
{autoScanRunning && (
|
||||
<button className="stop-button" onClick={stopScan}>
|
||||
Stop
|
||||
<div className="player-scan-lower">
|
||||
<div className="player-scan-actions">
|
||||
<button
|
||||
className="primary-button scan-cta"
|
||||
onClick={runGuidedAutoScan}
|
||||
disabled={!canStartAutoScan}
|
||||
title={autoScanButtonTitle}
|
||||
>
|
||||
<Play size={16} />
|
||||
{autoScanButtonLabel}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
className="ghost-button"
|
||||
onClick={runAutoReviewScan}
|
||||
disabled={!canStartManualScan}
|
||||
title={manualScanButtonTitle}
|
||||
>
|
||||
<Radar size={15} />
|
||||
Manueller Scan
|
||||
</button>
|
||||
<button
|
||||
className="ghost-button"
|
||||
onClick={captureSingleArtifact}
|
||||
disabled={!canCaptureSingle}
|
||||
title={captureSingleButtonTitle}
|
||||
>
|
||||
<Camera size={15} />
|
||||
Einzelnes Artifact
|
||||
</button>
|
||||
{autoScanRunning && (
|
||||
<button className="stop-button" onClick={stopScan}>
|
||||
Stop
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="player-status">
|
||||
{playerStatusText}
|
||||
</p>
|
||||
|
||||
{showPlayerProgress && (
|
||||
<div className="player-progress">
|
||||
<div className="player-progress-bar">
|
||||
<div style={{ width: `${progressWidth}%` }} />
|
||||
@@ -171,7 +165,11 @@ export function ScanTopControlsSection({
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="player-status">
|
||||
{playerStatusText}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -8,14 +8,12 @@ export interface ScanMainSectionModel {
|
||||
captureImageSrc: string;
|
||||
captureImageAlt: string;
|
||||
hasCapture: boolean;
|
||||
captureModeText: string;
|
||||
resultHeading: string;
|
||||
noArtifactText: string;
|
||||
noCaptureMessage: string;
|
||||
targetLabel: string;
|
||||
dbLabel: string;
|
||||
reviewLabel: string;
|
||||
rulesLabel: string;
|
||||
}
|
||||
|
||||
type UseScanMainSectionModelProps = Pick<
|
||||
@@ -25,8 +23,6 @@ type UseScanMainSectionModelProps = Pick<
|
||||
| "activeTargetCount"
|
||||
| "storedTotal"
|
||||
| "reviewSampleTotal"
|
||||
| "learningRulesLoaded"
|
||||
| "learningRuleCount"
|
||||
| "setDetailsOpen"
|
||||
| "openReviewQueue"
|
||||
>;
|
||||
@@ -37,8 +33,6 @@ export function useScanMainSectionModel({
|
||||
activeTargetCount,
|
||||
storedTotal,
|
||||
reviewSampleTotal,
|
||||
learningRulesLoaded,
|
||||
learningRuleCount,
|
||||
setDetailsOpen,
|
||||
openReviewQueue,
|
||||
}: UseScanMainSectionModelProps): ScanMainSectionModel {
|
||||
@@ -53,14 +47,12 @@ export function useScanMainSectionModel({
|
||||
const captureImageAlt = latestCapture
|
||||
? `Latest capture from ${latestCapture.name}`
|
||||
: "Latest capture is not available yet";
|
||||
const captureModeText = latestCapture ? "Erkannt" : "Warte";
|
||||
const resultHeading = parsedArtifact ? parsedArtifact.name : "Noch kein Artifact";
|
||||
const noArtifactText = "Oeffne ein Artifact in Genshin und nutze \"Einzelnes Artifact lesen\" - oder starte direkt den Auto-Scan.";
|
||||
const noCaptureMessage = noArtifactText;
|
||||
const targetLabel = `Ziel ${activeTargetCount}`;
|
||||
const dbLabel = `DB ${storedTotal ?? "-"}`;
|
||||
const reviewLabel = `Review ${reviewSampleTotal}`;
|
||||
const rulesLabel = `Regeln ${learningRulesLoaded ? learningRuleCount : "..."}`;
|
||||
|
||||
return {
|
||||
canOpenDetails,
|
||||
@@ -69,13 +61,11 @@ export function useScanMainSectionModel({
|
||||
captureImageSrc,
|
||||
captureImageAlt,
|
||||
hasCapture: Boolean(latestCapture),
|
||||
captureModeText,
|
||||
resultHeading,
|
||||
noArtifactText,
|
||||
noCaptureMessage,
|
||||
targetLabel,
|
||||
dbLabel,
|
||||
reviewLabel,
|
||||
rulesLabel,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import type { ScanSummaryFooterProps } from "../types";
|
||||
import type { ScanSummaryFooterProps } from "../types";
|
||||
|
||||
function formatDuration(ms: number) {
|
||||
if (ms <= 0) return "0s";
|
||||
const seconds = Math.round(ms / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const rest = seconds % 60;
|
||||
return minutes > 0 ? `${minutes}m ${rest}s` : `${rest}s`;
|
||||
}
|
||||
|
||||
export interface ScanSummaryFooterModel {
|
||||
summaryCopy: string;
|
||||
@@ -12,10 +20,10 @@ export function useScanSummaryFooterModel({
|
||||
}: ScanSummaryFooterProps): ScanSummaryFooterModel {
|
||||
const summaryCopy = scanSummary.status === "blocked" && scanSummary.clicked === 0 && scanSummary.mode !== "Manueller Scan"
|
||||
? "Es wurden keine Klicks ausgefuehrt. Grund siehe oben."
|
||||
: `${scanSummary.attempted} Positionen bearbeitet, ${scanSummary.verified} Ansichten verifiziert, ${scanSummary.parsed} Artifact${scanSummary.parsed === 1 ? "" : "s"} gelesen. Deine Sammlung: ${storedTotal ?? "?"} Artifacts.`;
|
||||
: `${scanSummary.attempted} Positionen bearbeitet, ${scanSummary.verified} Ansichten verifiziert, ${scanSummary.parsed} Artifact${scanSummary.parsed === 1 ? "" : "s"} gelesen in ${formatDuration(scanSummary.elapsedMs)} (${scanSummary.averageMsPerParsed || 0} ms/Artifact). Deine Sammlung: ${storedTotal ?? "?"} Artifacts.`;
|
||||
|
||||
const devCopy = devMode
|
||||
? `clicked ${scanSummary.clicked} · attempted ${scanSummary.attempted} · verified ${scanSummary.verified} · parsed ${scanSummary.parsed} · misses ${scanSummary.misses} · pages ${scanSummary.pages}`
|
||||
? `clicked ${scanSummary.clicked} | attempted ${scanSummary.attempted} | verified ${scanSummary.verified} | parsed ${scanSummary.parsed} | misses ${scanSummary.misses} | pages ${scanSummary.pages} | active ${formatDuration(scanSummary.activeScanMs)} | flush ${scanSummary.writeFlushMs}ms | capture ${scanSummary.averageCaptureMs}ms | ocr ${scanSummary.averageOcrMs}ms | ${scanSummary.artifactsPerMinute}/min | active ${scanSummary.activeArtifactsPerMinute}/min | 100 projected ${formatDuration(scanSummary.projectedMsFor100)}`
|
||||
: null;
|
||||
|
||||
return {
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface ScanTopControlsModel {
|
||||
openDiagnostics: () => void;
|
||||
captureSingleArtifact: () => void;
|
||||
stopScan: () => void;
|
||||
runVisibleGridScan: () => void;
|
||||
runGuidedAutoScan: () => void;
|
||||
runAutoReviewScan: () => void;
|
||||
bridgeStatusText: string;
|
||||
bridgePillClass: string;
|
||||
@@ -47,7 +47,7 @@ export function useScanTopControlsModel({
|
||||
setSettingsOpen,
|
||||
setDiagnosticsOpen,
|
||||
requestScanStop,
|
||||
runVisibleGridScan,
|
||||
runGuidedAutoScan,
|
||||
runAutoReviewScan,
|
||||
autoScanRunning,
|
||||
canCaptureSource,
|
||||
@@ -77,17 +77,20 @@ export function useScanTopControlsModel({
|
||||
const openDiagnostics = useCallback(() => setDiagnosticsOpen(true), [setDiagnosticsOpen]);
|
||||
const captureSingleArtifact = useCallback(() => captureSelectedSource(0, true), [captureSelectedSource]);
|
||||
const stopScan = useCallback(() => requestScanStop("Stop-Button gedrueckt."), [requestScanStop]);
|
||||
const startGuidedAutoScan = useCallback(() => {
|
||||
void runGuidedAutoScan();
|
||||
}, [runGuidedAutoScan]);
|
||||
const bridgeStatusText = bridgeReady ? "Bridge verbunden" : "Bridge fehlt";
|
||||
const bridgePillClass = bridgeReady ? "elevated" : "standard";
|
||||
const runtimeStatusText = runtimeInfo?.isElevated ? "Admin bereit" : "Standard";
|
||||
const runtimePillClass = runtimeInfo?.isElevated ? "elevated" : "standard";
|
||||
const playerStatusText = reviewStatus
|
||||
|| (runtimeInfo?.isElevated
|
||||
? "App laeuft als Administrator. Oeffne in Genshin das Artifact-Inventar und starte den Auto-Scan."
|
||||
? "App laeuft als Administrator. Auto-Scan prueft den Screen ohne OCR und startet erst, wenn eine Artifact-Detailkarte offen ist."
|
||||
: "App laeuft im Standard-Modus - Auto-Scan braucht Administrator-Rechte. Bitte die App schliessen und als Administrator neu starten.");
|
||||
const autoScanButtonTitle = requiresAdminForAutoScan
|
||||
? "App laeuft nicht als Administrator. Bitte die App als Administrator neu starten."
|
||||
: "Klickt und scrollt automatisch durch das sichtbare Artifact-Inventar.";
|
||||
: "Prueft zuerst ohne OCR den Screen, oeffnet bei Bedarf per Inventory-Kamera-Sequenz das Artifact-Inventar und scannt erst mit sichtbarer Detailkarte.";
|
||||
const autoScanButtonLabel = autoScanRunning ? "Scan laeuft..." : "Auto-Scan starten";
|
||||
const manualScanButtonTitle = "Du klickst die Artifacts in Genshin selbst an; die App liest nur mit. Kein Auto-Klick, kein Scrollen.";
|
||||
const captureSingleButtonTitle = "Liest das gerade in Genshin geoeffnete Artifact einmalig.";
|
||||
@@ -104,11 +107,20 @@ export function useScanTopControlsModel({
|
||||
{ label: "Klicks", value: autoScanStats.clicked },
|
||||
{ label: "Positionen", value: autoScanStats.attempted },
|
||||
{ label: "Verifiziert", value: autoScanStats.verified },
|
||||
{ label: "ms/Artifact", value: autoScanStats.averageMsPerParsed || "-" },
|
||||
{ label: "Gespeichert", value: autoScanStats.stored },
|
||||
{ label: "Review", value: autoScanStats.review },
|
||||
{ label: "Sammlung", value: storedTotal ?? "-", extraClass: "collection" },
|
||||
],
|
||||
[autoScanStats.clicked, autoScanStats.attempted, autoScanStats.verified, autoScanStats.stored, autoScanStats.review, storedTotal],
|
||||
[
|
||||
autoScanStats.clicked,
|
||||
autoScanStats.attempted,
|
||||
autoScanStats.verified,
|
||||
autoScanStats.averageMsPerParsed,
|
||||
autoScanStats.stored,
|
||||
autoScanStats.review,
|
||||
storedTotal,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -124,7 +136,7 @@ export function useScanTopControlsModel({
|
||||
openDiagnostics,
|
||||
captureSingleArtifact,
|
||||
stopScan,
|
||||
runVisibleGridScan,
|
||||
runGuidedAutoScan: startGuidedAutoScan,
|
||||
runAutoReviewScan,
|
||||
bridgeStatusText,
|
||||
bridgePillClass,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ScanSettingsModalProps } from "./types";
|
||||
import { useScanSettingsModalModel } from "./hooks/useScanSettingsModalModel";
|
||||
import type { StepperControlModel } from "./hooks/useScanSettingsModalModel";
|
||||
|
||||
export function ScanSettingsModal({
|
||||
open,
|
||||
@@ -16,9 +17,9 @@ export function ScanSettingsModal({
|
||||
|
||||
const {
|
||||
closeSettings,
|
||||
handleScanLimitChange,
|
||||
handleSkipRowsChange,
|
||||
stopPropagation,
|
||||
scanLimitControl,
|
||||
skipRowsControl,
|
||||
inventoryCountText,
|
||||
inventoryClassName,
|
||||
scanLimitClassName,
|
||||
@@ -50,33 +51,17 @@ export function ScanSettingsModal({
|
||||
<button className="ghost-button" onClick={closeSettings}>Schliessen</button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="scan-config-strip">
|
||||
<label>
|
||||
<span>Anzahl Artifacts</span>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={1800}
|
||||
value={scanLimit}
|
||||
onChange={handleScanLimitChange}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>Zeilen ueberspringen</span>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
max={8}
|
||||
value={skipRows}
|
||||
onChange={handleSkipRowsChange}
|
||||
/>
|
||||
</label>
|
||||
<p>
|
||||
Die App uebernimmt die erkannte Inventar-Anzahl nur als Startwert und Deckel nach oben. Dein manuell gesetztes Ziel bleibt erhalten.
|
||||
Mit "Zeilen ueberspringen" kannst du den Startverzug korrigieren, falls du nicht am Anfang der Liste beginnst.
|
||||
</p>
|
||||
<div className="scan-settings-layout">
|
||||
<div className="scan-settings-controls">
|
||||
<StepperControl control={scanLimitControl} />
|
||||
<StepperControl control={skipRowsControl} />
|
||||
</div>
|
||||
<div className="scan-settings-note">
|
||||
<strong>Manuelle Werte bleiben erhalten.</strong>
|
||||
<span>Die erkannte Inventar-Anzahl ist nur ein Vorschlag und oberer Deckel. Startzeilen brauchst du nur, wenn du nicht oben beginnst.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="scanner-preflight diagnostics-preflight">
|
||||
<div className="scanner-preflight settings-preflight">
|
||||
<div className={inventoryClassName}>
|
||||
<span>Inventarzaehler</span>
|
||||
<strong>{inventoryCountText}</strong>
|
||||
@@ -97,3 +82,42 @@ export function ScanSettingsModal({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StepperControl({ control }: { control: StepperControlModel }) {
|
||||
return (
|
||||
<section className="settings-stepper" aria-label={control.label}>
|
||||
<div className="settings-stepper-head">
|
||||
<div>
|
||||
<span>{control.label}</span>
|
||||
<small>{control.helper}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div className="settings-stepper-row">
|
||||
<button type="button" className="stepper-button" onClick={control.decrement} aria-label={`${control.label} verringern`}>
|
||||
−
|
||||
</button>
|
||||
<input
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
min={control.min}
|
||||
max={control.max}
|
||||
value={control.value}
|
||||
onChange={control.onChange}
|
||||
onBlur={control.onBlur}
|
||||
onKeyDown={control.onKeyDown}
|
||||
aria-label={control.label}
|
||||
/>
|
||||
<button type="button" className="stepper-button" onClick={control.increment} aria-label={`${control.label} erhoehen`}>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<div className="settings-presets" aria-label={`${control.label} Presets`}>
|
||||
{control.presets.map((value) => (
|
||||
<button type="button" key={value} onClick={() => control.applyPreset(value)}>
|
||||
{value}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,21 +33,24 @@ export function useScanDetailsModalModel({
|
||||
|
||||
const crops = latestCapture?.crops ?? [];
|
||||
const ocr = latestCapture?.ocr ?? [];
|
||||
const cropRows = crops
|
||||
.filter((crop) => Boolean(crop.dataUrl))
|
||||
.map((crop) => ({
|
||||
id: crop.id,
|
||||
dataUrl: crop.dataUrl ?? "",
|
||||
label: crop.label,
|
||||
x: crop.rect.x,
|
||||
y: crop.rect.y,
|
||||
width: crop.rect.width,
|
||||
height: crop.rect.height,
|
||||
}));
|
||||
|
||||
return {
|
||||
closeDetails,
|
||||
stopPropagation,
|
||||
parsedNotes,
|
||||
showParsedNotes: parsedNotes.length > 0,
|
||||
cropRows: crops.map((crop) => ({
|
||||
id: crop.id,
|
||||
dataUrl: crop.dataUrl,
|
||||
label: crop.label,
|
||||
x: crop.rect.x,
|
||||
y: crop.rect.y,
|
||||
width: crop.rect.width,
|
||||
height: crop.rect.height,
|
||||
})),
|
||||
cropRows,
|
||||
ocrRows: ocr.map((entry) => ({
|
||||
id: entry.id,
|
||||
label: entry.label,
|
||||
@@ -55,7 +58,7 @@ export function useScanDetailsModalModel({
|
||||
text: entry.text || "No text detected",
|
||||
})),
|
||||
debugText: `Debug: crops ${crops.length} / ocr ${ocr.length}`,
|
||||
showCrops: crops.length > 0,
|
||||
showCrops: cropRows.length > 0,
|
||||
showOcr: ocr.length > 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
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;
|
||||
@@ -37,6 +39,7 @@ export interface ScanDiagnosticsModalModel {
|
||||
showDevRows: boolean;
|
||||
reviewStatus: string;
|
||||
automationLogLines: string[];
|
||||
diagnosticEvents: ScanDiagnosticEvent[];
|
||||
canSaveReviewSample: boolean;
|
||||
}
|
||||
|
||||
@@ -98,7 +101,11 @@ export function useScanDiagnosticsModalModel({
|
||||
|
||||
const learningRulesText = controller.learningRulesLoaded ? `${controller.learningRuleCount} local rules` : "loading";
|
||||
const dataStaleness = dataPackageStatus(dataGeneratedAt, sourceVersion);
|
||||
const learningRulesSubtext = `Review samples feed deterministic OCR fixes before each new parse. Data package: ${sourceVersion}${dataStaleness.warning ? ` - ${dataStaleness.warning}` : ""}`;
|
||||
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(
|
||||
@@ -128,6 +135,25 @@ export function useScanDiagnosticsModalModel({
|
||||
{ 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: "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,
|
||||
@@ -139,6 +165,25 @@ export function useScanDiagnosticsModalModel({
|
||||
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.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,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -186,6 +231,7 @@ export function useScanDiagnosticsModalModel({
|
||||
showDevRows: controller.devMode,
|
||||
reviewStatus: controller.reviewStatus,
|
||||
automationLogLines: controller.automationLog,
|
||||
diagnosticEvents: controller.diagnosticEvents,
|
||||
canSaveReviewSample: canSaveReviewSample && Boolean(controller.parsedArtifact),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,12 +1,27 @@
|
||||
import { useCallback, type ChangeEvent, type MouseEvent } from "react";
|
||||
import { useCallback, useEffect, useState, type ChangeEvent, type FocusEvent, type KeyboardEvent, type MouseEvent } from "react";
|
||||
import type { CaptureResult, RuntimeInfo } from "../../../../../types/global";
|
||||
import { clampScanLimit, clampSkipRows } from "../../../../../lib/scannerSession";
|
||||
|
||||
export interface StepperControlModel {
|
||||
label: string;
|
||||
value: string;
|
||||
helper: string;
|
||||
min: number;
|
||||
max: number;
|
||||
presets: number[];
|
||||
onChange: (event: ChangeEvent<HTMLInputElement>) => void;
|
||||
onBlur: (event: FocusEvent<HTMLInputElement>) => void;
|
||||
onKeyDown: (event: KeyboardEvent<HTMLInputElement>) => void;
|
||||
decrement: () => void;
|
||||
increment: () => void;
|
||||
applyPreset: (value: number) => void;
|
||||
}
|
||||
|
||||
export interface ScanSettingsModalModel {
|
||||
closeSettings: () => void;
|
||||
handleScanLimitChange: (event: ChangeEvent<HTMLInputElement>) => void;
|
||||
handleSkipRowsChange: (event: ChangeEvent<HTMLInputElement>) => void;
|
||||
stopPropagation: (event: MouseEvent<HTMLDivElement>) => void;
|
||||
scanLimitControl: StepperControlModel;
|
||||
skipRowsControl: StepperControlModel;
|
||||
inventoryCountText: string;
|
||||
inventoryClassName: string;
|
||||
scanLimitClassName: string;
|
||||
@@ -36,17 +51,71 @@ export function useScanSettingsModalModel({
|
||||
setScanLimitTouched: (touched: boolean) => void;
|
||||
setSkipRows: (rows: number) => void;
|
||||
}): ScanSettingsModalModel {
|
||||
const [scanLimitText, setScanLimitText] = useState(String(scanLimit));
|
||||
const [skipRowsText, setSkipRowsText] = useState(String(skipRows));
|
||||
|
||||
useEffect(() => {
|
||||
setScanLimitText(String(scanLimit));
|
||||
}, [scanLimit]);
|
||||
|
||||
useEffect(() => {
|
||||
setSkipRowsText(String(skipRows));
|
||||
}, [skipRows]);
|
||||
|
||||
const closeSettings = useCallback(() => setSettingsOpen(false), [setSettingsOpen]);
|
||||
const handleScanLimitChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||
setScanLimitTouched(true);
|
||||
setScanLimit(clampScanLimit(Number(event.target.value)));
|
||||
}, [setScanLimit, setScanLimitTouched]);
|
||||
const handleSkipRowsChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||
setSkipRows(clampSkipRows(Number(event.target.value)));
|
||||
}, [setSkipRows]);
|
||||
const stopPropagation = useCallback((event: MouseEvent<HTMLDivElement>) => {
|
||||
event.stopPropagation();
|
||||
}, []);
|
||||
|
||||
const applyScanLimit = useCallback((value: number) => {
|
||||
const next = clampScanLimit(value);
|
||||
setScanLimitTouched(true);
|
||||
setScanLimit(next);
|
||||
setScanLimitText(String(next));
|
||||
}, [setScanLimit, setScanLimitTouched]);
|
||||
|
||||
const applySkipRows = useCallback((value: number) => {
|
||||
const next = clampSkipRows(value);
|
||||
setSkipRows(next);
|
||||
setSkipRowsText(String(next));
|
||||
}, [setSkipRows]);
|
||||
|
||||
const handleScanLimitChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||
setScanLimitText(event.target.value.replace(/\D/g, "").slice(0, 4));
|
||||
}, []);
|
||||
|
||||
const handleSkipRowsChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||
setSkipRowsText(event.target.value.replace(/\D/g, "").slice(0, 2));
|
||||
}, []);
|
||||
|
||||
const commitScanLimit = useCallback((rawValue: string) => {
|
||||
applyScanLimit(rawValue.trim() === "" ? scanLimit : Number(rawValue));
|
||||
}, [applyScanLimit, scanLimit]);
|
||||
|
||||
const commitSkipRows = useCallback((rawValue: string) => {
|
||||
applySkipRows(rawValue.trim() === "" ? skipRows : Number(rawValue));
|
||||
}, [applySkipRows, skipRows]);
|
||||
|
||||
const handleScanLimitBlur = useCallback((event: FocusEvent<HTMLInputElement>) => {
|
||||
commitScanLimit(event.target.value);
|
||||
}, [commitScanLimit]);
|
||||
|
||||
const handleSkipRowsBlur = useCallback((event: FocusEvent<HTMLInputElement>) => {
|
||||
commitSkipRows(event.target.value);
|
||||
}, [commitSkipRows]);
|
||||
|
||||
const handleScanLimitKeyDown = useCallback((event: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key !== "Enter") return;
|
||||
commitScanLimit(event.currentTarget.value);
|
||||
event.currentTarget.blur();
|
||||
}, [commitScanLimit]);
|
||||
|
||||
const handleSkipRowsKeyDown = useCallback((event: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key !== "Enter") return;
|
||||
commitSkipRows(event.currentTarget.value);
|
||||
event.currentTarget.blur();
|
||||
}, [commitSkipRows]);
|
||||
|
||||
const detectedInventoryCount = latestCapture?.inventoryCount?.current;
|
||||
const detectedInventoryTotal = latestCapture?.inventoryCount?.total;
|
||||
const inventoryClassName = detectedInventoryCount ? "ok" : "neutral";
|
||||
@@ -56,14 +125,40 @@ export function useScanSettingsModalModel({
|
||||
const scanLimitClassName = scanLimit !== detectedInventoryCount ? "ok" : "standard";
|
||||
const skipRowsClassName = skipRows > 0 ? "ok" : "standard";
|
||||
const focusModeText = runtimeInfo?.isElevated ? "Admin" : "Standard";
|
||||
const activeTargetText = `Aktive Zielvorgabe ${activeTargetCount} und Fokusmodus ${focusModeText}.`;
|
||||
const scanSummaryText = "Der Auto-Scan zaehlt \"Positionen\" und \"verified\", bevor die Datenbank in den Save-Pfad laeuft. So wird \"scanned\" nicht mit \"erfolgreich gespeichert\" verwechselt.";
|
||||
const activeTargetText = `${activeTargetCount} Ziele · ${focusModeText}`;
|
||||
const scanSummaryText = "Auto-Scan zaehlt Positionen, verifizierte Ansichten und gespeicherte Artifacts getrennt.";
|
||||
|
||||
return {
|
||||
closeSettings,
|
||||
handleScanLimitChange,
|
||||
handleSkipRowsChange,
|
||||
stopPropagation,
|
||||
scanLimitControl: {
|
||||
label: "Scan-Ziel",
|
||||
value: scanLimitText,
|
||||
helper: "Wie viele sichtbare Positionen verarbeitet werden.",
|
||||
min: 1,
|
||||
max: 1800,
|
||||
presets: [16, 20, 50, 100],
|
||||
onChange: handleScanLimitChange,
|
||||
onBlur: handleScanLimitBlur,
|
||||
onKeyDown: handleScanLimitKeyDown,
|
||||
decrement: () => applyScanLimit(scanLimit - 1),
|
||||
increment: () => applyScanLimit(scanLimit + 1),
|
||||
applyPreset: applyScanLimit,
|
||||
},
|
||||
skipRowsControl: {
|
||||
label: "Startzeilen ueberspringen",
|
||||
value: skipRowsText,
|
||||
helper: "Nur nutzen, wenn du mitten in der Liste beginnst.",
|
||||
min: 0,
|
||||
max: 8,
|
||||
presets: [0, 1, 2, 3],
|
||||
onChange: handleSkipRowsChange,
|
||||
onBlur: handleSkipRowsBlur,
|
||||
onKeyDown: handleSkipRowsKeyDown,
|
||||
decrement: () => applySkipRows(skipRows - 1),
|
||||
increment: () => applySkipRows(skipRows + 1),
|
||||
applyPreset: applySkipRows,
|
||||
},
|
||||
inventoryCountText,
|
||||
inventoryClassName,
|
||||
scanLimitClassName,
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { ScannerLearningRules } from "../../../lib/scannerLearning";
|
||||
import type { ParsedArtifactCandidate } from "../../../lib/artifactOcrParser";
|
||||
import type { AutoScanStats, ScanSummary } from "../../../lib/scannerSession";
|
||||
import type { ScanActionContext } from "./scanViewScanActions";
|
||||
import type { createScanDiagnosticEvent } from "../../../lib/scanDiagnosticsLog";
|
||||
import type {
|
||||
ReviewStateContext,
|
||||
} from "./scanViewReviewHelpers";
|
||||
@@ -42,6 +43,7 @@ export interface ScanActionContextInput {
|
||||
setReviewStatus: Dispatch<SetStateAction<string>>;
|
||||
appendAutomationLog: (line: string) => void;
|
||||
appendClickDiagnostics: (result: ClickResult, prefix?: string) => void;
|
||||
appendDiagnosticEvent: (event: Omit<Parameters<typeof createScanDiagnosticEvent>[0], "includeFullScreenshot">) => void;
|
||||
parseArtifact: (capture: CaptureResult | null) => ParsedArtifactCandidate | null;
|
||||
persistParsedArtifact: (
|
||||
capture: CaptureResult | null,
|
||||
@@ -60,7 +62,11 @@ export interface ScanActionContextInput {
|
||||
focusGenshin?: boolean,
|
||||
options?: CaptureOptions,
|
||||
) => Promise<CaptureResult | null>;
|
||||
captureFastSelectedSource: (delayMs?: number, focusGenshin?: boolean) => Promise<CaptureResult | null>;
|
||||
captureFastSelectedSource: (
|
||||
delayMs?: number,
|
||||
focusGenshin?: boolean,
|
||||
options?: CaptureOptions,
|
||||
) => Promise<CaptureResult | null>;
|
||||
}
|
||||
|
||||
export function createReviewContext(input: ReviewStateContextInput): ReviewStateContext {
|
||||
@@ -98,6 +104,7 @@ export function createScanActionContext(input: ScanActionContextInput): ScanActi
|
||||
setReviewStatus: input.setReviewStatus,
|
||||
appendAutomationLog: input.appendAutomationLog,
|
||||
appendClickDiagnostics: input.appendClickDiagnostics,
|
||||
appendDiagnosticEvent: input.appendDiagnosticEvent,
|
||||
parseArtifact: input.parseArtifact,
|
||||
persistParsedArtifact: input.persistParsedArtifact,
|
||||
shouldFlagArtifactForReview: (parsed) => (parsed ? shouldFlagArtifactForReview(parsed) : false),
|
||||
|
||||
@@ -277,29 +277,52 @@ export async function saveReviewSample(
|
||||
}
|
||||
if (!capture) return { result: null, recoveredParsed: null, recoveredToDb: false };
|
||||
|
||||
const compactAutomaticSample = /^automatic:/i.test(reason);
|
||||
const sampleCapture = compactAutomaticSample
|
||||
? {
|
||||
id: capture.id,
|
||||
name: capture.name,
|
||||
width: capture.width,
|
||||
height: capture.height,
|
||||
detailDataUrl: capture.detailDataUrl,
|
||||
captureTarget: capture.captureTarget,
|
||||
capturedAt: capture.capturedAt,
|
||||
crops: capture.crops?.map((crop: NonNullable<CaptureResult["crops"]>[number]) => ({
|
||||
id: crop.id,
|
||||
label: crop.label,
|
||||
rect: crop.rect,
|
||||
dataUrl: crop.dataUrl,
|
||||
})),
|
||||
inventoryGrid: capture.inventoryGrid,
|
||||
inventoryCount: capture.inventoryCount,
|
||||
locked: capture.locked,
|
||||
ocr: capture.ocr,
|
||||
}
|
||||
: {
|
||||
id: capture.id,
|
||||
name: capture.name,
|
||||
width: capture.width,
|
||||
height: capture.height,
|
||||
dataUrl: capture.dataUrl,
|
||||
detailDataUrl: capture.detailDataUrl,
|
||||
inventoryDataUrl: capture.inventoryDataUrl,
|
||||
captureTarget: capture.captureTarget,
|
||||
capturedAt: capture.capturedAt,
|
||||
crops: capture.crops?.map((crop: NonNullable<CaptureResult["crops"]>[number]) => ({
|
||||
id: crop.id,
|
||||
label: crop.label,
|
||||
rect: crop.rect,
|
||||
dataUrl: crop.dataUrl,
|
||||
})),
|
||||
inventoryGrid: capture.inventoryGrid,
|
||||
inventoryCount: capture.inventoryCount,
|
||||
locked: capture.locked,
|
||||
ocr: capture.ocr,
|
||||
};
|
||||
|
||||
const result = await reviewSamplesRepo.saveSample({
|
||||
reason,
|
||||
capture: {
|
||||
id: capture.id,
|
||||
name: capture.name,
|
||||
width: capture.width,
|
||||
height: capture.height,
|
||||
dataUrl: capture.dataUrl,
|
||||
detailDataUrl: capture.detailDataUrl,
|
||||
inventoryDataUrl: capture.inventoryDataUrl,
|
||||
captureTarget: capture.captureTarget,
|
||||
capturedAt: capture.capturedAt,
|
||||
crops: capture.crops?.map((crop: NonNullable<CaptureResult["crops"]>[number]) => ({
|
||||
id: crop.id,
|
||||
label: crop.label,
|
||||
rect: crop.rect,
|
||||
dataUrl: crop.dataUrl,
|
||||
})),
|
||||
inventoryGrid: capture.inventoryGrid,
|
||||
inventoryCount: capture.inventoryCount,
|
||||
locked: capture.locked,
|
||||
ocr: capture.ocr,
|
||||
},
|
||||
capture: sampleCapture,
|
||||
parsed,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { automationBlockReason, requiresAdminForAutomation } from "../../../lib/automationPlanner";
|
||||
import { artifactTabClickTarget, keyPressBlocked, validateAutoScanEntryPreflight, type ScanEntryMode } from "../../../lib/autoScanEntry";
|
||||
import { captureRejectionReason } from "../../../lib/scannerCaptureQuality";
|
||||
import { runAutoScanLoop } from "../../../lib/autoScanLoop";
|
||||
import { clampScanLimit, emptyAutoScanStats, resolveScanTargetCount, type AutoScanStats, type ScanSummary } from "../../../lib/scannerSession";
|
||||
import { addCaptureTiming, clampScanLimit, emptyAutoScanStats, resolveScanTargetCount, updateScanTiming, type AutoScanStats, type ScanSummary } from "../../../lib/scannerSession";
|
||||
import { getAutoReviewReason, wait } from "../../../lib/scanReviewUtils";
|
||||
import { summarizeClickResult, summarizeKeyPressResult, type createScanDiagnosticEvent } from "../../../lib/scanDiagnosticsLog";
|
||||
import type { AutomationRepositoryPort, RuntimeRepositoryPort } from "../../../infrastructure/repositories/rendererBridgeRepositories";
|
||||
import type {
|
||||
AutomationGuard,
|
||||
@@ -36,8 +38,9 @@ export interface ScanActionContext {
|
||||
setReviewStatus: Dispatch<SetStateAction<string>>;
|
||||
appendAutomationLog: (line: string) => void;
|
||||
appendClickDiagnostics: (result: ClickResult, prefix?: string) => void;
|
||||
appendDiagnosticEvent: (event: Omit<Parameters<typeof createScanDiagnosticEvent>[0], "includeFullScreenshot">) => void;
|
||||
captureSelectedSource: (delayMs?: number, focusGenshin?: boolean, options?: CaptureOptions) => Promise<CaptureResult | null>;
|
||||
captureFastSelectedSource: (delayMs?: number, focusGenshin?: boolean) => Promise<CaptureResult | null>;
|
||||
captureFastSelectedSource: (delayMs?: number, focusGenshin?: boolean, options?: CaptureOptions) => Promise<CaptureResult | null>;
|
||||
parseArtifact: (capture: CaptureResult | null) => ParsedArtifactCandidate | null;
|
||||
persistParsedArtifact: (capture: CaptureResult | null, parsed: ParsedArtifactCandidate, source: string, needsReview: boolean) => Promise<boolean>;
|
||||
saveReviewSample: (capture: CaptureResult | null, parsed: ParsedArtifactCandidate | null, reason?: string) => Promise<BooleanResult | null>;
|
||||
@@ -47,6 +50,9 @@ export interface ScanActionContext {
|
||||
|
||||
export interface VisibleGridScanOptions {
|
||||
scanLimit?: number;
|
||||
scanEntryMode?: ScanEntryMode;
|
||||
processInitialSelection?: boolean;
|
||||
ocrEngine?: CaptureOptions["ocrEngine"];
|
||||
}
|
||||
|
||||
function buildScanSignature(parsed: ParsedArtifactCandidate) {
|
||||
@@ -83,6 +89,11 @@ export async function runAutoReviewScan(context: ScanActionContext): Promise<voi
|
||||
|
||||
const seen = new Set<string>();
|
||||
const stats: AutoScanStats = { ...emptyAutoScanStats, pages: 1 };
|
||||
const startedAt = Date.now();
|
||||
const updateManualStats = () => {
|
||||
updateScanTiming(stats, startedAt);
|
||||
setAutoScanStats({ ...stats });
|
||||
};
|
||||
let idleTicks = 0;
|
||||
const maxArtifacts = resolveScanTargetCount(scanLimit, detectedInventoryCount);
|
||||
const maxIdleTicks = 90;
|
||||
@@ -96,7 +107,7 @@ export async function runAutoReviewScan(context: ScanActionContext): Promise<voi
|
||||
if (capture && rejection) {
|
||||
await saveReviewSample(capture, parsed, `manual:capture-rejected`);
|
||||
stats.review++;
|
||||
setAutoScanStats({ ...stats });
|
||||
updateManualStats();
|
||||
}
|
||||
idleTicks++;
|
||||
setReviewStatus(`Manueller Scan wartet auf ein lesbares Artifact... (${stats.parsed}/${maxArtifacts})${rejection ? ` ${rejection}` : ""}`);
|
||||
@@ -117,6 +128,7 @@ export async function runAutoReviewScan(context: ScanActionContext): Promise<voi
|
||||
stats.attempted++;
|
||||
stats.verified++;
|
||||
stats.parsed++;
|
||||
addCaptureTiming(stats, capture.timings);
|
||||
|
||||
const reason = getAutoReviewReason(capture, parsed);
|
||||
const needsReview = shouldFlagArtifactForReview(parsed);
|
||||
@@ -127,12 +139,13 @@ export async function runAutoReviewScan(context: ScanActionContext): Promise<voi
|
||||
if (await persistParsedArtifact(capture, parsed, "manual-scan", needsReview)) {
|
||||
stats.stored++;
|
||||
}
|
||||
setAutoScanStats({ ...stats });
|
||||
updateManualStats();
|
||||
setReviewStatus(`Manueller Scan: neues Artifact erkannt (${stats.parsed}/${maxArtifacts}). Klicke das naechste Artifact an oder druecke Stop.`);
|
||||
await wait(700);
|
||||
}
|
||||
|
||||
setAutoScanRunning(false);
|
||||
updateScanTiming(stats, startedAt);
|
||||
const status: ScanSummary["status"] = stopVisibleScanRef.current ? "stopped" : "done";
|
||||
const idleSuffix = idleTicks >= maxIdleTicks ? " Keine neuen Artifacts erkannt; manueller Scan beendet." : "";
|
||||
await focusDashboard();
|
||||
@@ -165,6 +178,7 @@ export async function runVisibleGridScan(context: ScanActionContext, options: Vi
|
||||
setReviewStatus,
|
||||
appendAutomationLog,
|
||||
appendClickDiagnostics,
|
||||
appendDiagnosticEvent,
|
||||
captureSelectedSource,
|
||||
captureFastSelectedSource,
|
||||
parseArtifact,
|
||||
@@ -177,8 +191,14 @@ export async function runVisibleGridScan(context: ScanActionContext, options: Vi
|
||||
focusDashboard,
|
||||
} = context;
|
||||
const scanLimit = typeof options.scanLimit === "number" ? clampScanLimit(options.scanLimit) : configuredScanLimit;
|
||||
const scanEntryMode = options.scanEntryMode ?? "visible-inventory";
|
||||
const ocrEngine = options.ocrEngine === "ik-traineddata" ? "ik-traineddata" : "current";
|
||||
|
||||
if (autoScanRunning || !bridgeReady || !selectedSourceId || !automationRepo?.focusGenshinForScanStart || !automationRepo?.focusGenshin || !automationRepo?.clickScreen || !automationRepo?.scrollScreen) return;
|
||||
if (scanEntryMode !== "visible-inventory" && !automationRepo.keyPress) {
|
||||
setReviewStatus("Auto-Scan-Einstieg ist nicht verfuegbar: Keypress-Bridge fehlt.");
|
||||
return;
|
||||
}
|
||||
|
||||
const requiresAdminForAutoScan = requiresAdminForAutomation(runtimeInfo);
|
||||
if (requiresAdminForAutoScan) {
|
||||
@@ -200,6 +220,12 @@ export async function runVisibleGridScan(context: ScanActionContext, options: Vi
|
||||
setScanSummary(null);
|
||||
setAutoScanStats(emptyAutoScanStats);
|
||||
setReviewStatus("Automatischer Scan startet. ESC gedrueckt halten oder Stop klicken beendet den Scan sofort.");
|
||||
appendDiagnosticEvent({
|
||||
phase: "scan-start",
|
||||
severity: "info",
|
||||
message: `Auto-scan start requested (${scanEntryMode}, ${ocrEngine})`,
|
||||
details: { scanLimit, skipRows, detectedInventoryCount, ocrEngine },
|
||||
});
|
||||
|
||||
const freshRuntime = await runtimeRepo?.getRuntimeInfo().catch(() => null);
|
||||
const adminBlockReason = automationBlockReason(freshRuntime);
|
||||
@@ -207,6 +233,12 @@ export async function runVisibleGridScan(context: ScanActionContext, options: Vi
|
||||
setAutoScanRunning(false);
|
||||
setReviewStatus(adminBlockReason);
|
||||
appendAutomationLog("blocked: App laeuft nicht als Administrator, keine In-Game-Klicks ausgefuehrt");
|
||||
appendDiagnosticEvent({
|
||||
phase: "preflight",
|
||||
severity: "error",
|
||||
message: adminBlockReason,
|
||||
details: { elevated: freshRuntime?.isElevated, genshinFound: freshRuntime?.genshinFound },
|
||||
});
|
||||
setScanSummary({
|
||||
mode: "Automatischer Scan",
|
||||
status: "blocked",
|
||||
@@ -220,6 +252,17 @@ export async function runVisibleGridScan(context: ScanActionContext, options: Vi
|
||||
if (freshRuntime) {
|
||||
const required = freshRuntime.genshinFound ? `found:${freshRuntime.targetProcess || "genshin"}` : "not-found";
|
||||
appendAutomationLog(`runtime ping: elevated=${freshRuntime.isElevated} ${required}`);
|
||||
appendDiagnosticEvent({
|
||||
phase: "runtime",
|
||||
severity: freshRuntime.genshinFound ? "ok" : "warn",
|
||||
message: `Runtime ping: ${required}`,
|
||||
details: {
|
||||
elevated: freshRuntime.isElevated,
|
||||
foreground: freshRuntime.foregroundProcess,
|
||||
target: freshRuntime.targetProcess,
|
||||
helperPid: freshRuntime.helperPid,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const focusGenshinForScanStart = automationRepo.focusGenshinForScanStart ?? automationRepo.focusGenshin;
|
||||
@@ -234,6 +277,18 @@ export async function runVisibleGridScan(context: ScanActionContext, options: Vi
|
||||
appendAutomationLog(
|
||||
`focus attempt ${attempt}/3: ${current.focused ? "ok" : "failed"} found:${current.genshinFound ? "yes" : "no"} setForeground:${current.setForegroundResult ?? "n/a"} target:${current.targetProcess || "?"} fg:${current.foregroundProcess || "?"}`,
|
||||
);
|
||||
appendDiagnosticEvent({
|
||||
phase: "focus",
|
||||
severity: current.focused ? "ok" : "warn",
|
||||
message: `Focus attempt ${attempt}/3 ${current.focused ? "succeeded" : "failed"}`,
|
||||
details: {
|
||||
found: current.genshinFound,
|
||||
setForeground: current.setForegroundResult,
|
||||
target: current.targetProcess,
|
||||
foreground: current.foregroundProcess,
|
||||
alreadyForeground: current.alreadyForeground,
|
||||
},
|
||||
});
|
||||
if (current.focused) break;
|
||||
if (!current.genshinFound) break;
|
||||
}
|
||||
@@ -246,6 +301,16 @@ export async function runVisibleGridScan(context: ScanActionContext, options: Vi
|
||||
? "Genshin-Prozess wurde nicht gefunden. Bitte pruefen, ob Genshin laeuft, und Auto-Scan erneut starten."
|
||||
: "Genshin konnte nicht in den Vordergrund geholt werden. Bitte Genshin manuell anklicken/fokussieren und Auto-Scan erneut starten.";
|
||||
setReviewStatus(reason);
|
||||
appendDiagnosticEvent({
|
||||
phase: "focus",
|
||||
severity: "error",
|
||||
message: reason,
|
||||
details: {
|
||||
found: focusResult?.genshinFound,
|
||||
target: focusResult?.targetProcess,
|
||||
foreground: focusResult?.foregroundProcess,
|
||||
},
|
||||
});
|
||||
setScanSummary({
|
||||
mode: "Automatischer Scan",
|
||||
status: "blocked",
|
||||
@@ -256,7 +321,46 @@ export async function runVisibleGridScan(context: ScanActionContext, options: Vi
|
||||
return;
|
||||
}
|
||||
|
||||
setReviewStatus("Genshin ist im Vordergrund. Automatischer Scan startet. ESC gedrueckt halten oder Stop klicken beendet den Scan sofort.");
|
||||
setReviewStatus(scanEntryMode !== "visible-inventory"
|
||||
? "Genshin ist im Vordergrund. Oeffne Artifact-Inventar und warte auf Detailkarte..."
|
||||
: "Genshin ist im Vordergrund. Automatischer Scan startet. ESC gedrueckt halten oder Stop klicken beendet den Scan sofort.");
|
||||
|
||||
const entryCapture = await prepareAutoScanEntry({
|
||||
mode: scanEntryMode,
|
||||
automationRepo,
|
||||
captureFastSelectedSource,
|
||||
appendAutomationLog,
|
||||
appendDiagnosticEvent,
|
||||
});
|
||||
const entryPreflight = validateAutoScanEntryPreflight(entryCapture);
|
||||
if (!entryPreflight.ok) {
|
||||
setAutoScanRunning(false);
|
||||
setReviewStatus(entryPreflight.reason);
|
||||
appendAutomationLog(`entry blocked: ${entryPreflight.reason}`);
|
||||
appendDiagnosticEvent({
|
||||
phase: "entry-preflight",
|
||||
severity: "error",
|
||||
message: entryPreflight.reason,
|
||||
capture: entryCapture,
|
||||
});
|
||||
setScanSummary({
|
||||
mode: scanEntryMode === "visible-inventory" ? "Automatischer Scan" : `Automatischer Scan (${scanEntryMode})`,
|
||||
status: "blocked",
|
||||
...emptyAutoScanStats,
|
||||
targetCount: resolveScanTargetCount(scanLimit, detectedInventoryCount),
|
||||
gridLabel: entryPreflight.reason,
|
||||
});
|
||||
await focusDashboard();
|
||||
return;
|
||||
}
|
||||
|
||||
setReviewStatus("Artifact-Inventar ist bereit. Automatischer Scan startet. ESC gedrueckt halten oder Stop klicken beendet den Scan sofort.");
|
||||
appendDiagnosticEvent({
|
||||
phase: "entry-preflight",
|
||||
severity: "ok",
|
||||
message: "Artifact inventory preflight passed.",
|
||||
capture: entryCapture,
|
||||
});
|
||||
|
||||
const result = await runAutoScanLoop(
|
||||
{
|
||||
@@ -285,7 +389,7 @@ export async function runVisibleGridScan(context: ScanActionContext, options: Vi
|
||||
automationRepo?.getAutomationGuard?.() ??
|
||||
Promise.resolve<AutomationGuard>({ ok: false, escapePressed: false, enterPressed: false, f9Pressed: false }),
|
||||
},
|
||||
captureSelectedSource: (delayMs = 0, focusGenshin = false) => captureSelectedSource(delayMs, focusGenshin),
|
||||
captureSelectedSource: (delayMs = 0, focusGenshin = false, options) => captureSelectedSource(delayMs, focusGenshin, options),
|
||||
captureFastSelectedSource,
|
||||
parseArtifact,
|
||||
persistParsedArtifact,
|
||||
@@ -302,6 +406,9 @@ export async function runVisibleGridScan(context: ScanActionContext, options: Vi
|
||||
scanLimit,
|
||||
skipRows,
|
||||
detectedInventoryCount,
|
||||
processInitialSelection: options.processInitialSelection ?? scanEntryMode !== "visible-inventory",
|
||||
skipInitialGridTarget: scanEntryMode !== "visible-inventory",
|
||||
ocrEngine,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -310,10 +417,242 @@ export async function runVisibleGridScan(context: ScanActionContext, options: Vi
|
||||
await focusDashboard();
|
||||
setReviewStatus(`Automatischer Scan ${result.status === "stopped" ? "gestoppt" : result.status === "blocked" ? "blockiert" : "fertig"}. ${result.stats.clicked} Klicks, ${result.stats.attempted} Positionen bearbeitet, ${result.stats.verified} Ansichten verifiziert, ${result.stats.parsed} gelesen, ${result.stats.stored} in der Datenbank, ${result.stats.review} Review-Samples, ${result.stats.duplicates} Duplikate, ${result.stats.misses} Misses.${result.blockedReason ? ` ${result.blockedReason}` : ""}`);
|
||||
setScanSummary({
|
||||
mode: "Automatischer Scan",
|
||||
mode: scanEntryMode === "visible-inventory" ? `Automatischer Scan [${ocrEngine}]` : `Automatischer Scan (${scanEntryMode}) [${ocrEngine}]`,
|
||||
status: result.status,
|
||||
...result.stats,
|
||||
targetCount: result.targetCount,
|
||||
gridLabel: result.gridLabel,
|
||||
});
|
||||
}
|
||||
|
||||
async function prepareAutoScanEntry({
|
||||
mode,
|
||||
automationRepo,
|
||||
captureFastSelectedSource,
|
||||
appendAutomationLog,
|
||||
appendDiagnosticEvent,
|
||||
}: {
|
||||
mode: ScanEntryMode;
|
||||
automationRepo: AutomationRepositoryPort;
|
||||
captureFastSelectedSource: (delayMs?: number, focusGenshin?: boolean, options?: CaptureOptions) => Promise<CaptureResult | null>;
|
||||
appendAutomationLog: (line: string) => void;
|
||||
appendDiagnosticEvent: (event: Omit<Parameters<typeof createScanDiagnosticEvent>[0], "includeFullScreenshot">) => void;
|
||||
}) {
|
||||
if (mode === "visible-inventory") {
|
||||
const capture = await captureFastSelectedSource(0, true);
|
||||
appendDiagnosticEvent({
|
||||
phase: "entry-visible",
|
||||
severity: capture ? "ok" : "error",
|
||||
message: capture ? "Visible inventory preflight capture ready." : "Visible inventory preflight capture failed.",
|
||||
capture,
|
||||
});
|
||||
return capture;
|
||||
}
|
||||
|
||||
if (mode === "direct-inventory") {
|
||||
return tryInventoryEntrySequence({
|
||||
label: "direct",
|
||||
sendEscapeFirst: false,
|
||||
automationRepo,
|
||||
captureFastSelectedSource,
|
||||
appendAutomationLog,
|
||||
appendDiagnosticEvent,
|
||||
});
|
||||
}
|
||||
|
||||
if (mode === "auto-entry") {
|
||||
const directCapture = await tryInventoryEntrySequence({
|
||||
label: "auto-direct",
|
||||
sendEscapeFirst: false,
|
||||
automationRepo,
|
||||
captureFastSelectedSource,
|
||||
appendAutomationLog,
|
||||
appendDiagnosticEvent,
|
||||
});
|
||||
const directPreflight = validateAutoScanEntryPreflight(directCapture);
|
||||
if (directPreflight.ok) return directCapture;
|
||||
appendAutomationLog(`auto-entry direct path failed: ${directPreflight.reason}`);
|
||||
appendDiagnosticEvent({
|
||||
phase: "entry-fallback",
|
||||
severity: "info",
|
||||
message: `Direct inventory entry did not reach an artifact detail card. Trying IK fallback. ${directPreflight.reason}`,
|
||||
capture: directCapture,
|
||||
});
|
||||
}
|
||||
|
||||
return tryInventoryEntrySequence({
|
||||
label: "paimon",
|
||||
sendEscapeFirst: true,
|
||||
automationRepo,
|
||||
captureFastSelectedSource,
|
||||
appendAutomationLog,
|
||||
appendDiagnosticEvent,
|
||||
});
|
||||
}
|
||||
|
||||
async function tryInventoryEntrySequence({
|
||||
label,
|
||||
sendEscapeFirst,
|
||||
automationRepo,
|
||||
captureFastSelectedSource,
|
||||
appendAutomationLog,
|
||||
appendDiagnosticEvent,
|
||||
}: {
|
||||
label: string;
|
||||
sendEscapeFirst: boolean;
|
||||
automationRepo: AutomationRepositoryPort;
|
||||
captureFastSelectedSource: (delayMs?: number, focusGenshin?: boolean, options?: CaptureOptions) => Promise<CaptureResult | null>;
|
||||
appendAutomationLog: (line: string) => void;
|
||||
appendDiagnosticEvent: (event: Omit<Parameters<typeof createScanDiagnosticEvent>[0], "includeFullScreenshot">) => void;
|
||||
}) {
|
||||
if (sendEscapeFirst) {
|
||||
const escapeResult = await automationRepo.keyPress?.("ESC");
|
||||
appendAutomationLog(`${label} entry key ESC: ${escapeResult?.ok ? "ok" : "blocked"}`);
|
||||
appendDiagnosticEvent({
|
||||
phase: "entry-key",
|
||||
severity: keyPressBlocked(escapeResult) ? "error" : "ok",
|
||||
message: `${label} entry key ESC`,
|
||||
details: summarizeKeyPressResult(escapeResult),
|
||||
});
|
||||
if (keyPressBlocked(escapeResult)) return null;
|
||||
const menuProbe = await waitForEntryCapture({
|
||||
captureFastSelectedSource,
|
||||
timeoutMs: 750,
|
||||
predicate: (capture) => Boolean(capture),
|
||||
});
|
||||
appendDiagnosticEvent({
|
||||
phase: "entry-capture",
|
||||
severity: menuProbe ? "info" : "warn",
|
||||
message: `${label} capture after ESC step.`,
|
||||
capture: menuProbe,
|
||||
});
|
||||
if (menuProbe?.paimonMenu?.present) {
|
||||
const closeMenuResult = await automationRepo.keyPress?.("ESC");
|
||||
appendAutomationLog(`${label} entry key ESC close menu: ${closeMenuResult?.ok ? "ok" : "blocked"}`);
|
||||
appendDiagnosticEvent({
|
||||
phase: "entry-key",
|
||||
severity: keyPressBlocked(closeMenuResult) ? "error" : "ok",
|
||||
message: `${label} entry key ESC close menu`,
|
||||
details: summarizeKeyPressResult(closeMenuResult),
|
||||
});
|
||||
if (keyPressBlocked(closeMenuResult)) return menuProbe;
|
||||
const worldProbe = await waitForEntryCapture({
|
||||
captureFastSelectedSource,
|
||||
timeoutMs: 750,
|
||||
predicate: (capture) => Boolean(capture && !capture.paimonMenu?.present),
|
||||
});
|
||||
appendDiagnosticEvent({
|
||||
phase: "entry-capture",
|
||||
severity: worldProbe ? "info" : "warn",
|
||||
message: `${label} capture after closing Paimon menu.`,
|
||||
capture: worldProbe,
|
||||
});
|
||||
if (worldProbe?.paimonMenu?.present) {
|
||||
appendAutomationLog(`${label} entry stopped: Paimon menu still visible after second ESC`);
|
||||
return worldProbe;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const inventoryResult = await automationRepo.keyPress?.("B");
|
||||
appendAutomationLog(`${label} entry key B: ${inventoryResult?.ok ? "ok" : "blocked"}`);
|
||||
appendDiagnosticEvent({
|
||||
phase: "entry-key",
|
||||
severity: keyPressBlocked(inventoryResult) ? "error" : "ok",
|
||||
message: `${label} entry key B`,
|
||||
details: summarizeKeyPressResult(inventoryResult),
|
||||
});
|
||||
if (keyPressBlocked(inventoryResult)) return null;
|
||||
const tabProbe = await waitForEntryCapture({
|
||||
captureFastSelectedSource,
|
||||
timeoutMs: 1200,
|
||||
predicate: (capture) => Boolean(capture && !capture.paimonMenu?.present && capture.inventoryGrid && capture.inventoryGrid.source !== "missing"),
|
||||
});
|
||||
appendDiagnosticEvent({
|
||||
phase: "entry-capture",
|
||||
severity: tabProbe ? "info" : "warn",
|
||||
message: `${label} capture after Inventory-key step.`,
|
||||
capture: tabProbe,
|
||||
});
|
||||
if (tabProbe?.paimonMenu?.present) {
|
||||
appendAutomationLog(`${label} entry stopped: Paimon menu still visible after Inventory key`);
|
||||
appendDiagnosticEvent({
|
||||
phase: "entry-capture",
|
||||
severity: "info",
|
||||
message: `${label} entry stopped before tab click because Paimon menu is still visible.`,
|
||||
capture: tabProbe,
|
||||
});
|
||||
return tabProbe;
|
||||
}
|
||||
if (!tabProbe?.inventoryGrid || tabProbe.inventoryGrid.source === "missing") return tabProbe;
|
||||
|
||||
const target = artifactTabClickTarget(tabProbe);
|
||||
appendAutomationLog(`${label} entry artifact tab -> ${target.x},${target.y}`);
|
||||
const click = await automationRepo.clickScreen(target.x, target.y);
|
||||
appendDiagnosticEvent({
|
||||
phase: "entry-click",
|
||||
severity: click.inputBlocked || click.clicked === false || click.moved === false ? "warn" : "ok",
|
||||
message: `${label} artifact tab click at ${target.x},${target.y}`,
|
||||
details: summarizeClickResult(click),
|
||||
capture: tabProbe,
|
||||
});
|
||||
if (click.inputBlocked || click.clicked === false) return null;
|
||||
const gridProbe = await waitForEntryCapture({
|
||||
captureFastSelectedSource,
|
||||
timeoutMs: 900,
|
||||
predicate: (capture) => Boolean(capture?.inventoryGrid && capture.inventoryGrid.source !== "missing"),
|
||||
});
|
||||
appendDiagnosticEvent({
|
||||
phase: "entry-capture",
|
||||
severity: gridProbe ? "info" : "warn",
|
||||
message: `${label} capture after artifact-tab click before first tile selection.`,
|
||||
capture: gridProbe,
|
||||
});
|
||||
const firstTarget = gridProbe?.inventoryGrid?.centers?.[0];
|
||||
if (!firstTarget) return gridProbe;
|
||||
appendAutomationLog(`${label} entry first artifact tile -> ${firstTarget.x},${firstTarget.y}`);
|
||||
const firstTileClick = await automationRepo.clickScreen(firstTarget.x, firstTarget.y);
|
||||
appendDiagnosticEvent({
|
||||
phase: "entry-click",
|
||||
severity: firstTileClick.inputBlocked || firstTileClick.clicked === false || firstTileClick.moved === false ? "warn" : "ok",
|
||||
message: `${label} first artifact tile click at ${firstTarget.x},${firstTarget.y}`,
|
||||
details: summarizeClickResult(firstTileClick),
|
||||
capture: gridProbe,
|
||||
});
|
||||
if (firstTileClick.inputBlocked || firstTileClick.clicked === false) return null;
|
||||
const finalCapture = await waitForEntryCapture({
|
||||
captureFastSelectedSource,
|
||||
timeoutMs: 900,
|
||||
predicate: (capture) => validateAutoScanEntryPreflight(capture).ok,
|
||||
});
|
||||
appendDiagnosticEvent({
|
||||
phase: "entry-capture",
|
||||
severity: finalCapture ? "info" : "warn",
|
||||
message: `${label} final capture after first artifact selection.`,
|
||||
capture: finalCapture,
|
||||
});
|
||||
return finalCapture;
|
||||
}
|
||||
|
||||
async function waitForEntryCapture({
|
||||
captureFastSelectedSource,
|
||||
timeoutMs,
|
||||
pollMs = 150,
|
||||
predicate,
|
||||
}: {
|
||||
captureFastSelectedSource: (delayMs?: number, focusGenshin?: boolean, options?: CaptureOptions) => Promise<CaptureResult | null>;
|
||||
timeoutMs: number;
|
||||
pollMs?: number;
|
||||
predicate: (capture: CaptureResult | null) => boolean;
|
||||
}) {
|
||||
const startedAt = Date.now();
|
||||
let latest: CaptureResult | null = null;
|
||||
while (Date.now() - startedAt <= timeoutMs) {
|
||||
latest = await captureFastSelectedSource(0, true);
|
||||
if (predicate(latest)) return latest;
|
||||
const remaining = timeoutMs - (Date.now() - startedAt);
|
||||
if (remaining <= 0) break;
|
||||
await wait(Math.min(pollMs, remaining));
|
||||
}
|
||||
return latest;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect } from "react";
|
||||
import type { AutomationRepositoryPort } from "../../../infrastructure/repositories/rendererBridgeRepositoryTypes";
|
||||
import type { ScannerCommand } from "../../../types/global";
|
||||
import type { CaptureOptions, ScannerCommand } from "../../../types/global";
|
||||
import type { VisibleGridScanOptions } from "./scanViewScanActions";
|
||||
|
||||
interface ScanCommandListenerInput {
|
||||
@@ -9,6 +9,7 @@ interface ScanCommandListenerInput {
|
||||
isScanning: boolean;
|
||||
selectedSourceId: string;
|
||||
requestScanStop: (reason: string) => void;
|
||||
runGuidedAutoScan: (options?: { scanLimit?: number; ocrEngine?: CaptureOptions["ocrEngine"] }) => Promise<void>;
|
||||
runVisibleGridScan: (options?: VisibleGridScanOptions) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -18,6 +19,7 @@ export function useScanCommandListener({
|
||||
isScanning,
|
||||
selectedSourceId,
|
||||
requestScanStop,
|
||||
runGuidedAutoScan,
|
||||
runVisibleGridScan,
|
||||
}: ScanCommandListenerInput) {
|
||||
useEffect(() => {
|
||||
@@ -29,9 +31,12 @@ export function useScanCommandListener({
|
||||
}
|
||||
const commandType = typeof command === "string" ? command : command.type;
|
||||
if (commandType === "start-auto" && !autoScanRunning && !isScanning && selectedSourceId) {
|
||||
const options = typeof command === "string" ? undefined : { scanLimit: command.scanLimit };
|
||||
void runVisibleGridScan(options);
|
||||
if (typeof command === "string" || !command.scanEntryMode) {
|
||||
void runGuidedAutoScan(typeof command === "string" ? undefined : { scanLimit: command.scanLimit, ocrEngine: command.ocrEngine });
|
||||
return;
|
||||
}
|
||||
void runVisibleGridScan({ scanLimit: command.scanLimit, scanEntryMode: command.scanEntryMode, ocrEngine: command.ocrEngine });
|
||||
}
|
||||
});
|
||||
}, [automationRepo, autoScanRunning, isScanning, selectedSourceId, requestScanStop, runVisibleGridScan]);
|
||||
}, [automationRepo, autoScanRunning, isScanning, selectedSourceId, requestScanStop, runGuidedAutoScan, runVisibleGridScan]);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { type AutoScanStats, type ScanSummary } from "../../../lib/scannerSessio
|
||||
import type { RuntimeInfo } from "../../../types/global";
|
||||
import type { SnapshotRepositoryPort } from "../../../infrastructure/repositories/rendererBridgeRepositoryTypes";
|
||||
import type { CaptureResult } from "../../../types/global";
|
||||
import { validateLookupPackage } from "../../../lib/genshinLookup";
|
||||
import type { ScanDiagnosticEvent } from "../../../lib/scanDiagnosticsLog";
|
||||
|
||||
type InventoryGrid = NonNullable<CaptureResult["inventoryGrid"]>;
|
||||
|
||||
@@ -17,6 +19,7 @@ interface ScanSnapshotPublisherInput {
|
||||
snapshot: AppSnapshot;
|
||||
latestInventoryGrid: InventoryGrid | null | undefined;
|
||||
automationLog: string[];
|
||||
diagnosticEvents: ScanDiagnosticEvent[];
|
||||
runtimeInfo: RuntimeInfo | null;
|
||||
storedTotal: number | null;
|
||||
learningRuleCount: number;
|
||||
@@ -33,6 +36,7 @@ export function useScanSnapshotPublisher({
|
||||
snapshot,
|
||||
latestInventoryGrid,
|
||||
automationLog,
|
||||
diagnosticEvents,
|
||||
runtimeInfo,
|
||||
storedTotal,
|
||||
learningRuleCount,
|
||||
@@ -52,9 +56,33 @@ export function useScanSnapshotPublisher({
|
||||
snapshotBuilds: snapshot.builds.length,
|
||||
grid: latestInventoryGrid ?? null,
|
||||
automationLog: automationLog.slice(-12),
|
||||
diagnosticEvents: diagnosticEvents.slice(-8).map((event) => ({
|
||||
...event,
|
||||
capture: event.capture
|
||||
? {
|
||||
...event.capture,
|
||||
screenshots: event.capture.screenshots
|
||||
? {
|
||||
detail: event.capture.screenshots.detail ? "[detail screenshot available in Diagnose]" : undefined,
|
||||
inventory: event.capture.screenshots.inventory ? "[inventory screenshot available in Diagnose]" : undefined,
|
||||
full: event.capture.screenshots.full ? "[full screenshot omitted]" : undefined,
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
: undefined,
|
||||
})),
|
||||
runtimeInfo,
|
||||
storedTotal,
|
||||
learningRuleCount,
|
||||
lookupStatus: validateLookupPackage(),
|
||||
ocrEngine: scanSummary?.mode.includes("ik-traineddata") ? "ik-traineddata" : "current",
|
||||
entryMode: scanSummary?.mode.includes("auto-entry")
|
||||
? "auto-entry"
|
||||
: scanSummary?.mode.includes("direct-inventory")
|
||||
? "direct-inventory"
|
||||
: scanSummary?.mode.includes("paimon-menu")
|
||||
? "paimon-menu"
|
||||
: "visible-inventory",
|
||||
updatedAt: new Date().toISOString(),
|
||||
}).catch(() => undefined);
|
||||
}, [
|
||||
@@ -67,6 +95,7 @@ export function useScanSnapshotPublisher({
|
||||
snapshot,
|
||||
latestInventoryGrid,
|
||||
automationLog,
|
||||
diagnosticEvents,
|
||||
runtimeInfo,
|
||||
storedTotal,
|
||||
learningRuleCount,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import type { Dispatch, MutableRefObject, SetStateAction } from "react";
|
||||
import { runAutoReviewScan as runAutoReviewScanAction, runVisibleGridScan as runVisibleGridScanAction, type VisibleGridScanOptions } from "./scanViewScanActions";
|
||||
import {
|
||||
@@ -21,6 +21,8 @@ import type {
|
||||
} from "../../../infrastructure/repositories/rendererBridgeRepositoryTypes";
|
||||
import type { AutoScanStats, ScanSummary } from "../../../lib/scannerSession";
|
||||
import type { RuntimeInfo } from "../../../types/global";
|
||||
import type { createScanDiagnosticEvent } from "../../../lib/scanDiagnosticsLog";
|
||||
import { validateAutoScanEntryPreflight } from "../../../lib/autoScanEntry";
|
||||
|
||||
type BooleanSetter = Dispatch<SetStateAction<boolean>>;
|
||||
type NumberSetter = Dispatch<SetStateAction<number>>;
|
||||
@@ -49,6 +51,7 @@ interface ScanViewActionInput {
|
||||
setReviewStatus: StringSetter;
|
||||
appendAutomationLog: (line: string) => void;
|
||||
appendClickDiagnostics: (result: ClickResult, prefix?: string) => void;
|
||||
appendDiagnosticEvent: (event: Omit<Parameters<typeof createScanDiagnosticEvent>[0], "includeFullScreenshot">) => void;
|
||||
parseArtifact: (capture: CaptureResult | null) => ParsedArtifactCandidate | null;
|
||||
artifactRepo?: ArtifactRepositoryPort;
|
||||
reviewSamplesRepo?: ReviewSampleRepositoryPort;
|
||||
@@ -77,6 +80,7 @@ export interface ScanViewActionResult {
|
||||
loadReviewQueue: () => Promise<void>;
|
||||
openReviewQueue: () => Promise<void>;
|
||||
runAutoReviewScan: () => Promise<void>;
|
||||
runGuidedAutoScan: (options?: { scanLimit?: number; ocrEngine?: CaptureOptions["ocrEngine"] }) => Promise<void>;
|
||||
runVisibleGridScan: (options?: VisibleGridScanOptions) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -99,6 +103,7 @@ export function useScanViewActions(input: ScanViewActionInput): ScanViewActionRe
|
||||
setReviewStatus,
|
||||
appendAutomationLog,
|
||||
appendClickDiagnostics,
|
||||
appendDiagnosticEvent,
|
||||
parseArtifact,
|
||||
artifactRepo,
|
||||
reviewSamplesRepo,
|
||||
@@ -116,6 +121,7 @@ export function useScanViewActions(input: ScanViewActionInput): ScanViewActionRe
|
||||
canCaptureSource,
|
||||
setReviewQueueOpen,
|
||||
} = input;
|
||||
const learningInitializedRef = useRef(false);
|
||||
|
||||
const requestScanStop = useCallback((reason = "Stop angefordert.") => {
|
||||
stopVisibleScanRef.current = true;
|
||||
@@ -154,8 +160,11 @@ export function useScanViewActions(input: ScanViewActionInput): ScanViewActionRe
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (learningInitializedRef.current) return;
|
||||
if (!learningRepo && !reviewSamplesRepo && !artifactRepo) return;
|
||||
learningInitializedRef.current = true;
|
||||
void initializeLearningState(reviewContext);
|
||||
}, [reviewContext]);
|
||||
}, [artifactRepo, learningRepo, reviewContext, reviewSamplesRepo]);
|
||||
|
||||
const focusDashboard = useCallback(async () => {
|
||||
try {
|
||||
@@ -215,12 +224,25 @@ export function useScanViewActions(input: ScanViewActionInput): ScanViewActionRe
|
||||
setReviewStatus,
|
||||
appendAutomationLog,
|
||||
appendClickDiagnostics,
|
||||
appendDiagnosticEvent,
|
||||
parseArtifact,
|
||||
persistParsedArtifact: parseArtifactAndPersist,
|
||||
saveReviewSample: handleSaveReviewSample,
|
||||
focusDashboard,
|
||||
captureSelectedSource,
|
||||
captureFastSelectedSource: (delayMs = 0, focusGenshin = false) => captureSelectedSource(delayMs, focusGenshin, { skipOcr: true }),
|
||||
captureSelectedSource: (delayMs = 0, focusGenshin = false, options) => captureSelectedSource(delayMs, focusGenshin, {
|
||||
ocrMode: "artifact",
|
||||
omitFullFrame: true,
|
||||
omitInventoryPreview: true,
|
||||
...options,
|
||||
}),
|
||||
captureFastSelectedSource: (delayMs = 0, focusGenshin = false, options) => captureSelectedSource(delayMs, focusGenshin, {
|
||||
skipOcr: true,
|
||||
omitFullFrame: true,
|
||||
omitCrops: true,
|
||||
omitCropImages: true,
|
||||
omitLockState: true,
|
||||
...options,
|
||||
}),
|
||||
}),
|
||||
[
|
||||
autoScanRunning,
|
||||
@@ -240,6 +262,7 @@ export function useScanViewActions(input: ScanViewActionInput): ScanViewActionRe
|
||||
setReviewStatus,
|
||||
appendAutomationLog,
|
||||
appendClickDiagnostics,
|
||||
appendDiagnosticEvent,
|
||||
parseArtifact,
|
||||
parseArtifactAndPersist,
|
||||
handleSaveReviewSample,
|
||||
@@ -276,12 +299,52 @@ export function useScanViewActions(input: ScanViewActionInput): ScanViewActionRe
|
||||
scanActionContext,
|
||||
]);
|
||||
|
||||
const runGuidedAutoScan = useCallback(async (options: { scanLimit?: number; ocrEngine?: CaptureOptions["ocrEngine"] } = {}) => {
|
||||
if (autoScanRunning || !bridgeReady || !selectedSourceId || !automationRepo?.clickScreen || !automationRepo?.scrollScreen) {
|
||||
return;
|
||||
}
|
||||
setReviewStatus("Auto-Scan prueft den Startzustand ohne OCR...");
|
||||
const preflightCapture = await captureSelectedSource(0, true, {
|
||||
skipOcr: true,
|
||||
omitFullFrame: true,
|
||||
omitCrops: true,
|
||||
omitCropImages: true,
|
||||
omitLockState: true,
|
||||
});
|
||||
const visibleInventoryReady = validateAutoScanEntryPreflight(preflightCapture).ok;
|
||||
appendDiagnosticEvent({
|
||||
phase: "guided-start",
|
||||
severity: visibleInventoryReady ? "ok" : "info",
|
||||
message: visibleInventoryReady
|
||||
? "Artifact inventory detail view already visible; starting scan directly."
|
||||
: "Artifact detail view is not ready; trying direct inventory entry, then Inventory Kamera fallback.",
|
||||
capture: preflightCapture,
|
||||
});
|
||||
await runVisibleGridScanAction(scanActionContext, {
|
||||
scanLimit: options.scanLimit,
|
||||
scanEntryMode: visibleInventoryReady ? "visible-inventory" : "auto-entry",
|
||||
processInitialSelection: visibleInventoryReady,
|
||||
ocrEngine: options.ocrEngine,
|
||||
});
|
||||
}, [
|
||||
autoScanRunning,
|
||||
bridgeReady,
|
||||
selectedSourceId,
|
||||
automationRepo?.clickScreen,
|
||||
automationRepo?.scrollScreen,
|
||||
setReviewStatus,
|
||||
captureSelectedSource,
|
||||
appendDiagnosticEvent,
|
||||
scanActionContext,
|
||||
]);
|
||||
|
||||
useScanCommandListener({
|
||||
automationRepo,
|
||||
autoScanRunning,
|
||||
isScanning,
|
||||
selectedSourceId,
|
||||
requestScanStop,
|
||||
runGuidedAutoScan,
|
||||
runVisibleGridScan,
|
||||
});
|
||||
|
||||
@@ -291,6 +354,7 @@ export function useScanViewActions(input: ScanViewActionInput): ScanViewActionRe
|
||||
loadReviewQueue: loadReviewQueueAction,
|
||||
openReviewQueue: openReviewQueueModal,
|
||||
runAutoReviewScan,
|
||||
runGuidedAutoScan,
|
||||
runVisibleGridScan,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useScanSnapshotPublisher } from "./useScanSnapshotPublisher";
|
||||
import { useScanViewActions } from "./useScanViewActions";
|
||||
import { useScanViewStateSync } from "./useScanViewStateSync";
|
||||
import { emptyAutoScanStats, resolveScanTargetCount, type AutoScanStats, type ScanSummary } from "../../../lib/scannerSession";
|
||||
import { createScanDiagnosticEvent, summarizeClickResult, type ScanDiagnosticEvent } from "../../../lib/scanDiagnosticsLog";
|
||||
import type { ScanViewProps, ScanViewControllerResult } from "../types";
|
||||
import type { CaptureResult, ClickResult, ReviewSampleRecord } from "../../../types/global";
|
||||
import type { StoredArtifactRecord } from "../../../types/storage";
|
||||
@@ -56,6 +57,7 @@ export function useScanViewController({
|
||||
const [scanLimitTouched, setScanLimitTouched] = useState(false);
|
||||
const [skipRows, setSkipRows] = useState(0);
|
||||
const [automationLog, setAutomationLog] = useState<string[]>([]);
|
||||
const [diagnosticEvents, setDiagnosticEvents] = useState<ScanDiagnosticEvent[]>([]);
|
||||
const [storedTotal, setStoredTotal] = useState<number | null>(null);
|
||||
const [devMode, setDevMode] = useState(() => localStorage.getItem("gaa-dev-mode") === "1");
|
||||
const [scannerLearningRules, setScannerLearningRules] = useState<ScannerLearningRules>({ textReplacements: {} });
|
||||
@@ -92,12 +94,28 @@ export function useScanViewController({
|
||||
setAutomationLog((previous) => [...previous.slice(-11), `${new Date().toLocaleTimeString()} ${line}`]);
|
||||
}, []);
|
||||
|
||||
const appendDiagnosticEvent = useCallback((event: Omit<Parameters<typeof createScanDiagnosticEvent>[0], "includeFullScreenshot">) => {
|
||||
setDiagnosticEvents((previous) => [
|
||||
...previous.slice(-23),
|
||||
createScanDiagnosticEvent({
|
||||
...event,
|
||||
includeFullScreenshot: false,
|
||||
}),
|
||||
]);
|
||||
}, []);
|
||||
|
||||
const appendClickDiagnostics = useCallback((result: ClickResult, prefix = "input") => {
|
||||
const cursor = `${result.cursorX ?? "?"},${result.cursorY ?? "?"}`;
|
||||
const focus = result.focused ? `fg:${result.foregroundProcess || "Genshin"}` : `fg-miss:${result.foregroundProcess || "?"}`;
|
||||
const blocked = result.inputBlocked ? " input:blocked" : "";
|
||||
appendAutomationLog(`${prefix}: ${focus}${blocked} cursor ${cursor} moved:${result.moved ? "yes" : "no"} clicked:${result.clicked ? "yes" : "no"}`);
|
||||
}, [appendAutomationLog]);
|
||||
appendDiagnosticEvent({
|
||||
phase: "input",
|
||||
severity: result.inputBlocked || result.clicked === false || result.moved === false ? "warn" : "ok",
|
||||
message: `${prefix}: click ${result.clicked ? "sent" : "not sent"}`,
|
||||
details: summarizeClickResult(result),
|
||||
});
|
||||
}, [appendAutomationLog, appendDiagnosticEvent]);
|
||||
|
||||
const toggleDevMode = useCallback(() => {
|
||||
setDevMode((previous) => {
|
||||
@@ -118,6 +136,7 @@ export function useScanViewController({
|
||||
loadReviewQueue: loadReviewQueueAction,
|
||||
openReviewQueue: openReviewQueueModal,
|
||||
runAutoReviewScan,
|
||||
runGuidedAutoScan,
|
||||
runVisibleGridScan,
|
||||
} = useScanViewActions({
|
||||
autoScanRunning,
|
||||
@@ -137,6 +156,7 @@ export function useScanViewController({
|
||||
setReviewStatus,
|
||||
appendAutomationLog,
|
||||
appendClickDiagnostics,
|
||||
appendDiagnosticEvent,
|
||||
parseArtifact,
|
||||
artifactRepo,
|
||||
reviewSamplesRepo,
|
||||
@@ -209,6 +229,7 @@ export function useScanViewController({
|
||||
snapshot,
|
||||
latestInventoryGrid: latestCapture?.inventoryGrid ?? null,
|
||||
automationLog,
|
||||
diagnosticEvents,
|
||||
runtimeInfo,
|
||||
storedTotal,
|
||||
learningRuleCount,
|
||||
@@ -230,6 +251,7 @@ export function useScanViewController({
|
||||
scanLimitTouched,
|
||||
skipRows,
|
||||
automationLog,
|
||||
diagnosticEvents,
|
||||
storedTotal,
|
||||
devMode,
|
||||
scannerLearningRules,
|
||||
@@ -267,6 +289,7 @@ export function useScanViewController({
|
||||
loadReviewQueue: loadReviewQueueAction,
|
||||
openReviewQueue: openReviewQueueModal,
|
||||
runAutoReviewScan,
|
||||
runGuidedAutoScan,
|
||||
runVisibleGridScan,
|
||||
canGoodInterop,
|
||||
exportGoodFromStore,
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { BooleanResult, CaptureOptions, CaptureResult, CaptureSourceInfo, R
|
||||
import type { StoredArtifactRecord } from "../../types/storage";
|
||||
import type { AutoScanStats, ScanSummary } from "../../lib/scannerSession";
|
||||
import type { ParsedArtifactCandidate } from "../../lib/artifactOcrParser";
|
||||
import type { ScanDiagnosticEvent } from "../../lib/scanDiagnosticsLog";
|
||||
import type { ScannerLearningRules } from "../../lib/scannerLearning";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import type { analyzeReviewSamples } from "../../lib/reviewSampleAnalysis";
|
||||
@@ -36,6 +37,7 @@ export interface ScanViewControllerResult {
|
||||
scanLimitTouched: boolean;
|
||||
skipRows: number;
|
||||
automationLog: string[];
|
||||
diagnosticEvents: ScanDiagnosticEvent[];
|
||||
storedTotal: number | null;
|
||||
devMode: boolean;
|
||||
scannerLearningRules: ScannerLearningRules;
|
||||
@@ -79,6 +81,7 @@ export interface ScanViewControllerResult {
|
||||
loadReviewQueue: () => Promise<void>;
|
||||
openReviewQueue: () => Promise<void>;
|
||||
runAutoReviewScan: () => Promise<void>;
|
||||
runGuidedAutoScan: (options?: { scanLimit?: number; ocrEngine?: CaptureOptions["ocrEngine"] }) => Promise<void>;
|
||||
runVisibleGridScan: () => Promise<void>;
|
||||
canGoodInterop: boolean;
|
||||
exportGoodFromStore: () => Promise<{ ok: boolean; path?: string; count: number }>;
|
||||
|
||||
Reference in New Issue
Block a user