394 lines
16 KiB
TypeScript
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 & Dev</p>
|
|
<h2>Laufzeit, Erkennung & 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 & 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 & 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>
|
|
);
|
|
}
|