Detection when debug as separate tab

This commit is contained in:
Kalle
2026-09-26 13:40:07 +03:00
parent 18ab21d41e
commit 7a955d68fb
4 changed files with 56 additions and 58 deletions

View File

@@ -315,15 +315,18 @@ button.expand {
/* the expanded content, indented under its card via a rail */
.details {
display: flex;
flex-direction: column;
gap: var(--s-4);
margin-inline-start: var(--s-2-5);
padding: var(--s-1) 0 var(--s-2) var(--s-3);
border-inline-start: var(--border-width) solid var(--color-border);
animation: scanner-detail-in 0.25s ease both;
}
.summary {
display: flex;
flex-direction: column;
gap: var(--s-4);
}
@keyframes scanner-detail-in {
from {
opacity: 0;

View File

@@ -4,14 +4,21 @@
* the game's clips and, once the game is over, its upload state beside the
* expand arrow. Expanded it shows the data and
* nothing interpreted: the scoreboard, the objective + player-status
* timeline and deaths and kills (each with a ▶ when a clip covers it).
* timeline and deaths and kills (each with a ▶ when a clip covers it). In
* debug mode that summary sits in a tab beside the raw events and frames.
*/
import clsx from "clsx";
import { ChevronDown, Play } from "lucide-react";
import { useState } from "react";
import { useId, useState } from "react";
import { Ability } from "~/components/Ability";
import { SendouButton } from "~/components/elements/Button";
import { SendouPopover } from "~/components/elements/Popover";
import {
SendouTab,
SendouTabList,
SendouTabPanel,
SendouTabs,
} from "~/components/elements/Tabs";
import { GameTimeline } from "~/components/GameTimeline";
import { Image, ModeImage, WeaponImage } from "~/components/Image";
import { LocaleTime } from "~/components/LocaleTime";
@@ -108,6 +115,7 @@ export function MatchCard({
}) {
const { match } = built;
const [expanded, setExpanded] = useState(false);
const tabIdPrefix = useId();
// fixed at mount: re-rendering must not cut the animation short
const [enter] = useState(justFormed);
const [prevUploadKind, setPrevUploadKind] = useState(upload.kind);
@@ -226,6 +234,16 @@ export function MatchCard({
</div>
);
const summary = (
<div className={styles.summary}>
<Scoreboard match={match} result={result} />
{match.objective || match.playerStatus ? (
<GameTimeline {...gameTimelineProps(match, matchOrigin, TEAM_LABELS)} />
) : null}
<DeathsAndKills built={built} clips={clips} onPlayClip={onPlayClip} />
</div>
);
const card =
match.stage !== null ? (
<StageBannerBox stageId={match.stage} className={className}>
@@ -240,16 +258,22 @@ export function MatchCard({
{card}
{expanded && expandable ? (
<div className={styles.details}>
<Scoreboard match={match} result={result} />
{match.objective || match.playerStatus ? (
<GameTimeline
{...gameTimelineProps(match, matchOrigin, TEAM_LABELS)}
/>
) : null}
<DeathsAndKills built={built} clips={clips} onPlayClip={onPlayClip} />
{debug ? (
<RawDetections sources={built.sources} getFrame={getFrame} />
) : null}
<SendouTabs>
<SendouTabList>
<SendouTab id={`${tabIdPrefix}-summary`}>Summary</SendouTab>
<SendouTab id={`${tabIdPrefix}-events`}>Events</SendouTab>
</SendouTabList>
<SendouTabPanel id={`${tabIdPrefix}-summary`}>
{summary}
</SendouTabPanel>
<SendouTabPanel id={`${tabIdPrefix}-events`}>
<RawDetections sources={built.sources} getFrame={getFrame} />
</SendouTabPanel>
</SendouTabs>
) : (
summary
)}
</div>
) : null}
</div>

View File

@@ -1,23 +1,7 @@
.details {
font-size: var(--font-2xs);
}
.summary {
cursor: pointer;
color: var(--color-text-high);
font-weight: var(--weight-semi);
text-transform: uppercase;
letter-spacing: 0.08em;
}
.events {
--scanner-card-padding: var(--s-2-5) 14px;
display: flex;
flex-direction: column;
gap: var(--s-2);
margin-top: var(--s-2);
margin-inline-start: var(--s-2-5);
padding-inline-start: var(--s-3);
border-inline-start: var(--border-width) solid var(--color-border);
}

View File

@@ -3,7 +3,6 @@
* their frames (Inspect, Save fixture). Counter/status/strip reads render as
* the timeline instead and stay out.
*/
import { useState } from "react";
import { connectAbilities } from "../core/ability-harvest";
import { OBJECTIVE_EVENT_TYPE } from "../core/detectors/objective/index";
import { PLAYER_STATUS_EVENT_TYPE } from "../core/detectors/objective/player-status";
@@ -22,7 +21,6 @@ export function RawDetections({
sources: readonly ScanEvent[];
getFrame: (event: ScanEvent) => GetFrame | undefined;
}) {
const [open, setOpen] = useState(false);
const events = withoutRepeatEvents(sources).filter(
(e) =>
e.type !== OBJECTIVE_EVENT_TYPE &&
@@ -31,31 +29,20 @@ export function RawDetections({
);
const abilityMap = connectAbilities(sources);
return (
<details
className={styles.details}
open={open}
onToggle={(e) => setOpen(e.currentTarget.open)}
>
<summary className={styles.summary}>
Raw detections · {sources.length}
</summary>
{open ? (
<div className={styles.events}>
{events.map((e) => (
<EventCard
key={e.id ?? `${e.type}-${e.t}`}
type={e.type}
t={e.t}
confidence={e.confidence}
data={e.data as FixtureData}
abilities={abilityMap.get(e)}
thumbnail={e.thumbnail}
detectedAt={e.detectedAt}
getFrame={getFrame(e)}
/>
))}
</div>
) : null}
</details>
<div className={styles.events}>
{events.map((e) => (
<EventCard
key={e.id ?? `${e.type}-${e.t}`}
type={e.type}
t={e.t}
confidence={e.confidence}
data={e.data as FixtureData}
abilities={abilityMap.get(e)}
thumbnail={e.thumbnail}
detectedAt={e.detectedAt}
getFrame={getFrame(e)}
/>
))}
</div>
);
}