Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions schemas/visual-config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,9 @@
"KEYLOG_GLOW_BLUR": {
"type": "number"
},
"KEYLOG_INTERVAL_LENGTH": {
"type": "number"
},
"KEYLOG_MAXIMUM_FOR_NORMALIZATION": {
"type": "number"
},
Expand Down
17 changes: 17 additions & 0 deletions src/frontend/overlay/src/components/molecules/Clock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,23 @@ const formatLongTime = (ms: number, showSeconds: boolean): string => {
}
};

export const getStartTime = (contestInfo: ContestInfo): number | undefined => {
const contestType = contestInfo?.status?.type;
if (contestType === undefined) {
return undefined;
}
switch (contestType) {
case ContestStatus.Type.before:
return contestInfo.status.scheduledStartAtUnixMs;
case ContestStatus.Type.running:
return contestInfo.status.startedAtUnixMs;
case ContestStatus.Type.finalized:
case ContestStatus.Type.over:
return contestInfo.status.startedAtUnixMs;
}
return undefined;
};

export const calculateContestTime = (contestInfo: ContestInfo): number => {
if (contestInfo === undefined) {
return undefined;
Expand Down
95 changes: 86 additions & 9 deletions src/frontend/overlay/src/components/organisms/holder/TimeLine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,17 @@ import styled from "styled-components";
import { useAppSelector } from "@/redux/hooks";
import c from "@/config";
import { getIOIColor } from "@/utils/statusInfo";
import { ContestInfo, TeamId, TeamInfo, TimeLineRunInfo } from "@shared/api";
import { calculateContestTime } from "@/components/molecules/Clock";
import {
ContestInfo,
ContestStatus,
TeamId,
TeamInfo,
TimeLineRunInfo,
} from "@shared/api";
import {
calculateContestTime,
getStartTime,
} from "@/components/molecules/Clock";
import { isShouldUseDarkColor } from "@/utils/colors";
import { KeylogGraph } from "./KeylogGraph";

Expand Down Expand Up @@ -425,6 +434,34 @@ export function TimeLineBackground({
);
}

interface KeyboardEvent {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timestamp: string; // ISO8601
keys: Record<string, KeyStats>;
}

interface KeyStats {
raw?: number;
bare?: number;
shift?: number;
ctrl?: number;
alt?: number;
meta?: number;

"ctrl+shift"?: number;
"ctrl+alt"?: number;
"shift+alt"?: number;
"ctrl+meta"?: number;
"shift+meta"?: number;
"alt+meta"?: number;

"ctrl+shift+alt"?: number;
"ctrl+shift+meta"?: number;
"ctrl+alt+meta"?: number;
"shift+alt+meta"?: number;

"ctrl+shift+alt+meta"?: number;
}

export function TimeLine({
teamId,
className = null,
Expand Down Expand Up @@ -509,20 +546,60 @@ export function TimeLine({
}, [contestInfo, isPvp]);

useEffect(() => {
if (!keylogUrl) return;
const startTime = getStartTime(contestInfo);
if (!keylogUrl || !startTime) return;

async function fetchKeylogData() {
// TODO: Move all this code to KeylogGraph
async function fetchNDJSON(): Promise<KeyboardEvent[]> {
try {
const response = await fetch(keylogUrl);
const data: number[] = await response.json();
setKeylog(data);
} catch (error) {
console.error("Error fetching keylog data:", error);
if (!response.ok) throw new Error("Failed to fetch keylog");

const text = await response.text();
return text
.trim()
.split("\n")
.filter((line) => line.trim())
.map((line) => JSON.parse(line) as KeyboardEvent);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a single last line is not fully read/written we won't be showing all the keyboard events. Maybe it's a good idea to add the try/catch somewhere else or extract this whole procedure to a function that would try to read all the events

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds computationally heavy considering that a keylog file might be more than a megabyte. Moving it to a seperate worker that would fetch and cache the keylog data is probably a good idea

} catch (e) {
console.error(e);
return [];
}
}

async function fetchKeylogData() {
const events = await fetchNDJSON();
if (events.length === 0) return;

const contestStart = new Date(startTime!).getTime();
const AGGREGATION_MS = c.KEYLOG_INTERVAL_LENGTH;

const totalIntervals = Math.ceil(
contestInfo!.contestLengthMs / c.KEYLOG_INTERVAL_LENGTH,
);
const interval_minutes = c.KEYLOG_INTERVAL_LENGTH / 60000;
const pressesPerMinuteFactor = 1 / interval_minutes;
const newKeylog = new Array(totalIntervals).fill(0);

events.forEach((event) => {
const eventTime = new Date(event.timestamp).getTime();

if (eventTime < contestStart) return;

const timeDiff = eventTime - contestStart;
const index = Math.floor(timeDiff / AGGREGATION_MS);

if (index >= 0 && index < totalIntervals) {
const pressesCount = Object.values(event.keys).reduce((sum, k) => sum + (k.bare ?? 0) + (k.shift ?? 0), 0);
newKeylog[index] += pressesCount;
}
});

setKeylog(newKeylog.map(v => v * pressesPerMinuteFactor));
}

fetchKeylogData();
}, [keylogUrl]);
}, [keylogUrl, contestInfo]);

if (!contestInfo) return null;

Expand Down
1 change: 1 addition & 0 deletions src/frontend/overlay/src/config.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ export interface OverlayConfig {

// Keylog
KEYLOG_MAXIMUM_FOR_NORMALIZATION: number; // max value for normalization
KEYLOG_INTERVAL_LENGTH: number; // ms
KEYLOG_TOP_PADDING: number; // px
KEYLOG_BOTTOM_PADDING: number; // px
KEYLOG_Z_INDEX: number;
Expand Down
1 change: 1 addition & 0 deletions src/frontend/overlay/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ function getDefaultConfig(): EvaluatableTo<OverlayConfig> {
TIMELINE_TIME_BORDER_WIDTH: 2,

KEYLOG_MAXIMUM_FOR_NORMALIZATION: 500, // max value for normalization
KEYLOG_INTERVAL_LENGTH: 1000 * 60, // ms
KEYLOG_TOP_PADDING: 6, // px
KEYLOG_BOTTOM_PADDING: 2, // px
KEYLOG_Z_INDEX: 0,
Expand Down
Loading