chore: initialize repository baseline
Import the existing Electron + React + TypeScript app as the version-control baseline before the scanner rework (C# input/capture sidecar, resolution-anchored layout profiles, OCR preprocessing, eval harness, rescan-merge, GOOD interop). Housekeeping in this commit: - Remove orphaned temp_inputhelper_block.ts (duplicate of the input-helper script). - Ignore .claude/scheduled_tasks.lock local session state. - Add .gitattributes to normalize line endings (LF in repo). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
import { shouldFlagArtifactForReview } from "../../../lib/scannerLearning";
|
||||
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 {
|
||||
ReviewStateContext,
|
||||
} from "./scanViewReviewHelpers";
|
||||
import type { ArtifactRepositoryPort, ReviewSampleRepositoryPort, LearningRepositoryPort, AutomationRepositoryPort, RuntimeRepositoryPort } from "../../../infrastructure/repositories/rendererBridgeRepositoryTypes";
|
||||
import type { BooleanResult, CaptureOptions, CaptureResult, ClickResult, ReviewSampleRecord, RuntimeInfo } from "../../../types/global";
|
||||
import type { Dispatch, MutableRefObject, SetStateAction } from "react";
|
||||
|
||||
export interface ReviewStateContextInput {
|
||||
artifactRepo?: ArtifactRepositoryPort;
|
||||
reviewSamplesRepo?: ReviewSampleRepositoryPort;
|
||||
learningRepo?: LearningRepositoryPort;
|
||||
onStoredArtifactsChanged?: () => Promise<void> | void;
|
||||
setReviewSampleTotal: Dispatch<SetStateAction<number>>;
|
||||
setReviewSamples: Dispatch<SetStateAction<ReviewSampleRecord[]>>;
|
||||
setLearningRulesLoaded: Dispatch<SetStateAction<boolean>>;
|
||||
setScannerLearningRules: Dispatch<SetStateAction<ScannerLearningRules>>;
|
||||
setReviewStatus: Dispatch<SetStateAction<string>>;
|
||||
setStoredTotal: Dispatch<SetStateAction<number | null>>;
|
||||
appendAutomationLog: (line: string) => void;
|
||||
}
|
||||
|
||||
export interface ScanActionContextInput {
|
||||
autoScanRunning: boolean;
|
||||
setAutoScanRunning: Dispatch<SetStateAction<boolean>>;
|
||||
isScanning: boolean;
|
||||
stopVisibleScanRef: MutableRefObject<boolean>;
|
||||
selectedSourceId: string;
|
||||
bridgeReady: boolean;
|
||||
automationRepo?: AutomationRepositoryPort;
|
||||
runtimeInfo?: RuntimeInfo | null;
|
||||
runtimeRepo?: RuntimeRepositoryPort;
|
||||
scanLimit: number;
|
||||
skipRows: number;
|
||||
detectedInventoryCount: number;
|
||||
setScanSummary: Dispatch<SetStateAction<ScanSummary | null>>;
|
||||
setAutoScanStats: Dispatch<SetStateAction<AutoScanStats>>;
|
||||
setReviewStatus: Dispatch<SetStateAction<string>>;
|
||||
appendAutomationLog: (line: string) => void;
|
||||
appendClickDiagnostics: (result: ClickResult, prefix?: string) => void;
|
||||
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>;
|
||||
focusDashboard: () => Promise<void>;
|
||||
captureSelectedSource: (
|
||||
delayMs?: number,
|
||||
focusGenshin?: boolean,
|
||||
options?: CaptureOptions,
|
||||
) => Promise<CaptureResult | null>;
|
||||
captureFastSelectedSource: (delayMs?: number, focusGenshin?: boolean) => Promise<CaptureResult | null>;
|
||||
}
|
||||
|
||||
export function createReviewContext(input: ReviewStateContextInput): ReviewStateContext {
|
||||
return {
|
||||
artifactRepo: input.artifactRepo,
|
||||
reviewSamplesRepo: input.reviewSamplesRepo,
|
||||
learningRepo: input.learningRepo,
|
||||
onStoredArtifactsChanged: input.onStoredArtifactsChanged,
|
||||
setReviewSampleTotal: input.setReviewSampleTotal,
|
||||
setReviewSamples: input.setReviewSamples,
|
||||
setLearningRulesLoaded: input.setLearningRulesLoaded,
|
||||
setScannerLearningRules: input.setScannerLearningRules,
|
||||
setReviewStatus: input.setReviewStatus,
|
||||
setStoredTotal: input.setStoredTotal,
|
||||
appendAutomationLog: input.appendAutomationLog,
|
||||
};
|
||||
}
|
||||
|
||||
export function createScanActionContext(input: ScanActionContextInput): ScanActionContext {
|
||||
return {
|
||||
autoScanRunning: input.autoScanRunning,
|
||||
setAutoScanRunning: input.setAutoScanRunning,
|
||||
isScanning: input.isScanning,
|
||||
stopVisibleScanRef: input.stopVisibleScanRef,
|
||||
selectedSourceId: input.selectedSourceId,
|
||||
bridgeReady: input.bridgeReady,
|
||||
automationRepo: input.automationRepo,
|
||||
runtimeInfo: input.runtimeInfo,
|
||||
runtimeRepo: input.runtimeRepo,
|
||||
scanLimit: input.scanLimit,
|
||||
skipRows: input.skipRows,
|
||||
detectedInventoryCount: input.detectedInventoryCount,
|
||||
setScanSummary: input.setScanSummary,
|
||||
setAutoScanStats: input.setAutoScanStats,
|
||||
setReviewStatus: input.setReviewStatus,
|
||||
appendAutomationLog: input.appendAutomationLog,
|
||||
appendClickDiagnostics: input.appendClickDiagnostics,
|
||||
parseArtifact: input.parseArtifact,
|
||||
persistParsedArtifact: input.persistParsedArtifact,
|
||||
shouldFlagArtifactForReview: (parsed) => (parsed ? shouldFlagArtifactForReview(parsed) : false),
|
||||
saveReviewSample: input.saveReviewSample,
|
||||
focusDashboard: input.focusDashboard,
|
||||
captureSelectedSource: input.captureSelectedSource,
|
||||
captureFastSelectedSource: input.captureFastSelectedSource,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import {
|
||||
applyScannerLearningRules,
|
||||
countScannerLearningRules,
|
||||
deriveScannerLearningRules,
|
||||
deriveScannerLearningRulesFromReviewSamples,
|
||||
shouldFlagArtifactForReview,
|
||||
type ScannerLearningRules,
|
||||
} from "../../../lib/scannerLearning";
|
||||
import { captureRejectionReason, captureSourceRejectionReason, shouldPersistParsedArtifact } from "../../../lib/scannerCaptureQuality";
|
||||
import { parseArtifactCandidate, type ParsedArtifactCandidate } from "../../../lib/artifactOcrParser";
|
||||
import { isReviewOnlyArtifactSource, storedArtifactStrength, toStoredArtifact } from "../../../lib/artifactStore";
|
||||
import {
|
||||
type ArtifactRepositoryPort,
|
||||
type LearningRepositoryPort,
|
||||
type ReviewSampleRepositoryPort,
|
||||
} from "../../../infrastructure/repositories/rendererBridgeRepositories";
|
||||
import type { CaptureResult, ReviewSampleRecord, SaveResultWithPath } from "../../../types/global";
|
||||
import type { StoredArtifactRecord } from "../../../types/storage";
|
||||
|
||||
const REVIEW_SAMPLE_LIMIT_INITIAL = 120;
|
||||
const REVIEW_SAMPLE_LIMIT_QUEUE = 80;
|
||||
|
||||
export interface ReviewStateContext {
|
||||
artifactRepo?: ArtifactRepositoryPort;
|
||||
reviewSamplesRepo?: ReviewSampleRepositoryPort;
|
||||
learningRepo?: LearningRepositoryPort;
|
||||
onStoredArtifactsChanged?: () => Promise<void> | void;
|
||||
setReviewSampleTotal: Dispatch<SetStateAction<number>>;
|
||||
setReviewSamples: Dispatch<SetStateAction<ReviewSampleRecord[]>>;
|
||||
setLearningRulesLoaded: Dispatch<SetStateAction<boolean>>;
|
||||
setScannerLearningRules: Dispatch<SetStateAction<ScannerLearningRules>>;
|
||||
setReviewStatus: Dispatch<SetStateAction<string>>;
|
||||
setStoredTotal: Dispatch<SetStateAction<number | null>>;
|
||||
appendAutomationLog: (line: string) => void;
|
||||
}
|
||||
|
||||
async function loadReviewSamplesForContext(
|
||||
context: ReviewStateContext,
|
||||
limit: number,
|
||||
): Promise<ReturnType<ReviewSampleRepositoryPort["loadSamples"]> | null> {
|
||||
return context.reviewSamplesRepo?.loadSamples(limit).catch(() => null) ?? null;
|
||||
}
|
||||
|
||||
async function loadReviewSamplesAndSetTotal(context: ReviewStateContext, limit: number) {
|
||||
const result = await loadReviewSamplesForContext(context, limit);
|
||||
if (result?.ok) {
|
||||
context.setReviewSampleTotal(result.total);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function parseLearnedArtifact(
|
||||
capture: CaptureResult | null,
|
||||
scannerLearningRules: ScannerLearningRules,
|
||||
): ParsedArtifactCandidate | null {
|
||||
return parseArtifactCandidate(applyScannerLearningRules(capture, scannerLearningRules));
|
||||
}
|
||||
|
||||
export function createCaptureFromReviewSample(entry: ReviewSampleRecord): CaptureResult | null {
|
||||
const capture = entry.sample?.capture;
|
||||
if (!capture?.ocr) return null;
|
||||
return {
|
||||
id: `review-${entry.savedAt}`,
|
||||
name: capture.name ?? "review-sample",
|
||||
width: capture.width ?? 0,
|
||||
height: capture.height ?? 0,
|
||||
dataUrl: "",
|
||||
capturedAt: capture.capturedAt ?? entry.savedAt,
|
||||
captureTarget: capture.captureTarget,
|
||||
crops: [],
|
||||
inventoryDataUrl: capture.inventoryDataUrl,
|
||||
ocr: capture.ocr,
|
||||
inventoryGrid: capture.inventoryGrid,
|
||||
};
|
||||
}
|
||||
|
||||
function storedArtifactFingerprint(record: StoredArtifactRecord | null | undefined) {
|
||||
if (!record) return "";
|
||||
return [
|
||||
record.name,
|
||||
record.slot,
|
||||
record.level ?? 0,
|
||||
record.mainStat,
|
||||
record.mainValue,
|
||||
record.setName,
|
||||
record.equipped,
|
||||
(record.substats ?? []).join("|"),
|
||||
record.needsReview ? "review" : "clean",
|
||||
].join("::");
|
||||
}
|
||||
|
||||
function shouldRecoverIntoStore(existing: StoredArtifactRecord | undefined, incoming: StoredArtifactRecord) {
|
||||
if (!existing) return true;
|
||||
if (storedArtifactFingerprint(existing) === storedArtifactFingerprint(incoming)) return false;
|
||||
|
||||
const existingStrength = storedArtifactStrength(existing);
|
||||
const incomingStrength = storedArtifactStrength(incoming);
|
||||
const existingSubstats = existing.substats?.length ?? 0;
|
||||
const incomingSubstats = incoming.substats?.length ?? 0;
|
||||
const existingEquippedKnown = Boolean(existing.equipped && !/not detected/i.test(existing.equipped));
|
||||
const incomingEquippedKnown = Boolean(incoming.equipped && !/not detected/i.test(incoming.equipped));
|
||||
|
||||
if (existing.needsReview && !incoming.needsReview) return true;
|
||||
if (incomingSubstats > existingSubstats) return true;
|
||||
if (!existingEquippedKnown && incomingEquippedKnown) return true;
|
||||
if (incomingStrength >= existingStrength + 6) return true;
|
||||
if (isReviewOnlyArtifactSource(existing.source)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function getDefaultScannerRules(loadedRules: { rules?: ScannerLearningRules } | null | undefined): ScannerLearningRules {
|
||||
return { textReplacements: { ...(loadedRules?.rules?.textReplacements ?? {}) } };
|
||||
}
|
||||
|
||||
async function loadReviewSamplesAndRecover(context: ReviewStateContext, rules: ScannerLearningRules, limit = REVIEW_SAMPLE_LIMIT_INITIAL) {
|
||||
const reviewResult = await loadReviewSamplesAndSetTotal(context, limit);
|
||||
if (reviewResult?.ok) {
|
||||
await recoverArtifactsFromReviewSamples(reviewResult.samples, rules, context);
|
||||
}
|
||||
return reviewResult;
|
||||
}
|
||||
|
||||
export async function recoverArtifactsFromReviewSamples(
|
||||
samples: ReviewSampleRecord[],
|
||||
rules: ScannerLearningRules,
|
||||
context: ReviewStateContext,
|
||||
): Promise<number> {
|
||||
const { artifactRepo, onStoredArtifactsChanged, setStoredTotal, appendAutomationLog } = context;
|
||||
if (!artifactRepo?.saveMany || samples.length === 0) return 0;
|
||||
|
||||
const existingResult = await artifactRepo.loadAll().catch(() => null);
|
||||
const existingById = new Map(
|
||||
(existingResult?.artifacts ?? []).map((record: StoredArtifactRecord) => [record.id, record]),
|
||||
);
|
||||
|
||||
const recoveredCandidates = samples.flatMap((entry) => {
|
||||
const capture = createCaptureFromReviewSample(entry);
|
||||
const parsed = parseLearnedArtifact(capture, rules);
|
||||
if (!capture || !parsed) return [];
|
||||
if (captureSourceRejectionReason(capture)) return [];
|
||||
const needsReview = shouldFlagArtifactForReview(parsed);
|
||||
if (!shouldPersistParsedArtifact(parsed, needsReview)) return [];
|
||||
const stored = toStoredArtifact(parsed, "review-reprocess", needsReview);
|
||||
return shouldRecoverIntoStore(existingById.get(stored.id), stored) ? [stored] : [];
|
||||
});
|
||||
|
||||
const recoveredById = new Map<string, StoredArtifactRecord>();
|
||||
for (const record of recoveredCandidates) {
|
||||
const existing = recoveredById.get(record.id);
|
||||
if (!existing || storedArtifactStrength(record) > storedArtifactStrength(existing)) {
|
||||
recoveredById.set(record.id, record);
|
||||
}
|
||||
}
|
||||
const recovered = [...recoveredById.values()];
|
||||
|
||||
if (recovered.length === 0) return 0;
|
||||
const result = await artifactRepo.saveMany(recovered).catch(() => null);
|
||||
if (result?.ok) {
|
||||
setStoredTotal(result.total);
|
||||
void onStoredArtifactsChanged?.();
|
||||
appendAutomationLog(`review reprocess: ${result.added} neu, ${result.updated} aktualisiert`);
|
||||
return result.added + result.updated;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
export async function initializeLearningState(context: ReviewStateContext): Promise<void> {
|
||||
const {
|
||||
learningRepo,
|
||||
setLearningRulesLoaded,
|
||||
setScannerLearningRules,
|
||||
} = context;
|
||||
|
||||
try {
|
||||
const loadedRules = await learningRepo?.loadRules().catch(() => null);
|
||||
const currentRules = getDefaultScannerRules(loadedRules);
|
||||
|
||||
if (countScannerLearningRules(currentRules) > 0) {
|
||||
setScannerLearningRules(currentRules);
|
||||
await loadReviewSamplesAndRecover(context, currentRules);
|
||||
setLearningRulesLoaded(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const reviewResult = await loadReviewSamplesAndRecover(context, { ...currentRules }, REVIEW_SAMPLE_LIMIT_INITIAL);
|
||||
const derived = deriveScannerLearningRulesFromReviewSamples(reviewResult?.samples ?? []);
|
||||
const activeRules = countScannerLearningRules(derived) > 0 ? derived : currentRules;
|
||||
if (countScannerLearningRules(derived) > 0) {
|
||||
setScannerLearningRules(derived);
|
||||
await learningRepo?.saveRules?.(derived).catch(() => null);
|
||||
} else {
|
||||
setScannerLearningRules(currentRules);
|
||||
}
|
||||
setLearningRulesLoaded(true);
|
||||
} catch {
|
||||
const reviewResult = await loadReviewSamplesAndSetTotal(context, REVIEW_SAMPLE_LIMIT_INITIAL);
|
||||
const derived = deriveScannerLearningRulesFromReviewSamples(reviewResult?.samples ?? []);
|
||||
setScannerLearningRules(derived);
|
||||
if (countScannerLearningRules(derived) > 0) {
|
||||
await learningRepo?.saveRules?.(derived).catch(() => null);
|
||||
}
|
||||
if (reviewResult?.ok) {
|
||||
await recoverArtifactsFromReviewSamples(reviewResult.samples, derived, context);
|
||||
}
|
||||
setLearningRulesLoaded(true);
|
||||
}
|
||||
}
|
||||
|
||||
export async function mergeLearningRules(
|
||||
nextRules: Partial<ScannerLearningRules> | null | undefined,
|
||||
currentRules: ScannerLearningRules,
|
||||
context: ReviewStateContext,
|
||||
) {
|
||||
if (!nextRules || countScannerLearningRules(nextRules) === 0) return null;
|
||||
const merged = {
|
||||
textReplacements: {
|
||||
...currentRules.textReplacements,
|
||||
...(nextRules.textReplacements ?? {}),
|
||||
},
|
||||
};
|
||||
context.setScannerLearningRules(merged);
|
||||
const result = await context.learningRepo?.saveRules?.(merged).catch(() => null);
|
||||
return { result, merged };
|
||||
}
|
||||
|
||||
export async function persistParsedArtifact(
|
||||
capture: CaptureResult | null,
|
||||
parsed: ParsedArtifactCandidate,
|
||||
source: string,
|
||||
needsReview: boolean,
|
||||
context: ReviewStateContext,
|
||||
) {
|
||||
const { artifactRepo, onStoredArtifactsChanged, setStoredTotal, appendAutomationLog } = context;
|
||||
if (!artifactRepo?.saveMany) return false;
|
||||
|
||||
const rejection = captureRejectionReason(capture, parsed);
|
||||
if (rejection) {
|
||||
appendAutomationLog(`persist skip: ${rejection}`);
|
||||
return false;
|
||||
}
|
||||
if (!shouldPersistParsedArtifact(parsed, needsReview)) {
|
||||
appendAutomationLog(`persist skip: parsed artifact bleibt vorerst nur Review (${parsed.name})`);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const result = await artifactRepo.saveMany([toStoredArtifact(parsed, source, needsReview)]);
|
||||
if (result?.ok) {
|
||||
setStoredTotal(result.total);
|
||||
void onStoredArtifactsChanged?.();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveReviewSample(
|
||||
capture: CaptureResult | null,
|
||||
parsed: ParsedArtifactCandidate | null,
|
||||
reason: string,
|
||||
scannerLearningRules: ScannerLearningRules,
|
||||
context: ReviewStateContext,
|
||||
): Promise<{ result: SaveResultWithPath | null; recoveredParsed: ParsedArtifactCandidate | null; recoveredToDb: boolean }> {
|
||||
const {
|
||||
reviewSamplesRepo,
|
||||
setReviewStatus,
|
||||
appendAutomationLog,
|
||||
setReviewSamples,
|
||||
onStoredArtifactsChanged,
|
||||
} = context;
|
||||
|
||||
if (!reviewSamplesRepo?.saveSample) {
|
||||
setReviewStatus("Review-Sample speichern ist nur in der Electron-App verfuegbar.");
|
||||
return { result: null, recoveredParsed: null, recoveredToDb: false };
|
||||
}
|
||||
if (!capture) return { result: null, recoveredParsed: null, recoveredToDb: false };
|
||||
|
||||
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,
|
||||
ocr: capture.ocr,
|
||||
},
|
||||
parsed,
|
||||
});
|
||||
|
||||
const learned = deriveScannerLearningRules(capture, parsed);
|
||||
const learnedResult = await mergeLearningRules(learned, scannerLearningRules, context);
|
||||
const learnedCount = countScannerLearningRules(learned);
|
||||
const activeRules = learnedResult?.merged ?? scannerLearningRules;
|
||||
const recoveredParsed = parseLearnedArtifact(capture, activeRules);
|
||||
const recoveredNeedsReview = shouldFlagArtifactForReview(recoveredParsed);
|
||||
let recoveredToDb = false;
|
||||
if (recoveredParsed && shouldPersistParsedArtifact(recoveredParsed, recoveredNeedsReview)) {
|
||||
const extendedContext: ReviewStateContext = {
|
||||
...context,
|
||||
artifactRepo: context.artifactRepo,
|
||||
onStoredArtifactsChanged,
|
||||
};
|
||||
recoveredToDb = await persistParsedArtifact(capture, recoveredParsed, "review-recovered", recoveredNeedsReview, extendedContext);
|
||||
}
|
||||
|
||||
setReviewStatus(
|
||||
result?.path
|
||||
? `Review-Sample gespeichert: ${result.path}${learnedCount > 0 ? ` - ${learnedCount} lokale Lernregeln aktualisiert` : ""}${recoveredToDb ? " - Artifact direkt in DB nachgezogen" : ""}`
|
||||
: "Review-Sample konnte nicht gespeichert werden.",
|
||||
);
|
||||
if (learnedResult?.result?.ok) appendAutomationLog(`learning: ${learnedResult.result.total} aktive Textregeln`);
|
||||
if (recoveredToDb && recoveredParsed) appendAutomationLog(`review recovered: ${recoveredParsed.name} +${recoveredParsed.level}`);
|
||||
|
||||
const reviewResult = await loadReviewSamplesAndSetTotal(context, REVIEW_SAMPLE_LIMIT_QUEUE);
|
||||
if (reviewResult?.ok) {
|
||||
setReviewSamples(reviewResult.samples);
|
||||
}
|
||||
|
||||
return { result, recoveredParsed, recoveredToDb };
|
||||
}
|
||||
|
||||
export async function loadReviewQueue(context: ReviewStateContext): Promise<void> {
|
||||
const { setReviewSamples } = context;
|
||||
const result = await loadReviewSamplesAndSetTotal(context, REVIEW_SAMPLE_LIMIT_QUEUE);
|
||||
if (!result?.ok) return;
|
||||
setReviewSamples(result.samples);
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
import { automationBlockReason, requiresAdminForAutomation } from "../../../lib/automationPlanner";
|
||||
import { captureRejectionReason } from "../../../lib/scannerCaptureQuality";
|
||||
import { runAutoScanLoop } from "../../../lib/autoScanLoop";
|
||||
import { clampScanLimit, emptyAutoScanStats, resolveScanTargetCount, type AutoScanStats, type ScanSummary } from "../../../lib/scannerSession";
|
||||
import { getAutoReviewReason, wait } from "../../../lib/scanReviewUtils";
|
||||
import type { AutomationRepositoryPort, RuntimeRepositoryPort } from "../../../infrastructure/repositories/rendererBridgeRepositories";
|
||||
import type { AutomationGuard, BooleanResult, CaptureOptions, CaptureResult, ClickResult, RuntimeInfo, ScrollResult } from "../../../types/global";
|
||||
import type { ParsedArtifactCandidate } from "../../../lib/artifactOcrParser";
|
||||
import type { MutableRefObject } from "react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
|
||||
export interface ScanActionContext {
|
||||
autoScanRunning: boolean;
|
||||
setAutoScanRunning: Dispatch<SetStateAction<boolean>>;
|
||||
isScanning: boolean;
|
||||
stopVisibleScanRef: MutableRefObject<boolean>;
|
||||
selectedSourceId: string;
|
||||
bridgeReady: boolean;
|
||||
automationRepo?: AutomationRepositoryPort;
|
||||
runtimeRepo?: RuntimeRepositoryPort;
|
||||
runtimeInfo?: RuntimeInfo | null;
|
||||
scanLimit: number;
|
||||
skipRows: number;
|
||||
detectedInventoryCount: number;
|
||||
setScanSummary: Dispatch<SetStateAction<ScanSummary | null>>;
|
||||
setAutoScanStats: Dispatch<SetStateAction<AutoScanStats>>;
|
||||
setReviewStatus: Dispatch<SetStateAction<string>>;
|
||||
appendAutomationLog: (line: string) => void;
|
||||
appendClickDiagnostics: (result: ClickResult, prefix?: string) => void;
|
||||
captureSelectedSource: (delayMs?: number, focusGenshin?: boolean, options?: CaptureOptions) => Promise<CaptureResult | null>;
|
||||
captureFastSelectedSource: (delayMs?: number, focusGenshin?: boolean) => 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>;
|
||||
shouldFlagArtifactForReview: (parsed: ParsedArtifactCandidate | null) => boolean;
|
||||
focusDashboard: () => Promise<void>;
|
||||
}
|
||||
|
||||
function buildScanSignature(parsed: ParsedArtifactCandidate) {
|
||||
return `${parsed.name}-${parsed.slot}-${parsed.mainStat}-${parsed.mainValue}-${parsed.level ?? 0}`;
|
||||
}
|
||||
|
||||
export async function runAutoReviewScan(context: ScanActionContext): Promise<void> {
|
||||
const {
|
||||
autoScanRunning,
|
||||
selectedSourceId,
|
||||
stopVisibleScanRef,
|
||||
setAutoScanRunning,
|
||||
setReviewStatus,
|
||||
setScanSummary,
|
||||
setAutoScanStats,
|
||||
appendAutomationLog,
|
||||
scanLimit,
|
||||
detectedInventoryCount,
|
||||
captureSelectedSource,
|
||||
parseArtifact,
|
||||
persistParsedArtifact,
|
||||
saveReviewSample,
|
||||
shouldFlagArtifactForReview,
|
||||
focusDashboard,
|
||||
} = context;
|
||||
|
||||
if (autoScanRunning || !selectedSourceId) return;
|
||||
|
||||
setAutoScanRunning(true);
|
||||
stopVisibleScanRef.current = false;
|
||||
setScanSummary(null);
|
||||
setAutoScanStats(emptyAutoScanStats);
|
||||
setReviewStatus("Manueller Scan laeuft. Klicke in Genshin auf ein anderes Artifact; nur neue Artifacts werden verarbeitet.");
|
||||
|
||||
const seen = new Set<string>();
|
||||
const stats: AutoScanStats = { ...emptyAutoScanStats, pages: 1 };
|
||||
let idleTicks = 0;
|
||||
const maxArtifacts = resolveScanTargetCount(scanLimit, detectedInventoryCount);
|
||||
const maxIdleTicks = 90;
|
||||
|
||||
while (!stopVisibleScanRef.current && stats.parsed < maxArtifacts && idleTicks < maxIdleTicks) {
|
||||
const capture = await captureSelectedSource(0, true);
|
||||
const rejection = captureRejectionReason(capture, parseArtifact(capture));
|
||||
const parsed = parseArtifact(capture);
|
||||
|
||||
if (!capture || !parsed || rejection) {
|
||||
if (capture && rejection) {
|
||||
await saveReviewSample(capture, parsed, `manual:capture-rejected`);
|
||||
stats.review++;
|
||||
setAutoScanStats({ ...stats });
|
||||
}
|
||||
idleTicks++;
|
||||
setReviewStatus(`Manueller Scan wartet auf ein lesbares Artifact... (${stats.parsed}/${maxArtifacts})${rejection ? ` ${rejection}` : ""}`);
|
||||
await wait(700);
|
||||
continue;
|
||||
}
|
||||
|
||||
const signature = buildScanSignature(parsed);
|
||||
if (seen.has(signature)) {
|
||||
idleTicks++;
|
||||
setReviewStatus(`Manueller Scan wartet auf ein neues Artifact... (${stats.parsed}/${maxArtifacts})`);
|
||||
await wait(700);
|
||||
continue;
|
||||
}
|
||||
|
||||
seen.add(signature);
|
||||
idleTicks = 0;
|
||||
stats.attempted++;
|
||||
stats.verified++;
|
||||
stats.parsed++;
|
||||
|
||||
const reason = getAutoReviewReason(capture, parsed);
|
||||
const needsReview = shouldFlagArtifactForReview(parsed);
|
||||
if (reason) {
|
||||
await saveReviewSample(capture, parsed, `manual:${reason}`);
|
||||
stats.review++;
|
||||
}
|
||||
if (await persistParsedArtifact(capture, parsed, "manual-scan", needsReview)) {
|
||||
stats.stored++;
|
||||
}
|
||||
setAutoScanStats({ ...stats });
|
||||
setReviewStatus(`Manueller Scan: neues Artifact erkannt (${stats.parsed}/${maxArtifacts}). Klicke das naechste Artifact an oder druecke Stop.`);
|
||||
await wait(700);
|
||||
}
|
||||
|
||||
setAutoScanRunning(false);
|
||||
const status: ScanSummary["status"] = stopVisibleScanRef.current ? "stopped" : "done";
|
||||
const idleSuffix = idleTicks >= maxIdleTicks ? " Keine neuen Artifacts erkannt; manueller Scan beendet." : "";
|
||||
await focusDashboard();
|
||||
setReviewStatus(
|
||||
`Manueller Scan ${status}. ${stats.verified} neue Ansichten verifiziert, ${stats.parsed} Artifacts gelesen, ${stats.stored} in der Datenbank gespeichert, ${stats.review} Review-Samples.${idleSuffix}`,
|
||||
);
|
||||
setScanSummary({
|
||||
mode: "Manueller Scan",
|
||||
status,
|
||||
...stats,
|
||||
targetCount: maxArtifacts,
|
||||
gridLabel: "Nur neue, vom User angeklickte Artifacts wurden verarbeitet.",
|
||||
});
|
||||
|
||||
appendAutomationLog(`manual scan finished: ${stats.parsed} parsed, ${stats.stored} stored, ${stats.review} review`);
|
||||
}
|
||||
|
||||
export async function runVisibleGridScan(context: ScanActionContext): Promise<void> {
|
||||
const {
|
||||
autoScanRunning,
|
||||
bridgeReady,
|
||||
selectedSourceId,
|
||||
runtimeInfo,
|
||||
automationRepo,
|
||||
runtimeRepo,
|
||||
stopVisibleScanRef,
|
||||
setAutoScanRunning,
|
||||
setScanSummary,
|
||||
setAutoScanStats,
|
||||
setReviewStatus,
|
||||
appendAutomationLog,
|
||||
appendClickDiagnostics,
|
||||
captureSelectedSource,
|
||||
captureFastSelectedSource,
|
||||
parseArtifact,
|
||||
persistParsedArtifact,
|
||||
saveReviewSample,
|
||||
shouldFlagArtifactForReview,
|
||||
scanLimit,
|
||||
skipRows,
|
||||
detectedInventoryCount,
|
||||
focusDashboard,
|
||||
} = context;
|
||||
|
||||
if (autoScanRunning || !bridgeReady || !selectedSourceId || !automationRepo?.clickScreen || !automationRepo?.scrollScreen) return;
|
||||
|
||||
const requiresAdminForAutoScan = requiresAdminForAutomation(runtimeInfo);
|
||||
if (requiresAdminForAutoScan) {
|
||||
setReviewStatus(
|
||||
"App laeuft nicht als Administrator. Bitte die App schliessen und als Administrator neu starten - Auto-Scan braucht Administrator-Rechte, damit Windows die simulierten Eingaben an Genshin nicht blockiert.",
|
||||
);
|
||||
setScanSummary({
|
||||
mode: "Automatischer Scan",
|
||||
status: "blocked",
|
||||
...emptyAutoScanStats,
|
||||
targetCount: resolveScanTargetCount(scanLimit, detectedInventoryCount),
|
||||
gridLabel: "App laeuft nicht als Administrator. Neustart als Administrator noetig.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setAutoScanRunning(true);
|
||||
stopVisibleScanRef.current = false;
|
||||
setScanSummary(null);
|
||||
setAutoScanStats(emptyAutoScanStats);
|
||||
setReviewStatus("Automatischer Scan startet. ESC gedrueckt halten oder Stop klicken beendet den Scan sofort.");
|
||||
|
||||
const freshRuntime = await runtimeRepo?.getRuntimeInfo().catch(() => null);
|
||||
const adminBlockReason = automationBlockReason(freshRuntime);
|
||||
if (adminBlockReason) {
|
||||
setAutoScanRunning(false);
|
||||
setReviewStatus(adminBlockReason);
|
||||
appendAutomationLog("blocked: App laeuft nicht als Administrator, keine In-Game-Klicks ausgefuehrt");
|
||||
setScanSummary({
|
||||
mode: "Automatischer Scan",
|
||||
status: "blocked",
|
||||
...emptyAutoScanStats,
|
||||
targetCount: resolveScanTargetCount(scanLimit, detectedInventoryCount),
|
||||
gridLabel: adminBlockReason,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (freshRuntime) {
|
||||
const required = freshRuntime.genshinFound ? `found:${freshRuntime.targetProcess || "genshin"}` : "not-found";
|
||||
appendAutomationLog(`runtime ping: elevated=${freshRuntime.isElevated} ${required}`);
|
||||
}
|
||||
|
||||
setReviewStatus("Genshin wird in den Vordergrund geholt...");
|
||||
const focusResult = await automationRepo?.focusGenshin().catch(() => null);
|
||||
if (focusResult) {
|
||||
appendAutomationLog(
|
||||
`focus: ${focusResult.focused ? "ok" : "fehlgeschlagen"} found:${focusResult.genshinFound ? "yes" : "no"} setForeground:${focusResult.setForegroundResult ?? "n/a"} target:${focusResult.targetProcess || "?"} fg:${focusResult.foregroundProcess || "?"}`,
|
||||
);
|
||||
}
|
||||
if (!focusResult?.focused) {
|
||||
setAutoScanRunning(false);
|
||||
const reason = !focusResult?.genshinFound
|
||||
? "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);
|
||||
setScanSummary({
|
||||
mode: "Automatischer Scan",
|
||||
status: "blocked",
|
||||
...emptyAutoScanStats,
|
||||
targetCount: resolveScanTargetCount(scanLimit, detectedInventoryCount),
|
||||
gridLabel: reason,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setReviewStatus("Genshin ist im Vordergrund. Automatischer Scan startet. ESC gedrueckt halten oder Stop klicken beendet den Scan sofort.");
|
||||
|
||||
const result = await runAutoScanLoop(
|
||||
{
|
||||
api: {
|
||||
clickScreen: (x: number, y: number) => {
|
||||
if (!automationRepo?.clickScreen) {
|
||||
return Promise.resolve<ClickResult>({
|
||||
ok: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
clicked: false,
|
||||
moved: false,
|
||||
focused: false,
|
||||
inputBlocked: false,
|
||||
});
|
||||
}
|
||||
return automationRepo.clickScreen(x, y);
|
||||
},
|
||||
scrollScreen: (notches: number, anchorX?: number, anchorY?: number) => {
|
||||
if (!automationRepo?.scrollScreen) {
|
||||
return Promise.resolve<ScrollResult>({ ok: false, notchesSent: 0, inputBlocked: false });
|
||||
}
|
||||
return automationRepo.scrollScreen(notches, anchorX, anchorY);
|
||||
},
|
||||
getAutomationGuard: () =>
|
||||
automationRepo?.getAutomationGuard?.() ??
|
||||
Promise.resolve<AutomationGuard>({ ok: false, escapePressed: false, enterPressed: false, f9Pressed: false }),
|
||||
},
|
||||
captureSelectedSource: (delayMs = 0, focusGenshin = false) => captureSelectedSource(delayMs, focusGenshin),
|
||||
captureFastSelectedSource,
|
||||
parseArtifact,
|
||||
persistParsedArtifact,
|
||||
saveReviewSample,
|
||||
getAutoReviewReason,
|
||||
shouldFlagArtifactForReview,
|
||||
appendAutomationLog,
|
||||
appendClickDiagnostics,
|
||||
setReviewStatus,
|
||||
setAutoScanStats,
|
||||
shouldStop: () => stopVisibleScanRef.current,
|
||||
},
|
||||
{
|
||||
scanLimit,
|
||||
skipRows,
|
||||
detectedInventoryCount,
|
||||
},
|
||||
);
|
||||
|
||||
setAutoScanRunning(false);
|
||||
if (result.blockedReason) appendAutomationLog(`stop: ${result.blockedReason}`);
|
||||
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",
|
||||
status: result.status,
|
||||
...result.stats,
|
||||
targetCount: result.targetCount,
|
||||
gridLabel: result.gridLabel,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useEffect } from "react";
|
||||
import type { AutomationRepositoryPort } from "../../../infrastructure/repositories/rendererBridgeRepositoryTypes";
|
||||
|
||||
interface ScanCommandListenerInput {
|
||||
automationRepo?: AutomationRepositoryPort;
|
||||
autoScanRunning: boolean;
|
||||
isScanning: boolean;
|
||||
selectedSourceId: string;
|
||||
requestScanStop: (reason: string) => void;
|
||||
runVisibleGridScan: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function useScanCommandListener({
|
||||
automationRepo,
|
||||
autoScanRunning,
|
||||
isScanning,
|
||||
selectedSourceId,
|
||||
requestScanStop,
|
||||
runVisibleGridScan,
|
||||
}: ScanCommandListenerInput) {
|
||||
useEffect(() => {
|
||||
if (!automationRepo?.onCommand) return;
|
||||
return automationRepo.onCommand((command: "start-auto" | "stop") => {
|
||||
if (command === "stop") {
|
||||
requestScanStop("Hotkey/Dev-Stop gedrueckt.");
|
||||
return;
|
||||
}
|
||||
if (command === "start-auto" && !autoScanRunning && !isScanning && selectedSourceId) {
|
||||
void runVisibleGridScan();
|
||||
}
|
||||
});
|
||||
}, [automationRepo, autoScanRunning, isScanning, selectedSourceId, requestScanStop, runVisibleGridScan]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { RuntimeInfo } from "../../../types/global";
|
||||
import type { RuntimeRepositoryPort } from "../../../infrastructure/repositories/rendererBridgeRepositoryTypes";
|
||||
|
||||
export function useScanRuntimeInfo(runtimeRepo?: RuntimeRepositoryPort) {
|
||||
const [runtimeInfo, setRuntimeInfo] = useState<RuntimeInfo | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
runtimeRepo?.getRuntimeInfo().then((result) => {
|
||||
if (mounted) {
|
||||
setRuntimeInfo(result);
|
||||
}
|
||||
}).catch(() => undefined);
|
||||
|
||||
const timer = window.setInterval(() => {
|
||||
runtimeRepo?.getRuntimeInfo().then((result) => {
|
||||
if (mounted) setRuntimeInfo(result);
|
||||
}).catch(() => undefined);
|
||||
}, 5000);
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
window.clearInterval(timer);
|
||||
};
|
||||
}, [runtimeRepo]);
|
||||
|
||||
return runtimeInfo;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { useEffect } from "react";
|
||||
import { type AppSnapshot } from "../../../types/domain";
|
||||
import { type AutoScanStats, type ScanSummary } from "../../../lib/scannerSession";
|
||||
import type { RuntimeInfo } from "../../../types/global";
|
||||
import type { SnapshotRepositoryPort } from "../../../infrastructure/repositories/rendererBridgeRepositoryTypes";
|
||||
import type { CaptureResult } from "../../../types/global";
|
||||
|
||||
type InventoryGrid = NonNullable<CaptureResult["inventoryGrid"]>;
|
||||
|
||||
interface ScanSnapshotPublisherInput {
|
||||
autoScanRunning: boolean;
|
||||
reviewStatus: string;
|
||||
captureStatus: string;
|
||||
selectedSourceName: string | null;
|
||||
autoScanStats: AutoScanStats;
|
||||
scanSummary: ScanSummary | null;
|
||||
snapshot: AppSnapshot;
|
||||
latestInventoryGrid: InventoryGrid | null | undefined;
|
||||
automationLog: string[];
|
||||
runtimeInfo: RuntimeInfo | null;
|
||||
storedTotal: number | null;
|
||||
learningRuleCount: number;
|
||||
snapshotRepo?: SnapshotRepositoryPort;
|
||||
}
|
||||
|
||||
export function useScanSnapshotPublisher({
|
||||
autoScanRunning,
|
||||
reviewStatus,
|
||||
captureStatus,
|
||||
selectedSourceName,
|
||||
autoScanStats,
|
||||
scanSummary,
|
||||
snapshot,
|
||||
latestInventoryGrid,
|
||||
automationLog,
|
||||
runtimeInfo,
|
||||
storedTotal,
|
||||
learningRuleCount,
|
||||
snapshotRepo,
|
||||
}: ScanSnapshotPublisherInput) {
|
||||
useEffect(() => {
|
||||
void snapshotRepo?.publishScannerStatus({
|
||||
running: autoScanRunning,
|
||||
reviewStatus,
|
||||
captureStatus,
|
||||
selectedSource: selectedSourceName,
|
||||
stats: autoScanStats,
|
||||
summary: scanSummary,
|
||||
snapshotArtifacts: snapshot.artifacts.length,
|
||||
snapshotCharacters: snapshot.characters.filter((character) => character.owned).length,
|
||||
snapshotRecommendations: snapshot.recommendations.length,
|
||||
snapshotBuilds: snapshot.builds.length,
|
||||
grid: latestInventoryGrid ?? null,
|
||||
automationLog: automationLog.slice(-12),
|
||||
runtimeInfo,
|
||||
storedTotal,
|
||||
learningRuleCount,
|
||||
updatedAt: new Date().toISOString(),
|
||||
}).catch(() => undefined);
|
||||
}, [
|
||||
autoScanRunning,
|
||||
reviewStatus,
|
||||
captureStatus,
|
||||
selectedSourceName,
|
||||
autoScanStats,
|
||||
scanSummary,
|
||||
snapshot,
|
||||
latestInventoryGrid,
|
||||
automationLog,
|
||||
runtimeInfo,
|
||||
storedTotal,
|
||||
learningRuleCount,
|
||||
snapshotRepo,
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import type { Dispatch, MutableRefObject, SetStateAction } from "react";
|
||||
import { runAutoReviewScan as runAutoReviewScanAction, runVisibleGridScan as runVisibleGridScanAction } from "./scanViewScanActions";
|
||||
import {
|
||||
initializeLearningState,
|
||||
loadReviewQueue as loadReviewQueueFromRepo,
|
||||
persistParsedArtifact as persistParsedArtifactHelper,
|
||||
saveReviewSample as saveReviewSampleHelper,
|
||||
} from "./scanViewReviewHelpers";
|
||||
import { createReviewContext, createScanActionContext } from "./scanViewControllerService";
|
||||
import { useScanCommandListener } from "./useScanCommandListener";
|
||||
import type { ParsedArtifactCandidate } from "../../../lib/artifactOcrParser";
|
||||
import type { ScannerLearningRules } from "../../../lib/scannerLearning";
|
||||
import type { BooleanResult, CaptureOptions, CaptureResult, ClickResult, ReviewSampleRecord } from "../../../types/global";
|
||||
import type {
|
||||
ArtifactRepositoryPort,
|
||||
ReviewSampleRepositoryPort,
|
||||
LearningRepositoryPort,
|
||||
RuntimeRepositoryPort,
|
||||
AutomationRepositoryPort,
|
||||
} from "../../../infrastructure/repositories/rendererBridgeRepositoryTypes";
|
||||
import type { AutoScanStats, ScanSummary } from "../../../lib/scannerSession";
|
||||
import type { RuntimeInfo } from "../../../types/global";
|
||||
|
||||
type BooleanSetter = Dispatch<SetStateAction<boolean>>;
|
||||
type NumberSetter = Dispatch<SetStateAction<number>>;
|
||||
type StringSetter = Dispatch<SetStateAction<string>>;
|
||||
type NumberOrNullSetter = Dispatch<SetStateAction<number | null>>;
|
||||
type ScannerRulesSetter = Dispatch<SetStateAction<ScannerLearningRules>>;
|
||||
type ReviewSamplesSetter = Dispatch<SetStateAction<ReviewSampleRecord[]>>;
|
||||
type ScanSummarySetter = Dispatch<SetStateAction<ScanSummary | null>>;
|
||||
type AutoScanStatsSetter = Dispatch<SetStateAction<AutoScanStats>>;
|
||||
|
||||
interface ScanViewActionInput {
|
||||
autoScanRunning: boolean;
|
||||
setAutoScanRunning: BooleanSetter;
|
||||
isScanning: boolean;
|
||||
stopVisibleScanRef: MutableRefObject<boolean>;
|
||||
selectedSourceId: string;
|
||||
bridgeReady: boolean;
|
||||
automationRepo?: AutomationRepositoryPort;
|
||||
runtimeInfo?: RuntimeInfo | null;
|
||||
runtimeRepo?: RuntimeRepositoryPort;
|
||||
scanLimit: number;
|
||||
skipRows: number;
|
||||
detectedInventoryCount: number;
|
||||
setScanSummary: ScanSummarySetter;
|
||||
setAutoScanStats: AutoScanStatsSetter;
|
||||
setReviewStatus: StringSetter;
|
||||
appendAutomationLog: (line: string) => void;
|
||||
appendClickDiagnostics: (result: ClickResult, prefix?: string) => void;
|
||||
parseArtifact: (capture: CaptureResult | null) => ParsedArtifactCandidate | null;
|
||||
artifactRepo?: ArtifactRepositoryPort;
|
||||
reviewSamplesRepo?: ReviewSampleRepositoryPort;
|
||||
learningRepo?: LearningRepositoryPort;
|
||||
onStoredArtifactsChanged?: (() => Promise<void>) | (() => void);
|
||||
setReviewSampleTotal: NumberSetter;
|
||||
setReviewSamples: ReviewSamplesSetter;
|
||||
setLearningRulesLoaded: BooleanSetter;
|
||||
setScannerLearningRules: ScannerRulesSetter;
|
||||
setStoredTotal: NumberOrNullSetter;
|
||||
scannerLearningRules: ScannerLearningRules;
|
||||
captureSelectedSource: (delayMs?: number, focusGenshin?: boolean, options?: CaptureOptions) => Promise<CaptureResult | null>;
|
||||
latestCapture: CaptureResult | null;
|
||||
parsedArtifact: ParsedArtifactCandidate | null;
|
||||
canCaptureSource: boolean;
|
||||
setReviewQueueOpen: BooleanSetter;
|
||||
}
|
||||
|
||||
export interface ScanViewActionResult {
|
||||
requestScanStop: (reason?: string) => void;
|
||||
saveReviewSample: (
|
||||
capture?: CaptureResult | null,
|
||||
parsed?: ParsedArtifactCandidate | null,
|
||||
reason?: string,
|
||||
) => Promise<BooleanResult | null>;
|
||||
loadReviewQueue: () => Promise<void>;
|
||||
openReviewQueue: () => Promise<void>;
|
||||
runAutoReviewScan: () => Promise<void>;
|
||||
runVisibleGridScan: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function useScanViewActions(input: ScanViewActionInput): ScanViewActionResult {
|
||||
const {
|
||||
autoScanRunning,
|
||||
setAutoScanRunning,
|
||||
isScanning,
|
||||
stopVisibleScanRef,
|
||||
selectedSourceId,
|
||||
bridgeReady,
|
||||
automationRepo,
|
||||
runtimeInfo,
|
||||
runtimeRepo,
|
||||
scanLimit,
|
||||
skipRows,
|
||||
detectedInventoryCount,
|
||||
setScanSummary,
|
||||
setAutoScanStats,
|
||||
setReviewStatus,
|
||||
appendAutomationLog,
|
||||
appendClickDiagnostics,
|
||||
parseArtifact,
|
||||
artifactRepo,
|
||||
reviewSamplesRepo,
|
||||
learningRepo,
|
||||
onStoredArtifactsChanged,
|
||||
setReviewSampleTotal,
|
||||
setReviewSamples,
|
||||
setLearningRulesLoaded,
|
||||
setScannerLearningRules,
|
||||
setStoredTotal,
|
||||
scannerLearningRules,
|
||||
captureSelectedSource,
|
||||
latestCapture,
|
||||
parsedArtifact,
|
||||
canCaptureSource,
|
||||
setReviewQueueOpen,
|
||||
} = input;
|
||||
|
||||
const requestScanStop = useCallback((reason = "Stop angefordert.") => {
|
||||
stopVisibleScanRef.current = true;
|
||||
appendAutomationLog(`stop requested: ${reason}`);
|
||||
setReviewStatus(`${reason} Der aktuelle Klick/Capture-Schritt wird noch sauber beendet.`);
|
||||
}, [appendAutomationLog, setReviewStatus, stopVisibleScanRef]);
|
||||
|
||||
const reviewContext = useMemo(
|
||||
() =>
|
||||
createReviewContext({
|
||||
artifactRepo,
|
||||
reviewSamplesRepo,
|
||||
learningRepo,
|
||||
onStoredArtifactsChanged,
|
||||
setReviewSampleTotal,
|
||||
setReviewSamples,
|
||||
setLearningRulesLoaded,
|
||||
setScannerLearningRules,
|
||||
setReviewStatus,
|
||||
setStoredTotal,
|
||||
appendAutomationLog,
|
||||
}),
|
||||
[
|
||||
artifactRepo,
|
||||
reviewSamplesRepo,
|
||||
learningRepo,
|
||||
onStoredArtifactsChanged,
|
||||
setReviewSampleTotal,
|
||||
setReviewSamples,
|
||||
setLearningRulesLoaded,
|
||||
setScannerLearningRules,
|
||||
setReviewStatus,
|
||||
setStoredTotal,
|
||||
appendAutomationLog,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
void initializeLearningState(reviewContext);
|
||||
}, [reviewContext]);
|
||||
|
||||
const focusDashboard = useCallback(async () => {
|
||||
try {
|
||||
await automationRepo?.focusMainWindow?.();
|
||||
} catch {
|
||||
// Best-effort fallback; state remains in renderer.
|
||||
}
|
||||
}, [automationRepo?.focusMainWindow]);
|
||||
|
||||
const parseArtifactAndPersist = useCallback(
|
||||
async function parseArtifactAndPersist(
|
||||
capture: CaptureResult | null,
|
||||
parsed: ParsedArtifactCandidate,
|
||||
source: string,
|
||||
needsReview: boolean,
|
||||
) {
|
||||
return persistParsedArtifactHelper(capture, parsed, source, needsReview, reviewContext);
|
||||
},
|
||||
[reviewContext],
|
||||
);
|
||||
|
||||
const handleSaveReviewSample = useCallback(
|
||||
async function handleSaveReviewSample(
|
||||
capture: CaptureResult | null = latestCapture,
|
||||
parsed: ParsedArtifactCandidate | null = parsedArtifact,
|
||||
reason = "manual",
|
||||
): Promise<BooleanResult | null> {
|
||||
const result = await saveReviewSampleHelper(
|
||||
capture,
|
||||
parsed,
|
||||
reason,
|
||||
scannerLearningRules,
|
||||
reviewContext,
|
||||
);
|
||||
return result.result ? { ok: result.result.ok } : null;
|
||||
},
|
||||
[latestCapture, parsedArtifact, reviewContext, scannerLearningRules],
|
||||
);
|
||||
|
||||
const scanActionContext = useMemo(
|
||||
() =>
|
||||
createScanActionContext({
|
||||
autoScanRunning,
|
||||
setAutoScanRunning,
|
||||
isScanning,
|
||||
stopVisibleScanRef,
|
||||
selectedSourceId,
|
||||
bridgeReady,
|
||||
automationRepo,
|
||||
runtimeInfo,
|
||||
runtimeRepo,
|
||||
scanLimit,
|
||||
skipRows,
|
||||
detectedInventoryCount,
|
||||
setScanSummary,
|
||||
setAutoScanStats,
|
||||
setReviewStatus,
|
||||
appendAutomationLog,
|
||||
appendClickDiagnostics,
|
||||
parseArtifact,
|
||||
persistParsedArtifact: parseArtifactAndPersist,
|
||||
saveReviewSample: handleSaveReviewSample,
|
||||
focusDashboard,
|
||||
captureSelectedSource,
|
||||
captureFastSelectedSource: (delayMs = 0, focusGenshin = false) => captureSelectedSource(delayMs, focusGenshin, { skipOcr: true }),
|
||||
}),
|
||||
[
|
||||
autoScanRunning,
|
||||
setAutoScanRunning,
|
||||
isScanning,
|
||||
stopVisibleScanRef,
|
||||
selectedSourceId,
|
||||
bridgeReady,
|
||||
automationRepo,
|
||||
runtimeInfo,
|
||||
runtimeRepo,
|
||||
scanLimit,
|
||||
skipRows,
|
||||
detectedInventoryCount,
|
||||
setScanSummary,
|
||||
setAutoScanStats,
|
||||
setReviewStatus,
|
||||
appendAutomationLog,
|
||||
appendClickDiagnostics,
|
||||
parseArtifact,
|
||||
parseArtifactAndPersist,
|
||||
handleSaveReviewSample,
|
||||
focusDashboard,
|
||||
captureSelectedSource,
|
||||
],
|
||||
);
|
||||
|
||||
const loadReviewQueueAction = useCallback(async () => {
|
||||
await loadReviewQueueFromRepo(reviewContext);
|
||||
}, [reviewContext]);
|
||||
|
||||
const openReviewQueueModal = useCallback(async () => {
|
||||
await loadReviewQueueAction();
|
||||
setReviewQueueOpen(true);
|
||||
}, [loadReviewQueueAction, setReviewQueueOpen]);
|
||||
|
||||
const runAutoReviewScan = useCallback(async () => {
|
||||
if (autoScanRunning || !selectedSourceId || !canCaptureSource) return;
|
||||
await runAutoReviewScanAction(scanActionContext);
|
||||
}, [autoScanRunning, canCaptureSource, selectedSourceId, scanActionContext]);
|
||||
|
||||
const runVisibleGridScan = useCallback(async () => {
|
||||
if (autoScanRunning || !bridgeReady || !selectedSourceId || !automationRepo?.clickScreen || !automationRepo?.scrollScreen) {
|
||||
return;
|
||||
}
|
||||
await runVisibleGridScanAction(scanActionContext);
|
||||
}, [
|
||||
autoScanRunning,
|
||||
bridgeReady,
|
||||
selectedSourceId,
|
||||
automationRepo?.clickScreen,
|
||||
automationRepo?.scrollScreen,
|
||||
scanActionContext,
|
||||
]);
|
||||
|
||||
useScanCommandListener({
|
||||
automationRepo,
|
||||
autoScanRunning,
|
||||
isScanning,
|
||||
selectedSourceId,
|
||||
requestScanStop,
|
||||
runVisibleGridScan,
|
||||
});
|
||||
|
||||
return {
|
||||
requestScanStop,
|
||||
saveReviewSample: handleSaveReviewSample,
|
||||
loadReviewQueue: loadReviewQueueAction,
|
||||
openReviewQueue: openReviewQueueModal,
|
||||
runAutoReviewScan,
|
||||
runVisibleGridScan,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
import { requiresAdminForAutomation } from "../../../lib/automationPlanner";
|
||||
import {
|
||||
countScannerLearningRules,
|
||||
type ScannerLearningRules,
|
||||
} from "../../../lib/scannerLearning";
|
||||
import { type ParsedArtifactCandidate } from "../../../lib/artifactOcrParser";
|
||||
import { analyzeReviewSamples } from "../../../lib/reviewSampleAnalysis";
|
||||
import {
|
||||
parseLearnedArtifact as parseLearnedArtifactHelper,
|
||||
} from "./scanViewReviewHelpers";
|
||||
import { useScanRuntimeInfo } from "./useScanRuntimeInfo";
|
||||
import { useScanSnapshotPublisher } from "./useScanSnapshotPublisher";
|
||||
import { useScanViewActions } from "./useScanViewActions";
|
||||
import { useScanViewStateSync } from "./useScanViewStateSync";
|
||||
import { emptyAutoScanStats, resolveScanTargetCount, type AutoScanStats, type ScanSummary } from "../../../lib/scannerSession";
|
||||
import type { ScanViewProps, ScanViewControllerResult } from "../types";
|
||||
import type { CaptureResult, ClickResult, ReviewSampleRecord } from "../../../types/global";
|
||||
import { createRendererRepositories } from "../../../infrastructure/repositories/rendererBridgeRepositories";
|
||||
|
||||
export function useScanViewController({
|
||||
snapshot,
|
||||
isScanning,
|
||||
captureSources,
|
||||
selectedSourceId,
|
||||
latestCapture,
|
||||
captureStatus,
|
||||
captureSelectedSource,
|
||||
bridgeReady,
|
||||
onStoredArtifactsChanged,
|
||||
}: ScanViewProps): ScanViewControllerResult {
|
||||
const repositories = useMemo(() => createRendererRepositories(), []);
|
||||
const artifactRepo = repositories?.artifacts;
|
||||
const runtimeRepo = repositories?.runtime;
|
||||
const reviewSamplesRepo = repositories?.reviewSamples;
|
||||
const learningRepo = repositories?.learning;
|
||||
const snapshotRepo = repositories?.snapshot;
|
||||
const automationRepo = repositories?.automation;
|
||||
const captureRepo = repositories?.capture;
|
||||
|
||||
const [detailsOpen, setDetailsOpen] = useState(false);
|
||||
const [diagnosticsOpen, setDiagnosticsOpen] = useState(false);
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
const [reviewQueueOpen, setReviewQueueOpen] = useState(false);
|
||||
const [reviewSamples, setReviewSamples] = useState<ReviewSampleRecord[]>([]);
|
||||
const [reviewSampleTotal, setReviewSampleTotal] = useState(0);
|
||||
const [reviewStatus, setReviewStatus] = useState("");
|
||||
const [autoScanRunning, setAutoScanRunning] = useState(false);
|
||||
const stopVisibleScanRef = useRef(false);
|
||||
const [autoScanStats, setAutoScanStats] = useState<AutoScanStats>(emptyAutoScanStats);
|
||||
const [scanSummary, setScanSummary] = useState<ScanSummary | null>(null);
|
||||
const [scanLimit, setScanLimit] = useState(16);
|
||||
const [scanLimitTouched, setScanLimitTouched] = useState(false);
|
||||
const [skipRows, setSkipRows] = useState(0);
|
||||
const [automationLog, setAutomationLog] = useState<string[]>([]);
|
||||
const [storedTotal, setStoredTotal] = useState<number | null>(null);
|
||||
const [devMode, setDevMode] = useState(() => localStorage.getItem("gaa-dev-mode") === "1");
|
||||
const [scannerLearningRules, setScannerLearningRules] = useState<ScannerLearningRules>({ textReplacements: {} });
|
||||
const [learningRulesLoaded, setLearningRulesLoaded] = useState(false);
|
||||
const runtimeInfo = useScanRuntimeInfo(runtimeRepo);
|
||||
|
||||
const parsedArtifact = useMemo(
|
||||
() => parseLearnedArtifactHelper(latestCapture, scannerLearningRules),
|
||||
[latestCapture, scannerLearningRules],
|
||||
);
|
||||
|
||||
const selectedSource = captureSources.find((source) => source.id === selectedSourceId);
|
||||
const genshinSource = captureSources.find((source) => source.isGenshinCandidate);
|
||||
const requiresAdminForAutoScan = requiresAdminForAutomation(runtimeInfo);
|
||||
const hasSourceSelected = selectedSourceId.length > 0;
|
||||
const canCaptureSource = bridgeReady && Boolean(captureRepo?.captureSource);
|
||||
const canReadReviewQueue = bridgeReady && Boolean(reviewSamplesRepo?.loadSamples);
|
||||
const canSaveReviewSample = bridgeReady && Boolean(reviewSamplesRepo?.saveSample);
|
||||
const canAutoScan = bridgeReady && Boolean(automationRepo?.clickScreen) && Boolean(automationRepo?.scrollScreen);
|
||||
const reviewAnalysis = useMemo(() => analyzeReviewSamples(reviewSamples), [reviewSamples]);
|
||||
const learningRuleCount = countScannerLearningRules(scannerLearningRules);
|
||||
const detectedInventoryCount = latestCapture?.inventoryCount?.current ?? 0;
|
||||
const activeTargetCount = autoScanRunning
|
||||
? resolveScanTargetCount(scanLimit, detectedInventoryCount)
|
||||
: scanSummary?.targetCount ?? resolveScanTargetCount(scanLimit, detectedInventoryCount);
|
||||
const scanProgressBase = Math.max(autoScanStats.attempted, autoScanStats.parsed, autoScanStats.verified);
|
||||
const scanProgressPercent = Math.min(100, Math.round((scanProgressBase / Math.max(1, activeTargetCount)) * 100));
|
||||
const scannerModeLabel = autoScanRunning ? "Scan laeuft" : runtimeInfo?.isElevated ? "Admin bereit" : "Bereit";
|
||||
const sourceLabel = selectedSource?.name ?? "Keine Quelle";
|
||||
const gridLabel = latestCapture?.inventoryGrid ? `${latestCapture.inventoryGrid.cols} x ${latestCapture.inventoryGrid.rows}` : "warte auf Capture";
|
||||
const inventoryLabel = latestCapture?.inventoryCount?.current ? `${latestCapture.inventoryCount.current}/${latestCapture.inventoryCount.total || "?"}` : "nicht erkannt";
|
||||
|
||||
const appendAutomationLog = useCallback((line: string) => {
|
||||
setAutomationLog((previous) => [...previous.slice(-11), `${new Date().toLocaleTimeString()} ${line}`]);
|
||||
}, []);
|
||||
|
||||
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]);
|
||||
|
||||
const toggleDevMode = useCallback(() => {
|
||||
setDevMode((previous) => {
|
||||
const next = !previous;
|
||||
localStorage.setItem("gaa-dev-mode", next ? "1" : "0");
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const parseArtifact = useCallback(
|
||||
(capture: CaptureResult | null) => parseLearnedArtifactHelper(capture, scannerLearningRules),
|
||||
[scannerLearningRules],
|
||||
);
|
||||
|
||||
const {
|
||||
requestScanStop,
|
||||
saveReviewSample: handleSaveReviewSample,
|
||||
loadReviewQueue: loadReviewQueueAction,
|
||||
openReviewQueue: openReviewQueueModal,
|
||||
runAutoReviewScan,
|
||||
runVisibleGridScan,
|
||||
} = useScanViewActions({
|
||||
autoScanRunning,
|
||||
setAutoScanRunning,
|
||||
isScanning,
|
||||
stopVisibleScanRef,
|
||||
selectedSourceId,
|
||||
bridgeReady,
|
||||
automationRepo,
|
||||
runtimeInfo,
|
||||
runtimeRepo,
|
||||
scanLimit,
|
||||
skipRows,
|
||||
detectedInventoryCount,
|
||||
setScanSummary,
|
||||
setAutoScanStats,
|
||||
setReviewStatus,
|
||||
appendAutomationLog,
|
||||
appendClickDiagnostics,
|
||||
parseArtifact,
|
||||
artifactRepo,
|
||||
reviewSamplesRepo,
|
||||
learningRepo,
|
||||
onStoredArtifactsChanged,
|
||||
setReviewSampleTotal,
|
||||
setReviewSamples,
|
||||
setLearningRulesLoaded,
|
||||
setScannerLearningRules,
|
||||
setStoredTotal,
|
||||
scannerLearningRules,
|
||||
captureSelectedSource,
|
||||
latestCapture,
|
||||
parsedArtifact,
|
||||
canCaptureSource,
|
||||
setReviewQueueOpen,
|
||||
});
|
||||
|
||||
useScanViewStateSync({
|
||||
artifactRepo,
|
||||
latestCapture,
|
||||
scanLimitTouched,
|
||||
setScanLimit,
|
||||
setStoredTotal,
|
||||
});
|
||||
|
||||
useScanSnapshotPublisher({
|
||||
autoScanRunning,
|
||||
reviewStatus,
|
||||
captureStatus,
|
||||
selectedSourceName: selectedSource?.name ?? null,
|
||||
autoScanStats,
|
||||
scanSummary,
|
||||
snapshot,
|
||||
latestInventoryGrid: latestCapture?.inventoryGrid ?? null,
|
||||
automationLog,
|
||||
runtimeInfo,
|
||||
storedTotal,
|
||||
learningRuleCount,
|
||||
snapshotRepo,
|
||||
});
|
||||
|
||||
return {
|
||||
detailsOpen,
|
||||
diagnosticsOpen,
|
||||
settingsOpen,
|
||||
reviewQueueOpen,
|
||||
reviewSamples,
|
||||
reviewSampleTotal,
|
||||
reviewStatus,
|
||||
autoScanRunning,
|
||||
autoScanStats,
|
||||
scanSummary,
|
||||
scanLimit,
|
||||
scanLimitTouched,
|
||||
skipRows,
|
||||
automationLog,
|
||||
storedTotal,
|
||||
devMode,
|
||||
scannerLearningRules,
|
||||
learningRulesLoaded,
|
||||
runtimeInfo,
|
||||
parsedArtifact,
|
||||
reviewAnalysis,
|
||||
learningRuleCount,
|
||||
detectedInventoryCount,
|
||||
scanProgressPercent,
|
||||
activeTargetCount,
|
||||
requiresAdminForAutoScan,
|
||||
hasSourceSelected,
|
||||
canCaptureSource,
|
||||
canReadReviewQueue,
|
||||
canSaveReviewSample,
|
||||
canAutoScan,
|
||||
scannerModeLabel,
|
||||
sourceLabel,
|
||||
gridLabel,
|
||||
inventoryLabel,
|
||||
selectedSource,
|
||||
genshinSource,
|
||||
setDetailsOpen,
|
||||
setDiagnosticsOpen,
|
||||
setSettingsOpen,
|
||||
setReviewQueueOpen,
|
||||
setScanSummary,
|
||||
setScanLimit,
|
||||
setScanLimitTouched,
|
||||
setSkipRows,
|
||||
toggleDevMode,
|
||||
requestScanStop,
|
||||
saveReviewSample: handleSaveReviewSample,
|
||||
loadReviewQueue: loadReviewQueueAction,
|
||||
openReviewQueue: openReviewQueueModal,
|
||||
runAutoReviewScan,
|
||||
runVisibleGridScan,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useEffect } from "react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import type { CaptureResult } from "../../../types/global";
|
||||
import type { ArtifactRepositoryPort } from "../../../infrastructure/repositories/rendererBridgeRepositoryTypes";
|
||||
import { clampScanLimit } from "../../../lib/scannerSession";
|
||||
|
||||
interface ScanViewStateSyncInput {
|
||||
artifactRepo?: ArtifactRepositoryPort;
|
||||
latestCapture: CaptureResult | null;
|
||||
scanLimitTouched: boolean;
|
||||
setScanLimit: Dispatch<SetStateAction<number>>;
|
||||
setStoredTotal: Dispatch<SetStateAction<number | null>>;
|
||||
}
|
||||
|
||||
export function useScanViewStateSync({
|
||||
artifactRepo,
|
||||
latestCapture,
|
||||
scanLimitTouched,
|
||||
setScanLimit,
|
||||
setStoredTotal,
|
||||
}: ScanViewStateSyncInput) {
|
||||
useEffect(() => {
|
||||
const detectedCount = latestCapture?.inventoryCount?.current ?? 0;
|
||||
if (!scanLimitTouched && detectedCount > 0) {
|
||||
setScanLimit(clampScanLimit(detectedCount));
|
||||
}
|
||||
}, [latestCapture?.inventoryCount?.current, scanLimitTouched, setScanLimit]);
|
||||
|
||||
useEffect(() => {
|
||||
artifactRepo?.loadAll().then((result) => {
|
||||
if (result?.ok) setStoredTotal(result.total);
|
||||
}).catch(() => undefined);
|
||||
}, [artifactRepo, setStoredTotal]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user