import type { ClipDropCode } from "../video-clips/clip-error-codes.js"; /** * Records the integrity signals for a single proctored writing answer. * * Unlike the media clip recorders, there is no MediaRecorder or upload * here — a writing answer's *text* is owned by the integrator (saved * through their own backend). This recorder only fires the `text.*` * signal events (paste, typing dynamics, checkpoints, focus, synthetic * input, save outcome, submit) through the SDK's event pipeline, where * they are queued, persisted, and uploaded to the proctoring server like * any other event. * * It is intentionally a thin, synchronous wrapper over `emit` so the Vue * doesn't sprinkle raw `emit("text.*")` calls and so * a standalone (client-less) build can swap in a no-op recorder with the * same shape. */ export type SyntheticInputSource = "grammarly" | "no-keystroke" | "untrusted-event"; export interface TextAnswerRecorderOptions { /** The integrator's question id. Stamped on every emitted event. */ questionId: string; /** * Emit one `text.*` event. Wired to the client's `emit` in the real * path; a no-op in standalone. Swallows the pre-ready throw so a * keystroke can never crash the field. */ emit: (kind: TextAnswerEventKind, payload: Record) => void; } export type TextAnswerEventKind = "text.started" | "text.paste" | "text.copy" | "text.cut" | "text.keystroke-summary" | "text.checkpoint" | "text.focus-lost" | "text.focus-regained" | "text.synthetic-input" | "text.saved" | "text.save-failed" | "text.submitted"; /** * Public handle returned to the caller. Each method maps to one * `text.*` event; the recorder stamps `questionId` so callers don't. */ export interface TextAnswerHandle { readonly questionId: string; started(detail?: { resumed?: boolean; }): void; paste(detail: { length: number; pastedRatio: number; blocked?: boolean; /** The pasted text, when capturePasteContent is on (capped, may be truncated). */ content?: string; truncated?: boolean; }): void; /** * The candidate copied from the field. `length` is the selection size; * `content` is the copied text when capturePasteContent is on (capped). */ copy(detail: { length: number; blocked?: boolean; content?: string; truncated?: boolean; }): void; /** The candidate cut from the field. Same shape as {@link copy}. */ cut(detail: { length: number; blocked?: boolean; content?: string; truncated?: boolean; }): void; keystrokeSummary(detail: { windowMs: number; keystrokes: number; backspaces: number; charsAdded: number; }): void; checkpoint(detail: { savedAt: number; length: number; content?: string; }): void; focusLost(): void; focusRegained(detail: { durationMs: number; }): void; syntheticInput(detail: { source: SyntheticInputSource; }): void; saved(detail: { savedAt: number; target: "client" | "proctoring"; }): void; saveFailed(detail: { code: ClipDropCode; reason: string; }): void; submitted(detail: { length: number; pasteCount: number; typingMs: number; syntheticDetected: boolean; /** * Whether the answer met the question's word/character minimum, when * the host enforces one. Lets a reviewer distinguish an incomplete * answer from a suspicious one. The limit itself stays the host's * concern — this is only a reported flag. */ metMinimum?: boolean; }): void; } export declare function createTextAnswerRecorder(options: TextAnswerRecorderOptions): TextAnswerHandle; //# sourceMappingURL=text-answer-recorder.d.ts.map