Files
genshin-assistant/src/features/scan/components/DiagnosticsView.tsx
T
2026-07-09 10:21:13 +02:00

394 lines
16 KiB
TypeScript

import { useState } from "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";
import { useScanDiagnosticsModalModel } from "./modals/hooks/useScanDiagnosticsModalModel";
import { useScanDetailsModalModel } from "./modals/hooks/useScanDetailsModalModel";
interface DiagnosticsViewProps {
controller: ScanViewControllerResult;
latestCapture: CaptureResult | null;
captureStatus: string;
onDemoScan?: () => void;
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 Lernregeln, GOOD Import/Export, Equipped-Footer und Lock-Status im Store nutzen.",
],
},
{
title: "Was noch fehlt",
tone: "warn",
icon: ClipboardList,
items: [
"Empfehlungen und Build-UX sind bewusst noch nicht der naechste Hauptfokus.",
"Auto-Entry-Modi wie paimon-menu und direct-inventory bleiben Dev-Control Experimente, nicht Produktionspfad.",
"3 Artifacts/Sekunde ist noch nicht bewiesen; der sichtbare Pfad liegt knapp darunter.",
"Mehr bestaetigte OCR-/Review-Corpus-Faelle fehlen, bevor wir breite Qualitaet behaupten.",
],
},
{
title: "Wo es Probleme macht",
tone: "risk",
icon: AlertTriangle,
items: [
"Scanner ist stark im aktuellen sichtbaren Inventory-Pfad; Repeatability ueber spaetere Sessions ist der naechste Pruefpunkt.",
"OCR ist schnell genug fuer gute Runs, bleibt aber der groesste Qualitaets- und Speed-Hebel.",
"Capture-Roundtrip ist sichtbar: Roundtrip und Overhead muessen bei 20/45/100 Runs mit bewertet werden.",
"Groessere Runs brauchen weiter Beobachtung auf Scroll-Uebergaenge, Wiederholseiten, Duplicate-Rate und Review-Quote.",
],
},
{
title: "Naechste Verbesserungen",
tone: "next",
icon: Target,
items: [
"Mehr Live-Wiederholungen: 20/45/100 Runs in spaeteren Sessions, sauber als Repeatability statt IK-Claim markieren.",
"Review-Samples sauber labeln und ueber `npm run eval:prepare-confirmed` ins Eval-Corpus uebernehmen.",
"Scanner-UI weiter beruhigen, aber den funktionierenden visible-inventory Pfad nicht umbauen.",
"Capture-Roundtrip-Overhead als eigenen Optimierungspunkt pruefen, bevor weitere OCR-Engine-Wechsel priorisiert werden.",
"Erst danach Empfehlungen wieder staerker nach vorne ziehen.",
],
},
];
// 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/
// confidence dump. Read-only; it drives no scan action except the demo snapshot.
export function DiagnosticsView({ controller, latestCapture, captureStatus, onDemoScan, canDemoScan }: DiagnosticsViewProps) {
const {
statusTitle,
rightsClassName,
rightsValue,
genshinClassName,
genshinValue,
shouldShowAdminBanner,
gridSourceClass,
gridMainValue,
gridMetaValue,
learningRulesText,
learningRulesSubtext,
autoScanModeLabel,
fingerprintText,
runtimeRows,
scanLimitText,
autoScanStatsLines,
playerProgress,
reviewStatus,
automationLogLines,
diagnosticEvents,
canSaveReviewSample,
handleSaveReviewSample,
} = useScanDiagnosticsModalModel({
setDetailsOpen: controller.setDetailsOpen,
setDiagnosticsOpen: controller.setDiagnosticsOpen,
saveReviewSample: controller.saveReviewSample,
canSaveReviewSample: controller.canSaveReviewSample,
latestCapture,
controller,
captureStatus,
});
const { parsedNotes, showParsedNotes, cropRows, ocrRows, debugText, showCrops, showOcr } = useScanDetailsModalModel({
setDetailsOpen: controller.setDetailsOpen,
parsedArtifact: controller.parsedArtifact,
latestCapture,
});
const [interopStatus, setInteropStatus] = useState("");
const handleExportGood = async () => {
setInteropStatus("Exportiere GOOD...");
const result = await controller.exportGoodFromStore();
setInteropStatus(
result.ok
? `GOOD exportiert: ${result.count} Artifacts${result.path ? ` -> ${result.path}` : ""}`
: "GOOD-Export fehlgeschlagen (App im Electron-Fenster oeffnen).",
);
};
const handleImportGood = async () => {
setInteropStatus("Waehle GOOD-Datei...");
const result = await controller.importGoodFromFile();
if (result.canceled) {
setInteropStatus("GOOD-Import abgebrochen.");
return;
}
setInteropStatus(
result.ok
? `Importiert: ${result.added} neu, ${result.updated} aktualisiert (${result.count} gelesen).`
: result.error === "No valid GOOD artifacts found."
? "Keine gueltigen Artifacts in der Datei gefunden."
: "Import fehlgeschlagen (Datei ist kein gueltiges GOOD/JSON oder Bridge fehlt).",
);
};
return (
<section className="diagnose-view">
<div className="diagnose-header">
<div>
<p className="eyebrow">Diagnose &amp; Dev</p>
<h2>Laufzeit, Erkennung &amp; Rohdaten</h2>
</div>
<div className="diagnose-header-actions">
<button className="ghost-button" onClick={controller.toggleDevMode}>
<Wrench size={15} />
{statusTitle}
</button>
{onDemoScan && (
<button className="ghost-button" onClick={onDemoScan} disabled={!canDemoScan}>
<Play size={15} />
Demo-Daten laden
</button>
)}
</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>
<div className="scanner-preflight diagnostics-preflight">
<div className={rightsClassName}>
<span>App-Rechte</span>
<strong>{rightsValue}</strong>
</div>
<div className={genshinClassName}>
<span>Genshin</span>
<strong>{genshinValue}</strong>
</div>
</div>
{shouldShowAdminBanner && (
<p className="scanner-subcopy">
App laeuft im Standard-Modus. Auto-Scan braucht Administrator-Rechte: App schliessen und als Administrator neu starten.
</p>
)}
<div className={`grid-detection-strip ${gridSourceClass}`}>
<span>Tile grid</span>
<strong>{gridMainValue}</strong>
<small>{gridMetaValue}</small>
</div>
<div className="learning-strip">
<span>Learning &amp; Daten</span>
<strong>{learningRulesText}</strong>
<small>{learningRulesSubtext}</small>
</div>
<div className="learning-strip">
<span>Fingerprint</span>
<strong>{fingerprintText}</strong>
<small>Aktiver Capture-Fingerprint fuer die Duplikat-Erkennung.</small>
</div>
</div>
<div className="diagnose-card">
<p className="eyebrow">{autoScanModeLabel}</p>
{playerProgress.show ? (
<div className="auto-scan-strip">
{autoScanStatsLines.map((entry) => (
<span className="auto-scan-stat" key={entry.label}>
<strong>{entry.value}</strong>
<small>{entry.label}</small>
</span>
))}
</div>
) : (
<p className="result-empty">Noch keine Scan-Aktivitaet.</p>
)}
<p className="scanner-result-caption">{scanLimitText}</p>
<div className="scanner-status-row diagnostics-status">
{runtimeRows.map((row) => (
<span key={row}>{row}</span>
))}
</div>
{reviewStatus && <p className="review-status">{reviewStatus}</p>}
</div>
</div>
<div className="diagnose-card">
<div className="diagnose-card-heading">
<p className="eyebrow">GOOD Interop</p>
<div className="diagnose-header-actions">
<button className="ghost-button" onClick={handleExportGood} disabled={!controller.canGoodInterop}>
<Download size={15} />
GOOD exportieren
</button>
<button className="ghost-button" onClick={handleImportGood} disabled={!controller.canGoodInterop}>
<Upload size={15} />
GOOD importieren
</button>
</div>
</div>
<p className="scanner-subcopy">
Exportiert den Scan-Store als GOOD (Genshin Optimizer / Inventory Kamera / Akasha) oder importiert eine GOOD-Datei in den Store.
</p>
{interopStatus && <p className="review-status">{interopStatus}</p>}
</div>
<div className="diagnose-card">
<p className="eyebrow">Automation log</p>
<div className="automation-log-lines">
{automationLogLines.length > 0 ? (
automationLogLines.map((line, index) => <span key={`${index}-${line}`}>{line}</span>)
) : (
<strong>Keine Scan-Aktivitaet.</strong>
)}
</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 &amp; Confidence</p>
<button className="ghost-button" onClick={handleSaveReviewSample} disabled={!canSaveReviewSample}>
<AlertTriangle size={15} />
Review-Sample speichern
</button>
</div>
{controller.parsedArtifact ? (
<>
<FieldConfidenceList parsedArtifact={controller.parsedArtifact} />
{showParsedNotes && (
<div className="parsed-notes compact">
{parsedNotes.map((note) => (
<span key={note}>{note}</span>
))}
</div>
)}
</>
) : (
<p className="result-empty">Noch kein Artifact gelesen. Lies ein Artifact im Scan-Tab, um Crops und OCR zu sehen.</p>
)}
{showCrops && (
<div className="crop-grid details-grid">
{cropRows.map((crop) => (
<div className="crop-card" key={crop.id}>
<img src={crop.dataUrl} alt={crop.label} />
<div>
<strong>{crop.label}</strong>
<span>{crop.x},{crop.y} - {crop.width}x{crop.height}</span>
</div>
</div>
))}
</div>
)}
{showOcr && (
<div className="ocr-panel">
<strong>OCR candidates</strong>
{ocrRows.map((entry) => (
<div className="ocr-row" key={entry.id}>
<div>
<span>{entry.label}</span>
<small>{entry.confidence}% confidence</small>
</div>
<pre>{entry.text}</pre>
</div>
))}
</div>
)}
{latestCapture && <p className="capture-debug">{debugText}</p>}
</div>
</section>
);
}