import { Question, CandidateAnswer, EvaluationResult, Rubric, EvaluationTurn, FollowUpDifficulty, SessionState, RubricDimensionInput, InterviewEventEmitter, AIProviderAdapter, FollowUpEngineConfig, SynthesisResult } from '@interview-sdk/core'; import * as react from 'react'; import { CSSProperties } from 'react'; interface TranscriptEntry { question: Question; prompt: string; isFollowUp: boolean; answer: CandidateAnswer; evaluation: EvaluationResult; } /** * Low-risk, opt-in integrity signals — tab-switch and paste-into-answer * counts, nothing biometric or behavioral. These are observations for a * human reviewer to weigh in context, not an automated cheating verdict; if * you track this, disclose it to candidates (most interview platforms that * track tab-switching say so up front). */ interface IntegritySignals { /** Number of times the browser tab/window lost visibility while the interview was in progress. */ tabSwitchCount: number; /** Wall-clock timestamp (ms since epoch) of each tab-switch-away event. */ tabSwitchTimestamps: number[]; /** Every paste event into an answer field — character length of what was pasted, and when. */ pasteEvents: Array<{ length: number; timestamp: number; }>; } interface InterviewReport { sessionId: string; totalScore: number; /** Only has an entry for a dimension at least one question actually assessed (see `Question.dimensions`) — one no question addressed is simply absent, not present at 0. */ dimensionAverages: Record; strengths: string[]; weaknesses: string[]; missedConcepts: string[]; transcript: TranscriptEntry[]; /** Only present when the caller opted into tracking (e.g. InterviewWidget's trackIntegritySignals prop). */ integritySignals?: IntegritySignals; } /** Also used by LiveSignals to mark where a "pass" begins on its meters — one definition of "strong" shared across the live view and the final report. */ declare const STRENGTH_THRESHOLD = 75; /** * Builds a heuristic report from rubric scores and concept coverage already * present in the transcript. Strengths/weaknesses/recommendations are * derived from dimension averages and missed concepts, not AI-generated * commentary — there is no such call in this SDK today. */ declare function buildReport(sessionId: string, rubric: Rubric, transcript: TranscriptEntry[], integritySignals?: IntegritySignals): InterviewReport; interface ProcessAnswerInput { question: Question; rubric: Rubric; answer: CandidateAnswer; previousTurns: EvaluationTurn[]; currentFollowUpDepth: number; askedFollowUps: string[]; } interface ProcessAnswerFollowUp { prompt: string; difficulty?: FollowUpDifficulty; targetsMissedConcepts: string[]; } interface ProcessAnswerResult { evaluation: EvaluationResult; followUp?: ProcessAnswerFollowUp; } /** * Abstracts over Client Mode (evaluate/follow-up run in the browser via an * injected AIProviderAdapter) and Server Mode (a single round trip to the * developer's own backend, which does the same work using * @interview-sdk/server). InterviewWidget/useInterview only ever talk to * this interface, never to the mode-specific detail. */ interface InterviewProcessor { processAnswer(input: ProcessAnswerInput): Promise; } interface UseInterviewOptions { questions: Question[]; rubric: RubricDimensionInput[]; processor: InterviewProcessor; maxFollowUpDepth?: number; sessionTimeoutMs?: number; onSessionEnd?: (report: InterviewReport) => void; /** Resume a session from a snapshot returned by this same hook's getSnapshot() — e.g. one restored after a page refresh or loaded back from your own backend. Only read once, on mount. */ initialSnapshot?: InterviewSnapshot; /** Read once whenever a report is built (on completion or voluntary end) and attached to it as `integritySignals` — e.g. from useIntegritySignals() in @interview-sdk/react. Omit if you don't track this. */ getIntegritySignals?: () => IntegritySignals; } interface SubmitAnswerOptions { isSkipped?: boolean; isSilence?: boolean; } /** Everything needed to resume this exact session elsewhere — persist it (localStorage, your own backend) and pass it back as `initialSnapshot` to pick up where it left off after a refresh or disconnect. */ interface InterviewSnapshot { flowState: SessionState; transcript: TranscriptEntry[]; } interface UseInterviewResult { status: SessionState['status']; /** Wall-clock time the session actually started, for a live elapsed-time display. Undefined until start() is called. */ startedAt: number | undefined; currentQuestion: Question | undefined; currentPrompt: string | undefined; isFollowUpPrompt: boolean; transcript: TranscriptEntry[]; isProcessing: boolean; error: Error | undefined; report: InterviewReport | undefined; start: () => void; pause: () => void; resume: () => void; /** Voluntarily ends the session before every question has been answered, building a report from whatever's been answered so far. */ endInterview: () => void; submitAnswer: (text: string, opts?: SubmitAnswerOptions) => Promise; retryLastAnswer: () => Promise; /** A serializable snapshot of the whole session right now — see InterviewSnapshot. */ getSnapshot: () => InterviewSnapshot; /** The underlying typed event emitter (sessionStart/sessionPause/sessionResume/sessionEnd/sessionExpired/questionAdvance/followUpGenerated/scoreComputed) — subscribe for your own analytics or logging, independent of the UI. */ events: InterviewEventEmitter; } declare function useInterview(options: UseInterviewOptions): UseInterviewResult; /** * Client Mode: evaluation and follow-up generation run directly in the * browser against a developer-supplied AIProviderAdapter. Prototyping only * — see InterviewWidget's production guard (§10). */ declare class ClientModeProcessor implements InterviewProcessor { private readonly adapter; private readonly evaluationEngine; private readonly followUpEngine; constructor(adapter: AIProviderAdapter, followUpConfig?: FollowUpEngineConfig); processAnswer(input: ProcessAnswerInput): Promise; } declare class ServerModeRequestError extends Error { } interface ServerModeProcessorConfig { /** * URL to POST each answer to. The developer's `@interview-sdk/server` * route handles evaluation and follow-up generation server-side, so AI * keys and score integrity never reach the browser. Defaults to * `/api/interview/answer`. */ endpoint?: string; /** Override fetch (e.g. for testing). */ fetchImpl?: typeof fetch; /** Extra headers to send with every request (e.g. a session/auth token). */ headers?: Record; } /** * Server Mode: each answer is sent in a single request to the developer's * own backend, which evaluates it (using the same @interview-sdk/core * engines, server-side) and returns the result. This is the wire contract * @interview-sdk/server (Phase 5) implements — see that package's docs for * the authoritative shape once it exists. */ declare class ServerModeProcessor implements InterviewProcessor { private readonly endpoint; private readonly fetchImpl; private readonly headers; constructor(config?: ServerModeProcessorConfig); processAnswer(input: ProcessAnswerInput): Promise; } interface AudioLevelMeterProps { /** Normalized 0..1 amplitude samples — one bar per entry. */ levels: number[]; /** Color/role of the meter: the AI's voice, or the candidate's mic. */ variant: 'speaking' | 'listening'; /** Renders a calm ambient pulse instead of reactive bars — used whenever live amplitude data isn't available. */ isIdle?: boolean; className?: string; } /** * Purely presentational amplitude bars, shared by QuestionAudio (AI * speaking) and MicButton (candidate listening). Contains no Web Audio * code — callers own sampling the real data. */ declare function AudioLevelMeter({ levels, variant, isIdle, className }: AudioLevelMeterProps): react.JSX.Element; interface FeedbackNoteProps { transcript: TranscriptEntry[]; /** Label shown above the note. Defaults to 'AI Interviewer', matching the stage tile's own label. */ assistantName?: string; } /** * A chat-style note surfacing the AI's own rationale for the most recent * scored answer — evaluation.rationale is a real field populated by the * evaluation engine's model response (see @interview-sdk/core), not * fabricated UI copy. Renders nothing when no scored entry has a rationale * yet (e.g. a skipped/silent answer, or an adapter that doesn't return one). */ declare function FeedbackNote({ transcript, assistantName }: FeedbackNoteProps): react.JSX.Element | null; interface InterviewLobbyProps { /** Begins the real interview session — typically useInterview's start(). */ onJoin: () => void; /** Whether this interview uses voice at all (a transcribe function was given to InterviewWidget). Text-only mode skips the mic check entirely. */ voiceEnabled: boolean; totalQuestions?: number; /** * Requests a live mic stream purely to preview it — never produces a * recording, distinct from MicButton's Blob-producing AudioRecorder. * Defaults to a real getUserMedia({ audio: true }) call. Injectable for * testing. */ requestMicStream?: () => Promise; /** Overrides the primary action's visible text. Defaults to 'Start interview'. */ joinLabel?: string; } /** * A pre-join screen — a mic check plus a "ready to begin" confirmation, * matching the lobby real video-call/interview platforms put candidates * through before a live session. The mic check is always optional: joining * never depends on it succeeding, matching this codebase's standing rule * that voice is an enhancement, never a blocker. */ declare function InterviewLobby({ onJoin, voiceEnabled, totalQuestions, requestMicStream, joinLabel, }: InterviewLobbyProps): react.JSX.Element; interface InterviewProgressProps { questions: Question[]; currentQuestion: Question | undefined; transcript: TranscriptEntry[]; } /** * A running checklist of every question in the interview and where the * candidate is right now — built entirely from data useInterview already * exposes (questions + transcript + currentQuestion), no new session state. */ declare function InterviewProgress({ questions, currentQuestion, transcript }: InterviewProgressProps): react.JSX.Element; interface LiveSignalsProps { rubric: Rubric; transcript: TranscriptEntry[]; } /** * Live per-dimension score meters for the most recently answered question — * reads straight from the latest transcript entry's real dimensionScores, * the same evaluation data ScoreSummary uses for the final report. Renders * nothing until at least one answer has been scored. * * Styled as a vertical instrument cluster (think a mixing console's level * meters) rather than the horizontal "skill bar" almost every scoring UI * defaults to — each meter also carries a real tick at STRENGTH_THRESHOLD, * the same cutoff the final report uses to call a dimension a strength, so * "does this clear the bar" reads at a glance instead of requiring the * candidate or reviewer to do the comparison themselves. */ declare function LiveSignals({ rubric, transcript }: LiveSignalsProps): react.JSX.Element | null; interface AudioRecorder { stop: () => Promise; /** * The live MediaStream backing this recording, when available — enables * a real input-level meter on the SAME stream already feeding * MediaRecorder, never a second getUserMedia() request. Optional: * test-injected fake recorders can omit it and the live meter simply * doesn't render. */ stream?: MediaStream; } interface MicButtonProps { /** Turns captured audio into text — typically a VoiceProviderAdapter's transcribe(). */ transcribe: (audio: Blob) => Promise; onTranscript: (text: string) => void; /** * Called on mic denial, capture failure, empty audio, or a transcription * error. The candidate always has the text input in QuestionCard as a * fallback — this button never needs to render its own. */ onError?: (error: Error) => void; disabled?: boolean; label?: string; /** Injectable for testing; defaults to a real getUserMedia/MediaRecorder implementation. */ createRecorder?: () => Promise; /** Notified whenever recording starts/stops — informational only, not fired for the initial mount. */ onRecordingChange?: (isRecording: boolean) => void; /** Promotes this control to the primary, inviting call-to-action (e.g. once it's the candidate's turn). Defaults to false — zero visual change for existing callers. */ emphasized?: boolean; /** Live input-amplitude meter while recording, sourced from the same capture stream already feeding MediaRecorder. Defaults to true; renders nothing when no stream is available. */ showLevelMeter?: boolean; /** Notified with the live amplitude data whenever it changes — lets a caller drive its own separate meter display (e.g. a self-view tile) instead of this button's own inline one. Informational only. */ onLevelsChange?: (levels: number[], isSupported: boolean) => void; } declare function MicButton({ transcribe, onTranscript, onError, disabled, label, createRecorder, onRecordingChange, emphasized, showLevelMeter, onLevelsChange, }: MicButtonProps): react.JSX.Element; interface QuestionAudioProps { /** The prompt text to speak — re-synthesized whenever this changes. */ text: string; /** Turns text into speech — typically a VoiceProviderAdapter's synthesize(). */ synthesize: (text: string) => Promise; /** * Called when synthesis fails or playback is unavailable. The question * text is always visible in QuestionCard regardless — this is a bonus * audio channel, never the only way to get the question. */ onError?: (error: Error) => void; /** Attempts to play as soon as audio is ready. Defaults to true. */ autoPlay?: boolean; /** Fires on the