"use client"; /** * Per-episode annotation state for the v3.1 language schema. * * - Atoms live in memory + sessionStorage so the user can browse without a * backend (read/edit, but no parquet rewrite). * - When `NEXT_PUBLIC_ANNOTATE_BACKEND_URL` is set, the context syncs with * the FastAPI service in `backend/`: GET on episode entry, POST on save, * plus frame-timestamp fetches used to snap event-style atoms to exact * source-frame timestamps (the writer in lerobot#3471 enforces exact match). * * - VQA drawings (active `pendingDraw`) live here too so the panel and the * video overlay component share a single source of truth. */ import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react"; import type { LanguageAtom } from "../types/language.types"; import { snapToFrame } from "../types/language.types"; import { fetchEpisodeAtoms, saveEpisodeAtoms, fetchFrameTimestamps, isAnnotateBackendEnabled, } from "../utils/annotationsClient"; const STORAGE_PREFIX = "lerobot-annotations:v2:"; function storageKey(repoOrPath: string, episodeId: number): string { return `${STORAGE_PREFIX}${repoOrPath}::${episodeId}`; } export interface PendingBboxDraw { kind: "bbox"; bbox: [number, number, number, number]; // 0..1, image-relative label: string; camera?: string; } export interface PendingPointDraw { kind: "keypoint"; point: [number, number]; // 0..1, image-relative label: string; camera?: string; } export type PendingDraw = PendingBboxDraw | PendingPointDraw | null; /** * `"auto"` — drag = bbox, single click = keypoint (the natural mode the * Annotations tab boots into). The other values force a single gesture * and exist for the legacy panel-driven flow. */ export type DrawMode = "off" | "auto" | "bbox" | "keypoint"; interface DatasetIdent { repoId?: string | null; localPath?: string | null; revision?: string | null; } interface AnnotationsContextType { episodeId: number | null; ident: DatasetIdent; atoms: LanguageAtom[]; frameTimestamps: number[]; /** * Index in `atoms` of the currently selected atom (the one the right-rail * editor is bound to). `null` means nothing is selected — the editor shows * an empty state. Selection survives content edits because we mutate atoms * in place at the same index; we clear it on delete or when atoms reset. */ selectedIdx: number | null; selectAtom: (idx: number | null) => void; /** * Active