Open classifier hit drawer when alert missing
This commit is contained in:
parent
dcda4006e9
commit
f08abec68a
1 changed files with 206 additions and 22 deletions
|
|
@ -2388,6 +2388,114 @@ const AlertDrawer = ({ alert, flowPacket, evidence, onClose }: AlertDrawerProps)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ClassifierHitDrawerProps = {
|
||||||
|
hit: ClassifierHitEvent;
|
||||||
|
flowPacket: FlowPacket | null;
|
||||||
|
evidence: EvidenceItem[];
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ClassifierHitDrawer = ({ hit, flowPacket, evidence, onClose }: ClassifierHitDrawerProps) => {
|
||||||
|
const direction = normalizeDirection(hit.direction);
|
||||||
|
const evidencePrints = evidence.filter((item) => item.kind === "print");
|
||||||
|
const unknownCount = evidence.filter((item) => item.kind === "unknown").length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside className="drawer">
|
||||||
|
<div className="drawer-header">
|
||||||
|
<div>
|
||||||
|
<p className="drawer-eyebrow">Classifier hit</p>
|
||||||
|
<h3>{humanizeClassifierId(hit.classifier_id)}</h3>
|
||||||
|
<p className="drawer-subtitle">{formatDateTime(hit.source_ts)}</p>
|
||||||
|
</div>
|
||||||
|
<button className="drawer-close" type="button" onClick={onClose}>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="drawer-meta">
|
||||||
|
<span className={`pill direction-${direction}`}>{direction}</span>
|
||||||
|
<span className="drawer-chip">Confidence {formatConfidence(hit.confidence)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="drawer-section">
|
||||||
|
<h4>Explanation</h4>
|
||||||
|
{hit.explanations.length === 0 ? (
|
||||||
|
<p className="drawer-empty">No explanation strings captured for this hit.</p>
|
||||||
|
) : (
|
||||||
|
<div className="drawer-list">
|
||||||
|
{hit.explanations.slice(0, 6).map((text, idx) => (
|
||||||
|
<div className="drawer-row" key={`${hit.trace_id}-${hit.seq}-ex-${idx}`}>
|
||||||
|
<p className="drawer-note">{text}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{hit.explanations.length > 6 ? (
|
||||||
|
<p className="drawer-empty">+{hit.explanations.length - 6} more explanations not shown.</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="drawer-section">
|
||||||
|
<h4>Flow packet</h4>
|
||||||
|
{flowPacket ? (
|
||||||
|
<div className="drawer-row">
|
||||||
|
<div className="drawer-row-title">
|
||||||
|
{String(flowPacket.features.option_contract_id ?? flowPacket.id ?? "Flow packet")}
|
||||||
|
</div>
|
||||||
|
<div className="drawer-row-meta">
|
||||||
|
<span>
|
||||||
|
{formatFlowMetric(parseNumber(flowPacket.features.count, flowPacket.members.length))} prints
|
||||||
|
</span>
|
||||||
|
<span>{formatFlowMetric(parseNumber(flowPacket.features.total_size, 0))} size</span>
|
||||||
|
<span>
|
||||||
|
Notional $
|
||||||
|
{formatUsd(
|
||||||
|
parseNumber(
|
||||||
|
flowPacket.features.total_notional,
|
||||||
|
parseNumber(flowPacket.features.total_premium, 0) * 100
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="drawer-note">
|
||||||
|
Window {formatFlowMetric(parseNumber(flowPacket.features.window_ms, 0), "ms")} ·{" "}
|
||||||
|
{formatTime(parseNumber(flowPacket.features.start_ts, flowPacket.source_ts))} →{" "}
|
||||||
|
{formatTime(parseNumber(flowPacket.features.end_ts, flowPacket.source_ts))}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="drawer-empty">Flow packet not in the current live cache.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="drawer-section">
|
||||||
|
<h4>Evidence prints</h4>
|
||||||
|
{evidencePrints.length === 0 ? (
|
||||||
|
<p className="drawer-empty">No linked option prints in the live cache yet.</p>
|
||||||
|
) : (
|
||||||
|
<div className="drawer-list">
|
||||||
|
{evidencePrints.slice(0, 6).map((item) => (
|
||||||
|
<div className="drawer-row" key={item.id}>
|
||||||
|
<div className="drawer-row-title">{item.print.option_contract_id}</div>
|
||||||
|
<div className="drawer-row-meta">
|
||||||
|
<span>${formatPrice(item.print.price)}</span>
|
||||||
|
<span>{formatSize(item.print.size)}x</span>
|
||||||
|
<span>{item.print.exchange}</span>
|
||||||
|
</div>
|
||||||
|
<p className="drawer-note">{formatTime(item.print.ts)}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{unknownCount > 0 ? (
|
||||||
|
<p className="drawer-empty">+{unknownCount} evidence prints not in cache.</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
type DarkDrawerProps = {
|
type DarkDrawerProps = {
|
||||||
event: InferredDarkEvent;
|
event: InferredDarkEvent;
|
||||||
evidence: DarkEvidenceItem[];
|
evidence: DarkEvidenceItem[];
|
||||||
|
|
@ -2513,6 +2621,7 @@ export default function HomePage() {
|
||||||
const [replaySource, setReplaySource] = useState<string | null>(null);
|
const [replaySource, setReplaySource] = useState<string | null>(null);
|
||||||
const [selectedAlert, setSelectedAlert] = useState<AlertEvent | null>(null);
|
const [selectedAlert, setSelectedAlert] = useState<AlertEvent | null>(null);
|
||||||
const [selectedDarkEvent, setSelectedDarkEvent] = useState<InferredDarkEvent | null>(null);
|
const [selectedDarkEvent, setSelectedDarkEvent] = useState<InferredDarkEvent | null>(null);
|
||||||
|
const [selectedClassifierHit, setSelectedClassifierHit] = useState<ClassifierHitEvent | null>(null);
|
||||||
const [filterInput, setFilterInput] = useState<string>("");
|
const [filterInput, setFilterInput] = useState<string>("");
|
||||||
const [chartIntervalMs, setChartIntervalMs] = useState<number>(CANDLE_INTERVALS[0].ms);
|
const [chartIntervalMs, setChartIntervalMs] = useState<number>(CANDLE_INTERVALS[0].ms);
|
||||||
|
|
||||||
|
|
@ -2779,6 +2888,7 @@ export default function HomePage() {
|
||||||
setSelectedAlert(null);
|
setSelectedAlert(null);
|
||||||
}
|
}
|
||||||
setSelectedDarkEvent(null);
|
setSelectedDarkEvent(null);
|
||||||
|
setSelectedClassifierHit(null);
|
||||||
}, [mode]);
|
}, [mode]);
|
||||||
|
|
||||||
const extractPacketContract = useCallback((packet: FlowPacket): string => {
|
const extractPacketContract = useCallback((packet: FlowPacket): string => {
|
||||||
|
|
@ -2806,6 +2916,43 @@ export default function HomePage() {
|
||||||
return traceId.slice(idx);
|
return traceId.slice(idx);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const selectedClassifierPacketId = useMemo(() => {
|
||||||
|
if (!selectedClassifierHit) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return extractPacketIdFromClassifierHitTrace(selectedClassifierHit.trace_id);
|
||||||
|
}, [extractPacketIdFromClassifierHitTrace, selectedClassifierHit]);
|
||||||
|
|
||||||
|
const selectedClassifierFlowPacket = useMemo(() => {
|
||||||
|
if (!selectedClassifierPacketId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return flowPacketMap.get(selectedClassifierPacketId) ?? null;
|
||||||
|
}, [flowPacketMap, selectedClassifierPacketId]);
|
||||||
|
|
||||||
|
const selectedClassifierEvidence = useMemo((): EvidenceItem[] => {
|
||||||
|
if (!selectedClassifierHit) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedClassifierPacketId) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const packet = flowPacketMap.get(selectedClassifierPacketId);
|
||||||
|
if (!packet) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return packet.members.map((id) => {
|
||||||
|
const print = optionPrintMap.get(id);
|
||||||
|
if (print) {
|
||||||
|
return { kind: "print", id, print };
|
||||||
|
}
|
||||||
|
return { kind: "unknown", id };
|
||||||
|
});
|
||||||
|
}, [flowPacketMap, optionPrintMap, selectedClassifierHit, selectedClassifierPacketId]);
|
||||||
|
|
||||||
const inferAlertUnderlying = useCallback(
|
const inferAlertUnderlying = useCallback(
|
||||||
(alert: AlertEvent): string | null => {
|
(alert: AlertEvent): string | null => {
|
||||||
const fromTrace = extractUnderlyingFromTrace(alert.trace_id);
|
const fromTrace = extractUnderlyingFromTrace(alert.trace_id);
|
||||||
|
|
@ -2924,29 +3071,50 @@ export default function HomePage() {
|
||||||
});
|
});
|
||||||
}, [chartTicker, inferredDark.items, equityJoinMap, equityPrintMap]);
|
}, [chartTicker, inferredDark.items, equityJoinMap, equityPrintMap]);
|
||||||
|
|
||||||
const handleClassifierMarkerClick = useCallback(
|
const findAlertForClassifierHit = useCallback(
|
||||||
(hit: ClassifierHitEvent) => {
|
(hit: ClassifierHitEvent): AlertEvent | null => {
|
||||||
const packetId = extractPacketIdFromClassifierHitTrace(hit.trace_id);
|
const packetId = extractPacketIdFromClassifierHitTrace(hit.trace_id);
|
||||||
if (!packetId) {
|
if (!packetId) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const desiredTrace = `alert:${packetId}`;
|
const desiredTrace = `alert:${packetId}`;
|
||||||
const alert = alerts.items.find(
|
return (
|
||||||
|
alerts.items.find(
|
||||||
(item) => item.trace_id === desiredTrace || item.evidence_refs[0] === packetId
|
(item) => item.trace_id === desiredTrace || item.evidence_refs[0] === packetId
|
||||||
|
) ?? null
|
||||||
);
|
);
|
||||||
if (!alert) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedDarkEvent(null);
|
|
||||||
setSelectedAlert(alert);
|
|
||||||
},
|
},
|
||||||
[alerts.items, extractPacketIdFromClassifierHitTrace]
|
[alerts.items, extractPacketIdFromClassifierHitTrace]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const openFromClassifierHit = useCallback(
|
||||||
|
(hit: ClassifierHitEvent) => {
|
||||||
|
const alert = findAlertForClassifierHit(hit);
|
||||||
|
if (alert) {
|
||||||
|
setSelectedClassifierHit(null);
|
||||||
|
setSelectedDarkEvent(null);
|
||||||
|
setSelectedAlert(alert);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedAlert(null);
|
||||||
|
setSelectedDarkEvent(null);
|
||||||
|
setSelectedClassifierHit(hit);
|
||||||
|
},
|
||||||
|
[findAlertForClassifierHit]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClassifierMarkerClick = useCallback(
|
||||||
|
(hit: ClassifierHitEvent) => {
|
||||||
|
openFromClassifierHit(hit);
|
||||||
|
},
|
||||||
|
[openFromClassifierHit]
|
||||||
|
);
|
||||||
|
|
||||||
const handleDarkMarkerClick = useCallback((event: InferredDarkEvent) => {
|
const handleDarkMarkerClick = useCallback((event: InferredDarkEvent) => {
|
||||||
setSelectedAlert(null);
|
setSelectedAlert(null);
|
||||||
|
setSelectedClassifierHit(null);
|
||||||
setSelectedDarkEvent(event);
|
setSelectedDarkEvent(event);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -3401,6 +3569,7 @@ export default function HomePage() {
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedDarkEvent(null);
|
setSelectedDarkEvent(null);
|
||||||
|
setSelectedClassifierHit(null);
|
||||||
setSelectedAlert(alert);
|
setSelectedAlert(alert);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -3468,7 +3637,12 @@ export default function HomePage() {
|
||||||
filteredClassifierHits.map((hit) => {
|
filteredClassifierHits.map((hit) => {
|
||||||
const direction = normalizeDirection(hit.direction);
|
const direction = normalizeDirection(hit.direction);
|
||||||
return (
|
return (
|
||||||
<div className="row" key={`${hit.trace_id}-${hit.seq}`}>
|
<button
|
||||||
|
className="row row-button"
|
||||||
|
key={`${hit.trace_id}-${hit.seq}`}
|
||||||
|
type="button"
|
||||||
|
onClick={() => openFromClassifierHit(hit)}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<div className="contract">{humanizeClassifierId(hit.classifier_id)}</div>
|
<div className="contract">{humanizeClassifierId(hit.classifier_id)}</div>
|
||||||
<div className="meta">
|
<div className="meta">
|
||||||
|
|
@ -3480,7 +3654,7 @@ export default function HomePage() {
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="time">{formatTime(hit.source_ts)}</div>
|
<div className="time">{formatTime(hit.source_ts)}</div>
|
||||||
</div>
|
</button>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
|
|
@ -3534,6 +3708,7 @@ export default function HomePage() {
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedAlert(null);
|
setSelectedAlert(null);
|
||||||
|
setSelectedClassifierHit(null);
|
||||||
setSelectedDarkEvent(event);
|
setSelectedDarkEvent(event);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -3567,6 +3742,15 @@ export default function HomePage() {
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{selectedClassifierHit ? (
|
||||||
|
<ClassifierHitDrawer
|
||||||
|
hit={selectedClassifierHit}
|
||||||
|
flowPacket={selectedClassifierFlowPacket}
|
||||||
|
evidence={selectedClassifierEvidence}
|
||||||
|
onClose={() => setSelectedClassifierHit(null)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{selectedDarkEvent ? (
|
{selectedDarkEvent ? (
|
||||||
<DarkDrawer
|
<DarkDrawer
|
||||||
event={selectedDarkEvent}
|
event={selectedDarkEvent}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue