diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..7edc2d1 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,340 @@ + + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `frontend/node_modules/next/dist/docs/` before writing any code. Heed deprecation notices. + + + +# AGENTS.md + +Project-wide conventions, patterns, and agent instructions. `CLAUDE.md` points here. + +For setup, running both services, and repo structure, see the [root README](README.md). +For system architecture and data flow, see [docs/architecture.md](docs/architecture.md). + +--- + +## Repository instructions + +### Project overview + +**Interview Coach** is an AI-powered interview coaching platform. The user selects an interviewer persona, listens to a spoken question, records their answer, and receives structured per-segment feedback on delivery, tone, and answer quality. + +End-to-end pipeline: + +1. Interviewer selected → TTS introduction + question read aloud (`POST /speech/tts`) +2. User records their answer in the browser (MediaRecorder + VAD) +3. Audio → Groq Whisper → timestamped transcript segments (`POST /speech/transcribe`) +4. Each segment → local wav2vec2 emotion model → arousal / dominance / valence scores +5. *(Coming)* Transcript + scores + question → LLM → structured feedback (`POST /feedback/generate`) +6. *(Coming)* Frontend renders full scorecard + +The frontend orchestrates the pipeline. The backend exposes stateless single-purpose endpoints. + +### Tech stack + +| Area | Technology | +| ---------------- | ---------------------------------------------------------------- | +| Framework | Next.js 16 (App Router) | +| Language | TypeScript (`strict`) | +| UI | React 19, Tailwind CSS 4 | +| Package manager | pnpm | +| Lint / format | ESLint (`eslint-config-next`), Prettier | +| Component docs | Storybook (dependency present; add scripts when configured) | +| Git hooks | lefthook (installed via `pnpm install` prepare) | +| Backend | FastAPI (Python) | +| Text-to-speech | Groq Orpheus (`canopylabs/orpheus-v1-english`) | +| Speech-to-text | Groq Whisper (`whisper-large-v3-turbo`) | +| Tone/delivery | `audeering/wav2vec2-large-robust-12-ft-emotion-msp-dim` (local) | +| LLM feedback | Anthropic Claude (coming) | +| Deploy | Vercel (frontend, planned) / Render or Fly.io (backend, planned) | + +### Monorepo layout + +``` +Interview-Coach/ + README.md — setup, architecture, how to run everything + AGENTS.md — this file; project-wide agent conventions + CLAUDE.md — points to AGENTS.md + docs/ + architecture.md — system architecture and data flow + agent-workflows/ — repeatable AI agent workflow guides + backend/ — FastAPI (Python), http://localhost:8000 + app.py + requirements.txt + services/ + speech_to_text/ — /speech/tts and /speech/transcribe + tone_delivery_analyzer/ — /emotion/analyze (local wav2vec2 model) + llm/ — /feedback/generate (coming) + text_analysis/ — reserved (coming) + frontend/ — Next.js app, http://localhost:3000 + app/ + page.tsx — home / interview UI + InterviewClient.tsx — interview state machine (recording, VAD, TTS) + scorecard/ — scorecard display components + dev/ + flow/ — pipeline visualization dev page + transcribe/ — transcription debug page + lib/ + prompts/ + questions.ts — question bank (20 questions + intro question) + interviewers.ts — interviewer voice personas + interview-coach/ + types.ts — shared TypeScript types + mocks.ts — mock data for UI development + pipelineStages.ts — pipeline stage definitions +``` + +### Backend integration + +- **Base URL (local):** `http://localhost:8000` +- **Interactive docs:** `http://localhost:8000/docs` + +| Method | Path | Status | Purpose | +| ------ | --------------------- | ------- | ---------------------------------------------------- | +| GET | `/health` | done | Liveness check | +| GET | `/emotion/health` | done | Confirms emotion model is loaded | +| POST | `/speech/tts` | done | Text → WAV audio (Groq Orpheus TTS) | +| POST | `/speech/transcribe` | done | Audio → per-segment transcript + emotion scores | +| POST | `/emotion/analyze` | done | Audio → single arousal / dominance / valence score | +| POST | `/feedback/generate` | coming | Transcript + scores + question → LLM feedback | + +Call these from the browser via `fetch`. Do not reimplement ML, TTS, or LLM logic in the frontend. Use env-based API base URLs for production (`NEXT_PUBLIC_API_URL`). + +### Import conventions + +- `@/*` maps to the **frontend project root** (see `frontend/tsconfig.json`), e.g. `@/lib/prompts/questions`. +- Prefer `@/` over deep relative paths (`../../../`). + +### Hard rules (never violate) + +1. Never run `git commit` or `git push` without explicit user approval. +2. Do not put backend secrets or API keys in frontend code — keys belong in `backend/.env` only. +3. Do not duplicate pipeline logic (Whisper, emotion model, TTS, LLM) in the frontend; use backend endpoints. +4. Use `next/image` for images and `next/link` for internal navigation — not raw `` or `` for in-app routes. +5. Prefer `@/` imports over long relative paths when the alias applies. +6. No inline `style` props in JSX (exception: `*.stories.tsx` for Storybook decorators; dev-only pages under `app/dev/**`). +7. Every new component that has any of its own styles requires `ComponentName.module.css`. +8. Every new reusable component or component with meaningful visual states requires `ComponentName.stories.tsx` with named stories and real mock data. +9. If you modify a component's structural JSX, proactively update its `.module.css` and `.stories.tsx` when those files exist. +10. Do not modify `AGENTS.md` without proposing the change first when the edit reflects new team conventions. +11. Do not modify `backend/services/tone_delivery_analyzer/emotion_model.py` — it is marked do not modify. + +--- + +## Running the project (agent-driven) + +### Groq Orpheus TTS — known setup requirement + +The interviewer voice uses the Groq Orpheus TTS model (`canopylabs/orpheus-v1-english`). **Each Groq account must accept the model terms once before the endpoint works.** The backend starts without error whether or not terms are accepted — the failure only appears at request time as a 400 from Groq, which the frontend surfaces as "Could not load audio." + +**When to raise this proactively:** +- When helping a new user set up the project +- When the user reports the interviewer is silent or sees "Could not load audio" +- Before starting the project (see Running the project section below) +- Any time a user adds a new `GROQ_API_KEY` to their `.env` + +**Guidance to give:** +> "To enable the interviewer voice, you need to accept the Groq Orpheus terms once for your account. Go to https://console.groq.com/playground?model=canopylabs%2Forpheus-v1-english and click Accept. This is a one-time step — it won't be needed again." + +**Symptom if skipped:** backend runs fine, `POST /speech/tts` returns 400, frontend shows "Could not load audio — check the backend is running." Recording and transcription still work; only the voice is affected. + +--- + +## Running the project (agent-driven) + +When the user says any of the following — **"run the project"**, **"run the interview coach"**, **"start the project"**, **"start interview coach"**, **"start the app"**, **"launch the project"**, or any close variation — the agent must first confirm with the user: + +> "Ready to start Interview Coach. This will launch the backend (port 8000) and frontend (port 3000). +> Before I start — have you accepted the Groq Orpheus TTS terms for your account? This is required for the interviewer voice. Visit https://console.groq.com/playground?model=canopylabs%2Forpheus-v1-english to accept if you haven't yet. +> Confirm start?" + +Only after the user confirms, follow `docs/agent-workflows/run-project.md` exactly. Do not ask the user to open a terminal or type any commands. + +**Required order:** +1. Assess state (venv, deps, `.env`, node_modules, port availability) — all checks in parallel +2. Fix only what is missing +3. Start backend in background — wait for `Application startup complete` +4. Start frontend in background +5. Report both URLs to the user + +**Hard stops before starting:** +- `backend/.env` missing or `GROQ_API_KEY` not set → tell the user, do not start +- A port is occupied by an unknown process → ask the user before killing it + +**When the user says "stop the project" / "stop the app":** kill the processes on ports 8000 and 3000 using the method in `docs/agent-workflows/run-project.md`. + +--- + +## Development commands + +Run from `frontend/` unless noted. + +| Task | Command | +| ------------ | ------------------- | +| Dev server | `pnpm dev` → :3000 | +| Production | `pnpm build` | +| Start prod | `pnpm start` | +| Lint | `pnpm lint` | +| Type check | `pnpm typecheck` | +| Format | `pnpm format` | +| Format check | `pnpm format:check` | + +Install hooks: `pnpm install` (runs `lefthook install` via `prepare`). + +Git hooks (repo root `lefthook.yml`): + +- **pre-commit:** Prettier and Ruff format **staged** files and re-stage fixes (`stage_fixed: true`). +- **pre-push:** ESLint, Prettier check, and TypeScript on the frontend; Ruff check/format on the backend. + +Backend: `cd backend && uvicorn app:app --reload` — starts at :8000. First run downloads ~1 GB of model weights. + +--- + +## Environment variables + +All secrets live in `backend/.env` (gitignored). See [backend/.env.example](backend/.env.example). + +| Variable | Required | Purpose | +| ------------------- | -------- | ---------------------------------------------- | +| `GROQ_API_KEY` | Yes | Groq Whisper (transcription) + Orpheus (TTS) | +| `ANTHROPIC_API_KEY` | Coming | Claude LLM feedback (`/feedback/generate`) | +| `OPENAI_API_KEY` | No | Reserved | + +Frontend env vars (in `frontend/.env.local` if needed): + +| Variable | Purpose | +| --------------------- | -------------------------------------------- | +| `NEXT_PUBLIC_API_URL` | FastAPI base URL for production deployments | + +Defaults to `http://localhost:8000` in development when unset. + +> **Groq TTS:** Orpheus requires one-time terms acceptance per account at `https://console.groq.com/playground?model=canopylabs%2Forpheus-v1-english`. + +--- + +## Code organization + +- **API calls:** All backend `fetch` calls originate in components (`InterviewClient.tsx`) using `NEXT_PUBLIC_API_URL` or the localhost default. When the API surface grows, centralize in `frontend/lib/api.ts`. +- **Types:** Shared types in `frontend/lib/interview-coach/types.ts`. Feature-specific types colocated with the feature. +- **Server vs client:** Server Components by default; add `'use client'` only for recording, media APIs, and interactive UI (`InterviewClient.tsx`). +- **State:** React `useState`/`useRef` for recording and session flow. No external state library needed yet. + +--- + +## React and Next.js patterns + +### File naming + +- Routes: `page.tsx`, `layout.tsx` +- Client entry points: `*Client.tsx` with `'use client'` at the top +- API routes (if added): `route.ts` + +### Components and styling + +Every new UI component with its own styles uses three files: + +1. **`ComponentName.tsx`** — No inline `style` props (except in stories). Tailwind for generic layout; `.module.css` for component-specific look and state variants. +2. **`ComponentName.module.css`** — Required the moment any component-specific style is added. +3. **`ComponentName.stories.tsx`** — Required for reusable UI and any component with distinct visual states. Named stories with real mock props — not empty defaults. + +**Tailwind vs module CSS** + +- Tailwind in JSX: layout (flex, grid, gap), spacing, display, alignment. +- `.module.css`: hover/focus/disabled/error states, animations, component identity, anything that clutters JSX. +- Do not use `@apply group-hover:*` or `@apply peer-*:*` in `.module.css`. + +**Hydration** + +- Do not use `suppressHydrationWarning` — fix the root cause. Defer browser-only values (like `Math.random()`) until after mount with `useEffect` + `useState(null)`. + +### Storybook + +Storybook is listed in `package.json`. Add `storybook` / `build-storybook` scripts when the config is initialized. Colocate `ComponentName.stories.tsx` next to the component. + +--- + +## Agent limitations and escalation + +### Do not do autonomously + +- `git commit` or `git push` without explicit approval +- Deploy to Vercel or any environment unless asked +- Modify `backend/services/tone_delivery_analyzer/emotion_model.py` +- Commit `.env` files or API keys + +### Stop and ask when + +- Requirements for session flow, scorecard layout, or question bank content are ambiguous +- A change requires new backend contract fields not yet documented in [backend/README.md](backend/README.md) +- Implementing features that depend on `todo` endpoints before they exist + +--- + +## Git conventions + +- Ask before committing or pushing. +- Hooks: pre-commit auto-formats staged files; pre-push runs lint, format check, and typecheck. +- Keep commits focused; match existing message style on the branch. + +--- + +## Domain context + +- **Delivery scores:** Arousal, dominance, and valence from the local wav2vec2 model. Communicate as delivery/tone signals — not clinical or diagnostic labels. +- **Transcript:** Timestamped segments from Groq Whisper. Each segment is ~2–5 seconds of natural speech. +- **Feedback:** *(Coming)* Structured LLM output from Claude. Render sections clearly; do not expose raw JSON to users. +- **TTS voices:** Groq Orpheus voices — `autumn`, `diana`, `hannah`, `austin`, `daniel`, `troy`. Mapped to interviewer personas in `frontend/lib/prompts/interviewers.ts`. +- **Question bank:** 20 questions in `frontend/lib/prompts/questions.ts` — 1 intro ("tell me about yourself"), 10 technical, 10 behavioral/soft skill. Always starts with the intro question. + +--- + +## Testing + +No frontend test runner is configured yet. When tests are added, colocate with the feature (`ComponentName.test.tsx`) or mirror under `__tests__/`. Prefer behavior-focused tests for recording flow, API error handling, and scorecard rendering. + +--- + +## Key references + +| Topic | Location | +| ------------------------ | ---------------------------------------------------------------------------------- | +| Full stack setup | [README.md](README.md) | +| Architecture & data flow | [docs/architecture.md](docs/architecture.md) | +| Backend endpoints | [backend/README.md](backend/README.md) | +| Emotion model | [backend/services/tone_delivery_analyzer/README.md](backend/services/tone_delivery_analyzer/README.md) | +| Frontend scripts | [frontend/README.md](frontend/README.md) | +| Agent workflows | [docs/agent-workflows/](docs/agent-workflows/) | + +--- + +## Maintaining AGENTS.md + +After features or conventions change, update this file: + +- New routes, components, or folder layout → **Monorepo layout** + **Project structure** +- New backend endpoints → **Backend integration** +- New pnpm scripts → **Development commands** +- New env vars → **Environment variables** +- New hard rules or team decisions → **Hard rules** + +For task-specific reviews, use workflows in `docs/agent-workflows/`. Invoke explicitly, e.g.: + +``` +Use docs/agent-workflows/pull-request-description-generator.md for this branch. +``` + +| Workflow | Use when | +| --------------------------------------- | ---------------------------------------------- | +| `pre-merge-full-review.md` | Full pre-merge review with phase gates | +| `branch-change-impact-audit.md` | What changed and regression risks vs `main` | +| `code-quality-review.md` | Correctness, conventions, API integration | +| `css-and-component-standards-review.md` | module.css, Tailwind placement, Storybook | +| `test-suite-quality-review.md` | Test simplicity (or Storybook if no tests yet) | +| `manual-qa-checklist-generator.md` | Manual QA for recording/scorecard flows | +| `pull-request-description-generator.md` | PR title and description from diff | +| `feature-implementation-planning.md` | Plan before non-trivial features | +| `feature-flag-gating-review.md` | Env/flag gating completeness | +| `figma-design-to-code.md` | Implement UI from Figma | +| `run-project.md` | Start backend + frontend from scratch | diff --git a/frontend/CLAUDE.md b/CLAUDE.md similarity index 100% rename from frontend/CLAUDE.md rename to CLAUDE.md diff --git a/README.md b/README.md index 50d73f0..cf5836e 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,18 @@ # Interview Coach -AI-powered interview coaching platform. Users record a response to an interview question and receive structured feedback on delivery, tone, and answer quality. +AI-powered interview coaching platform. Users select an interviewer, listen to a spoken question, record their answer, and receive structured feedback on delivery, tone, and answer quality. ## How it works -1. User records an answer in the browser -2. Audio → Whisper API → timestamped transcript -3. Audio + transcript → Audeering wav2vec2 → delivery scores (arousal, dominance, valence) -4. Transcript + scores + question → LLM → structured feedback -5. Frontend renders a scorecard +1. User selects an interviewer voice and presses **Start Interview** +2. Interviewer introduces themselves via TTS (`POST /speech/tts`) +3. Interviewer reads the question aloud; recording starts automatically +4. User answers and presses **I'm Done Answering** +5. Audio → Groq Whisper → timestamped transcript segments (`POST /speech/transcribe`) +6. Each segment → local wav2vec2 model → arousal / dominance / valence scores +7. Scores + transcript displayed per segment on the interview screen +8. *(Coming)* Transcript + scores + question → LLM → structured feedback (`POST /feedback/generate`) +9. *(Coming)* Frontend renders full scorecard ## Prerequisites @@ -16,7 +20,7 @@ Install these once at the OS level before running setup: - **Python 3.9+** - **pnpm** — `npm install -g pnpm` -- **ffmpeg** — decodes audio files, installed via your OS package manager: +- **ffmpeg** — required to decode audio files: | Platform | Command | | -------- | ------------------------------------------------ | @@ -24,11 +28,11 @@ Install these once at the OS level before running setup: | macOS | `brew install ffmpeg` | | Linux | `sudo apt install ffmpeg` | -Verify ffmpeg with `ffmpeg -version` after installing. On Windows, restart the terminal first so the PATH updates. +Verify with `ffmpeg -version` after installing. On Windows, restart the terminal first. ## Setup -Run these once from the project root after cloning. +Run once from the project root after cloning. ### 1. Python environment @@ -39,27 +43,46 @@ source venv/Scripts/activate # Windows (Git Bash) pip install -r backend/requirements.txt ``` -The prompt shows `(venv)` when the environment is active. You must activate it in every new terminal session — packages stay installed, only activation is per-session. +`(venv)` appears in the prompt when active. Re-activate in every new terminal — packages stay installed. ### 2. Environment variables -Create `backend/.env` and add your API keys (required once Whisper and LLM services are wired up): +```bash +cp backend/.env.example backend/.env +``` + +Open `backend/.env` and fill in your keys: ``` -OPENAI_API_KEY=your_key_here -ANTHROPIC_API_KEY=your_key_here +GROQ_API_KEY=your_key_here # https://console.groq.com (speech-to-text + TTS) +ANTHROPIC_API_KEY=your_key_here # https://console.anthropic.com (LLM feedback, coming) +OPENAI_API_KEY=your_key_here # Reserved ``` -This file is gitignored — never commit it. +`backend/.env` is gitignored — never commit it. + +### 3. Accept Groq Orpheus TTS terms ⚠️ Required + +The text-to-speech feature uses Groq's Orpheus model. **Each Groq account must accept the model terms once before the TTS endpoint will work.** Without this step the backend starts fine but every call to `POST /speech/tts` returns a 400 error and the interviewer will be silent. + +1. Log in to [console.groq.com](https://console.groq.com) +2. Visit this URL directly: + ``` + https://console.groq.com/playground?model=canopylabs%2Forpheus-v1-english + ``` +3. Click **Accept** on the terms banner +4. Done — acceptance is permanent for your account, no need to repeat it + +> If you skip this step and try to run the project, the interviewer will not speak and the frontend will show "Could not load audio — check the backend is running." The backend itself will still start correctly. -### 3. Frontend +### 4. Install frontend dependencies ```bash cd frontend pnpm install ``` -This also installs git hooks via lefthook. **Pre-commit** auto-formats staged frontend/backend files; **pre-push** runs ESLint, Prettier check, and TypeScript on the frontend (plus Ruff on the backend). +Installs dependencies and git hooks (lefthook). Pre-commit auto-formats staged files; pre-push runs ESLint, Prettier, and TypeScript checks. ## Running the project @@ -68,12 +91,13 @@ Open two terminals from the project root. **Terminal 1 — Backend:** ```bash -source venv/Scripts/activate # Windows; use venv/bin/activate on macOS/Linux +source venv/Scripts/activate # Windows; venv/bin/activate on macOS/Linux cd backend uvicorn app:app --reload ``` -Server starts at **http://localhost:8000**. Interactive API docs at **http://localhost:8000/docs**. The emotion model loads on startup; first run downloads ~1 GB of model weights. +Starts at **http://localhost:8000**. API docs at **http://localhost:8000/docs**. +First run downloads ~1 GB of emotion model weights — this is normal. **Terminal 2 — Frontend:** @@ -82,36 +106,60 @@ cd frontend pnpm dev ``` -App opens at **http://localhost:3000**. +App at **http://localhost:3000**. ## Repo structure ``` -backend/ — FastAPI backend (Python) - app.py — entry point; registers all routers - requirements.txt — Python dependencies +backend/ + app.py — FastAPI entry point; registers routers, loads emotion model + requirements.txt — Python dependencies services/ - tone_delivery_analyzer/ — Audeering wav2vec2 emotion model (local) - speech_to_text/ — Whisper API transcription (todo) - llm/ — LLM feedback generation (todo) -frontend/ — Next.js frontend (React) -docs/ — project proposal and reference documents + tone_delivery_analyzer/ — local wav2vec2 emotion model (arousal/dominance/valence) + speech_to_text/ — Groq Whisper transcription + TTS + llm/ — LLM feedback generation (coming) + text_analysis/ — transcript scoring (coming) + +frontend/ + app/ + page.tsx — interview UI (home page) + InterviewClient.tsx — full interview state machine + layout.tsx / globals.css — root layout and styles + scorecard/ — scorecard display components + dev/ + flow/ — pipeline visualization dev page + transcribe/ — transcription dev/debug page + lib/ + prompts/ + questions.ts — question bank (20 questions + intro) + interviewers.ts — interviewer voices and personas + interview-coach/ + types.ts — shared TypeScript types + mocks.ts — mock data for UI development + pipelineStages.ts — pipeline stage definitions + +docs/ + architecture.md — system architecture and data flow + agent-workflows/ — AI agent workflow guides ``` -**Complete the Setup section above before consulting any of the links below.** +## Reference -- Backend endpoint reference: [backend/README.md](backend/README.md) -- Frontend scripts reference: [frontend/README.md](frontend/README.md) -- Tone/delivery model details: [backend/services/tone_delivery_analyzer/README.md](backend/services/tone_delivery_analyzer/README.md) +- Architecture and data flow: [docs/architecture.md](docs/architecture.md) +- Backend endpoints: [backend/README.md](backend/README.md) +- Agent conventions: [AGENTS.md](AGENTS.md) +- Tone/delivery model: [backend/services/tone_delivery_analyzer/README.md](backend/services/tone_delivery_analyzer/README.md) ## Stack -| Layer | Technology | -| --------------- | ------------------------------------------------------------- | -| Frontend | Next.js (React) | -| Backend | FastAPI (Python) | -| Speech-to-text | OpenAI Whisper API | -| Tone/delivery | audeering/wav2vec2-large-robust-12-ft-emotion-msp-dim (local) | -| Feedback | Claude or GPT-4o (API) | -| Frontend deploy | Vercel | -| Backend deploy | Render or Fly.io | +| Layer | Technology | +| ---------------- | -------------------------------------------------------------- | +| Frontend | Next.js 16, React 19, TypeScript, Tailwind CSS 4 | +| Backend | FastAPI (Python) | +| Text-to-speech | Groq Orpheus (`canopylabs/orpheus-v1-english`) | +| Speech-to-text | Groq Whisper (`whisper-large-v3-turbo`) | +| Tone/delivery | `audeering/wav2vec2-large-robust-12-ft-emotion-msp-dim` (local)| +| LLM feedback | Anthropic Claude (coming) | +| Package manager | pnpm | +| Frontend deploy | Vercel (planned) | +| Backend deploy | Render or Fly.io (planned) | diff --git a/backend/.env.example b/backend/.env.example index d5b196f..27f8323 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,8 +1,12 @@ -# Copy this file to .env and fill in your keys. +# Copy this file to .env and fill in your keys before running the backend. # .env is gitignored — never commit it. +# All three keys are required for the full pipeline to function. -# Required for speech-to-text (Whisper) -OPENAI_API_KEY= +# Required for speech-to-text (Groq Whisper API — https://console.groq.com) +GROQ_API_KEY= -# Required for LLM feedback (Claude) +# Required for LLM feedback (Claude — https://console.anthropic.com) ANTHROPIC_API_KEY= + +# Reserved for future use (OpenAI fallback or embeddings) +OPENAI_API_KEY= diff --git a/backend/README.md b/backend/README.md index bc317fe..0572302 100644 --- a/backend/README.md +++ b/backend/README.md @@ -4,39 +4,99 @@ For setup and running instructions see the [root README](../README.md). ## Endpoints -| Method | Path | Status | Description | -|--------|------|--------|-------------| -| GET | `/health` | done | liveness check | -| GET | `/emotion/health` | done | confirms emotion model is loaded | -| POST | `/emotion/analyze` | done | upload audio → arousal / dominance / valence | -| POST | `/speech/transcribe` | todo | upload audio → transcript + word timestamps | -| POST | `/feedback/generate` | todo | transcript + scores → structured feedback | +| Method | Path | Status | Description | +|--------|----------------------|--------|----------------------------------------------------------| +| GET | `/health` | done | Liveness check | +| GET | `/emotion/health` | done | Confirms emotion model is loaded | +| POST | `/speech/tts` | done | Text → WAV audio via Groq Orpheus TTS | +| POST | `/speech/transcribe` | done | Audio file → per-segment transcript + emotion scores | +| POST | `/emotion/analyze` | done | Audio file → single arousal / dominance / valence score | +| POST | `/feedback/generate` | todo | Transcript + scores + question → structured LLM feedback | ## Structure ``` -app.py — entry point; registers all routers, loads emotion model at startup -requirements.txt — Python dependencies +app.py — entry point; registers all routers, loads emotion model at startup +requirements.txt — Python dependencies services/ - tone_delivery_analyzer/ - router.py — POST /emotion/analyze - emotion_model.py — model class definitions (do not modify) - run_emotion.py — standalone CLI for testing the model directly speech_to_text/ - router.py — POST /speech/transcribe (not yet implemented) + router.py — POST /speech/tts and POST /speech/transcribe + tone_delivery_analyzer/ + router.py — POST /emotion/analyze and GET /emotion/health + emotion_model.py — model class definitions (do not modify) + run_emotion.py — standalone CLI for testing the model directly llm/ - router.py — POST /feedback/generate (not yet implemented) + router.py — POST /feedback/generate (not yet implemented) + text_analysis/ + router.py — reserved for transcript scoring (not yet implemented) +``` + +## POST /speech/tts + +Converts text to speech using Groq Orpheus and returns WAV audio. + +**Request body (JSON):** +```json +{ "text": "Hello, welcome to your interview.", "voice": "hannah" } ``` -## Test /emotion/analyze +Available voices: `autumn`, `diana`, `hannah`, `austin`, `daniel`, `troy` + +`voice` defaults to `hannah` if omitted or invalid. + +**Response:** `audio/wav` binary ```bash -curl -X POST http://localhost:8000/emotion/analyze \ - -F "file=@path/to/audio.mp3" +curl -X POST http://localhost:8000/speech/tts \ + -H "Content-Type: application/json" \ + -d '{"text": "Tell me about yourself.", "voice": "hannah"}' \ + --output question.wav +``` + +Requires `GROQ_API_KEY` in `backend/.env` and Orpheus terms accepted at `https://console.groq.com/playground?model=canopylabs%2Forpheus-v1-english`. + +## POST /speech/transcribe + +Accepts an audio file. Transcribes via Groq Whisper, slices audio at segment boundaries, and scores each slice with the local emotion model. + +**Request:** `multipart/form-data`, field name `file` + +**Response:** array of segments + +```json +[ + { "start": 0.0, "end": 2.4, "text": "I think the best approach...", "arousal": 0.61, "dominance": 0.55, "valence": 0.32 }, + { "start": 2.4, "end": 5.1, "text": "would be to first consider...", "arousal": 0.48, "dominance": 0.52, "valence": 0.41 } +] +``` + +```bash +curl -X POST http://localhost:8000/speech/transcribe \ + -F "file=@path/to/audio.webm" ``` -Expected response: +Requires `GROQ_API_KEY` and the emotion model loaded (backend running). + +## POST /emotion/analyze + +Scores a full audio file as a single arousal / dominance / valence reading. Used for whole-recording analysis; `/speech/transcribe` is preferred for per-segment scores. +**Request:** `multipart/form-data`, field name `file` + +**Response:** ```json -{"arousal": 0.61, "dominance": 0.55, "valence": 0.32} +{ "arousal": 0.61, "dominance": 0.55, "valence": 0.32 } +``` + +```bash +curl -X POST http://localhost:8000/emotion/analyze \ + -F "file=@path/to/audio.mp3" ``` + +## Score interpretation + +| Dimension | Low (→ 0) | High (→ 1) | +|------------|--------------------|-------------------------| +| Arousal | Calm, flat | Excited, energetic | +| Dominance | Hesitant, passive | Assertive, confident | +| Valence | Negative, stressed | Positive, enthusiastic | diff --git a/backend/app.py b/backend/app.py index ac2d976..147dec5 100644 --- a/backend/app.py +++ b/backend/app.py @@ -25,7 +25,7 @@ async def lifespan(app: FastAPI): app.add_middleware( CORSMiddleware, allow_origins=["http://localhost:3000"], - allow_methods=["GET", "POST"], + allow_methods=["GET", "POST", "OPTIONS"], allow_headers=["*"], ) diff --git a/backend/requirements.txt b/backend/requirements.txt index bcffa8c..f20168f 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -16,7 +16,10 @@ transformers>=4.38.0 librosa>=0.10.0 numpy>=1.24.0 -# Speech-to-text — OpenAI Whisper API (requires OPENAI_API_KEY) +# Speech-to-text — Groq Whisper API (requires GROQ_API_KEY) +groq>=0.9.0 + +# OpenAI SDK — reserved for fallback / embeddings if needed openai>=1.0.0 # LLM feedback — Anthropic Claude API (requires ANTHROPIC_API_KEY) diff --git a/backend/services/speech_to_text/router.py b/backend/services/speech_to_text/router.py index 3429401..c1a6980 100644 --- a/backend/services/speech_to_text/router.py +++ b/backend/services/speech_to_text/router.py @@ -1,6 +1,162 @@ -from fastapi import APIRouter +import os +import sys +import tempfile + +from fastapi import APIRouter, File, HTTPException, UploadFile +from fastapi.responses import Response +from groq import Groq +from pydantic import BaseModel + +# tone_delivery_analyzer uses flat imports — insert its directory so they resolve. +_EMOTION_DIR = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "..", + "tone_delivery_analyzer", +) +if _EMOTION_DIR not in sys.path: + sys.path.insert(0, os.path.abspath(_EMOTION_DIR)) + +from run_emotion import load_audio, predict_adv # noqa: E402 +from services.tone_delivery_analyzer.router import _state as _emotion_state # noqa: E402 + +TARGET_SR = 16000 +_groq = Groq(api_key=os.environ.get("GROQ_API_KEY", "")) + +TTS_MODEL = "canopylabs/orpheus-v1-english" +TTS_VOICES = {"autumn", "diana", "hannah", "austin", "daniel", "troy"} +TTS_DEFAULT_VOICE = "hannah" + + +def _require_groq() -> None: + if not os.environ.get("GROQ_API_KEY"): + raise HTTPException(status_code=503, detail="GROQ_API_KEY is not configured.") + router = APIRouter() -# TODO: implement POST /transcribe +class TTSRequest(BaseModel): + text: str + voice: str = TTS_DEFAULT_VOICE + + +@router.post("/tts") +async def tts(body: TTSRequest): + """Converts text to speech using Groq Orpheus and returns WAV audio bytes.""" + _require_groq() + voice = body.voice if body.voice in TTS_VOICES else TTS_DEFAULT_VOICE + try: + response = _groq.audio.speech.create( + model=TTS_MODEL, + voice=voice, + input=body.text, + response_format="wav", + ) + audio_bytes = response.read() + except Exception as exc: + raise HTTPException(status_code=502, detail=f"TTS failed: {exc}") + + return Response(content=audio_bytes, media_type="audio/wav") + + +@router.post("/transcribe") +async def transcribe(file: UploadFile = File(...)): + """ + Accepts an audio file. Returns a list of timestamped segments, each with + the spoken text and per-segment arousal/dominance/valence scores. + + Requires GROQ_API_KEY in the environment and the emotion model loaded. + + Response shape: + [ + { + "start": 0.0, + "end": 2.4, + "text": "I think the best approach...", + "arousal": 0.61, + "dominance": 0.55, + "valence": 0.32 + }, + ... + ] + """ + _require_groq() + if _emotion_state["model"] is None: + raise HTTPException(status_code=503, detail="Emotion model not loaded yet.") + + suffix = os.path.splitext(file.filename or "upload")[1] or ".wav" + audio_bytes = await file.read() + + # Write to temp file — needed for both Groq upload and librosa decode. + try: + with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp: + tmp.write(audio_bytes) + tmp_path = tmp.name + except Exception as exc: + raise HTTPException(status_code=400, detail=f"Could not write upload: {exc}") + + try: + # --- Step 1: Transcribe with Groq Whisper, get segment timestamps --- + try: + with open(tmp_path, "rb") as f: + transcription = _groq.audio.transcriptions.create( + file=(file.filename or "audio" + suffix, f), + model="whisper-large-v3-turbo", + response_format="verbose_json", + timestamp_granularities=["segment"], + ) + except Exception as exc: + raise HTTPException( + status_code=502, detail=f"Groq transcription failed: {exc}" + ) + + segments = transcription.segments or [] + if not segments: + return [] + + # --- Step 2: Load full audio for slicing --- + try: + full_signal = load_audio(tmp_path) # float32 array at 16 kHz mono + except Exception as exc: + raise HTTPException( + status_code=422, detail=f"Could not decode audio: {exc}" + ) + + finally: + os.unlink(tmp_path) + + # --- Step 3: Score each segment with the emotion model --- + results = [] + for seg in segments: + start_s: float = seg["start"] + end_s: float = seg["end"] + text: str = seg["text"].strip() + + start_i = int(start_s * TARGET_SR) + end_i = int(end_s * TARGET_SR) + slice_signal = full_signal[start_i:end_i] + + # Skip segments too short for the model (< 0.1 s → < 1600 samples) + if len(slice_signal) < 1600: + continue + + adv, _ = predict_adv( + slice_signal, + _emotion_state["processor"], + _emotion_state["model"], + _emotion_state["device"], + ) + arousal, dominance, valence = [round(float(x), 4) for x in adv.tolist()] + + results.append( + { + "start": round(start_s, 3), + "end": round(end_s, 3), + "text": text, + "arousal": arousal, + "dominance": dominance, + "valence": valence, + } + ) + + return results diff --git a/docs/agent-workflows/README.md b/docs/agent-workflows/README.md index 103dd85..0004211 100644 --- a/docs/agent-workflows/README.md +++ b/docs/agent-workflows/README.md @@ -6,24 +6,24 @@ This directory contains canonical, agent-neutral workflow instructions for repet This repository uses multiple layers of agent-facing documentation. -- `frontend/AGENTS.md` is the always-on instruction file for frontend conventions, component/CSS standards, and agent expectations. The root [README.md](../../README.md) covers full-stack setup and architecture. +- `AGENTS.md` (repo root) is the always-on instruction file for project-wide conventions, component/CSS standards, pipeline architecture, and agent expectations. The root [README.md](../../README.md) covers full-stack setup and running the project. - Agent-specific markdown files such as `CLAUDE.md` are adapters for tools that support their own project instruction file. These should contain only the guidance that is specific to that agent or needed by that agent's workflow. - `docs/agent-workflows/` is the shared library of reusable, task-specific workflows. These documents are for on-demand tasks such as branch reviews, CSS reviews, test-suite reviews, PR text generation, and implementation planning. -Use `frontend/AGENTS.md` for durable frontend standards, use agent-specific files only when a specific tool requires them, and use the workflow docs in this directory for repeatable task instructions that should not run automatically on every request. +Use `AGENTS.md` for durable project standards, use agent-specific files only when a specific tool requires them, and use the workflow docs in this directory for repeatable task instructions that should not run automatically on every request. -Do not create agent-specific markdown files unless there is a real agent integration that reads them. This repo uses `frontend/AGENTS.md` and `frontend/CLAUDE.md`; additional files such as `gemini.md` or `codex.md` are not needed unless a specific tool requires them. +Do not create agent-specific markdown files unless there is a real agent integration that reads them. This repo uses `AGENTS.md` and `CLAUDE.md`; additional files such as `gemini.md` or `codex.md` are not needed unless a specific tool requires them. ## Purpose -- Keep task-specific prompts out of `frontend/AGENTS.md`, `CLAUDE.md`, and other always-on agent files +- Keep task-specific prompts out of `AGENTS.md`, `CLAUDE.md`, and other always-on agent files - Maintain one reviewed source of truth for recurring workflows - Allow different agent systems to reference the same workflow docs without duplicating prompt content ## Usage - Use these documents for task-specific workflows that should run on command, not automatically on every task -- Keep frontend standing rules in `frontend/AGENTS.md` +- Keep project standing rules in `AGENTS.md` - Keep agent-specific adapter files (`CLAUDE.md`, Cursor rules, Codex skills, etc.) lightweight and point them to these workflow docs when appropriate ## How to invoke these workflows @@ -78,9 +78,10 @@ Use docs/agent-workflows/figma-design-to-code.md and implement this Figma design | `pull-request-description-generator.md` | Generates PR-ready text — summary, change-overview table, grouped file-by-file changes, dependency declaration, env var callouts, migration notes, linked tickets | Reviews or recommends anything | | `feature-implementation-planning.md` | Creates an implementation plan covering scope, schema changes, complete file manifest, implementation order, integration points, and verification steps | Writes code; reviews existing code | | `figma-design-to-code.md` | Guides Figma design extraction, token mapping, and component implementation | Reviews existing code independently | +| `run-project.md` | Starts backend + frontend from scratch; assesses project state and runs only what is needed; also handles stopping the project | Requires user terminal input | ## Maintenance - Update these files when the team changes the workflow requirements or output formats - Treat these files as the canonical version rather than maintaining separate copies in multiple agent-specific files -- When adding a new workflow, add it to this README and the workflow section in `frontend/AGENTS.md` +- When adding a new workflow, add it to this README and the workflow section in `AGENTS.md` diff --git a/docs/agent-workflows/code-quality-review.md b/docs/agent-workflows/code-quality-review.md index fe59471..7512302 100644 --- a/docs/agent-workflows/code-quality-review.md +++ b/docs/agent-workflows/code-quality-review.md @@ -19,7 +19,7 @@ Perform a strict, scoped code quality review of a feature branch. ## Before review -- Read `frontend/AGENTS.md` (and [README.md](../../README.md) for full-stack context) to load project conventions, hard rules, and architecture patterns +- Read `AGENTS.md` (and [README.md](../../README.md) for full-stack context) to load project conventions, hard rules, and architecture patterns - Read commit messages or the PR description to understand the intent of the changes - Run `git diff --name-only origin/main...HEAD` to enumerate changed files (substitute the confirmed target branch if not `main`) - Read all changed files @@ -84,7 +84,7 @@ Decision rule: Check for: - environment-dependent code without fallbacks or validation -- environment variables used for a purpose that does not match their semantic role (e.g. a backend-only API key referenced in client code, or `NEXT_PUBLIC_*` used for secrets) — cross-reference against `frontend/AGENTS.md` and the root README; backend keys belong in `backend/.env` only +- environment variables used for a purpose that does not match their semantic role (e.g. a backend-only API key referenced in client code, or `NEXT_PUBLIC_*` used for secrets) — cross-reference against `AGENTS.md` and the root README; backend keys belong in `backend/.env` only - build-time versus runtime mismatches - hardcoded values that should come from configuration - unnecessary large imports for small utilities @@ -112,7 +112,7 @@ Decision rule: ### 5. Convention compliance -Check changed code against project conventions in `frontend/AGENTS.md`, specifically: +Check changed code against project conventions in `AGENTS.md`, specifically: - Duplicating backend pipeline logic (Whisper, emotion model, LLM) in the frontend instead of calling FastAPI endpoints - API keys or secrets in client bundles (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY` belong in `backend/.env` only) @@ -157,7 +157,7 @@ For new `fetch` wrappers or API client modules, flag missing tests only when a t Decision rule: -- Only flag missing coverage if it would realistically catch a regression in the changed code. This project may not have a frontend test script yet — if `pnpm test` is not configured, note that and limit findings to Storybook/state coverage per `frontend/AGENTS.md`. +- Only flag missing coverage if it would realistically catch a regression in the changed code. This project may not have a frontend test script yet — if `pnpm test` is not configured, note that and limit findings to Storybook/state coverage per `AGENTS.md`. ### 8. Comments diff --git a/docs/agent-workflows/css-and-component-standards-review.md b/docs/agent-workflows/css-and-component-standards-review.md index 7b60e56..cc617f6 100644 --- a/docs/agent-workflows/css-and-component-standards-review.md +++ b/docs/agent-workflows/css-and-component-standards-review.md @@ -12,7 +12,7 @@ Review all styling related to a branch with a strict focus on CSS quality, maint ## Before review -- Read `frontend/AGENTS.md` to load project styling conventions and hard rules +- Read `AGENTS.md` to load project styling conventions and hard rules - Run `git diff --name-only origin/main...HEAD` to enumerate changed files (substitute the confirmed target branch if not `main`) - Read adjacent component styles to understand existing patterns before flagging inconsistencies diff --git a/docs/agent-workflows/feature-implementation-planning.md b/docs/agent-workflows/feature-implementation-planning.md index 71f5322..2a2ae59 100644 --- a/docs/agent-workflows/feature-implementation-planning.md +++ b/docs/agent-workflows/feature-implementation-planning.md @@ -39,7 +39,7 @@ Group files by category such as: - presentational components + containers + `*.module.css` + `*.stories.tsx` - App Router pages (`app/`) - tests (when test runner exists) -- docs (`backend/README.md`, `frontend/AGENTS.md` if conventions change) +- docs (`backend/README.md`, `AGENTS.md` if conventions change) ## Usage diff --git a/docs/agent-workflows/figma-design-to-code.md b/docs/agent-workflows/figma-design-to-code.md index c6d8360..145f5ad 100644 --- a/docs/agent-workflows/figma-design-to-code.md +++ b/docs/agent-workflows/figma-design-to-code.md @@ -4,7 +4,7 @@ Guides **implementation of UI from a Figma design** — extracting designs via MCP tools, adapting reference code to the Interview Coach frontend stack, mapping design tokens to `app/globals.css`, and verifying component compliance before marking work complete. -**Does not:** review existing code, perform standalone CSS review, or assess code quality independently of a design task. Styling and component standards are defined in `frontend/AGENTS.md` and `css-and-component-standards-review.md` — this workflow references those standards rather than restating them. +**Does not:** review existing code, perform standalone CSS review, or assess code quality independently of a design task. Styling and component standards are defined in `AGENTS.md` and `css-and-component-standards-review.md` — this workflow references those standards rather than restating them. --- @@ -62,7 +62,7 @@ The MCP reference output is generic React + Tailwind. Before implementing: - [ ] Recording/scorecard UI uses presentational components with mock-friendly props for Storybook - [ ] No backend secrets in client code; API calls go through the FastAPI client module -There is no shared `app/components/ui/` library yet — reuse existing components in the branch or create new ones with `.module.css` + `.stories.tsx` per `frontend/AGENTS.md`. +There is no shared `app/components/ui/` library yet — reuse existing components in the branch or create new ones with `.module.css` + `.stories.tsx` per `AGENTS.md`. ### Step 3 — Map design tokens @@ -81,7 +81,7 @@ Wait for user confirmation when reuse vs new component is ambiguous. ### Step 5 — Implement -Follow `frontend/AGENTS.md` and `css-and-component-standards-review.md` for file layout, Storybook states (recording, processing, scorecard, errors), and presentational/container split. +Follow `AGENTS.md` and `css-and-component-standards-review.md` for file layout, Storybook states (recording, processing, scorecard, errors), and presentational/container split. **Responsive:** Ask how breakpoints should behave; do not infer from fixed-width Figma frames. diff --git a/docs/agent-workflows/pre-merge-full-review.md b/docs/agent-workflows/pre-merge-full-review.md index 6b86553..1ac4bb8 100644 --- a/docs/agent-workflows/pre-merge-full-review.md +++ b/docs/agent-workflows/pre-merge-full-review.md @@ -323,7 +323,7 @@ Each phase's sub-workflow doc may add more, but at minimum every phase runs thes - Pattern check on changed lines only: - New `console.log`, `console.error`, `console.warn` not gated by debug flag → finding. - New `// @ts-ignore`, `// @ts-expect-error`, `// eslint-disable-*` without a comment explaining why → finding. - - New `any` types in TypeScript files → finding (unless project conventions in `frontend/AGENTS.md` say otherwise). + - New `any` types in TypeScript files → finding (unless project conventions in `AGENTS.md` say otherwise). - New `TODO`, `FIXME`, `XXX` comments → subjective observation (variable severity). - New `fetch(` calls in non-test source files: grep the surrounding call site (within ~5 lines) for `signal:`. If absent → finding. Title: "fetch without abort signal". Recommended fix: add an `AbortController` with an appropriate timeout and pass `signal: controller.signal`. Impact if not fixed: a slow or hung upstream can hold the server request open until the platform timeout. Exception: calls inside test files, or calls where the surrounding function signature already accepts and threads a signal from the caller. - Build the project if a build command exists (`pnpm build` or equivalent). Build failure → finding. @@ -439,7 +439,7 @@ The principle: silence is the default for unchanged code. Speak about adjacent c ## Before starting -1. Read `frontend/AGENTS.md` and the root [README.md](../../README.md) to load conventions, hard rules, and architecture. +1. Read `AGENTS.md` and the root [README.md](../../README.md) to load conventions, hard rules, and architecture. 2. **Optional issue reference.** If the branch or commits reference a GitHub issue (e.g. `#12` in the branch name), record it for Phase 6 (PR description). No fixed Jira prefix is required for this project. diff --git a/docs/agent-workflows/run-project.md b/docs/agent-workflows/run-project.md new file mode 100644 index 0000000..106f571 --- /dev/null +++ b/docs/agent-workflows/run-project.md @@ -0,0 +1,169 @@ +# Run Project Workflow + +**Trigger phrases:** "run the project", "run the interview coach", "run the interview project", "start the project", "start interview coach", "start the app", "launch the project", or any close variation. + +**Before running anything**, the agent must confirm with the user: + +> "Ready to start Interview Coach. This will launch the backend (port 8000) and frontend (port 3000). Confirm?" + +Only after the user confirms does the agent proceed. The agent starts both services without any terminal input from the user. It must assess current project state first and run only what is necessary. + +--- + +## Step 1 — Assess project state + +Run all checks in parallel before doing anything: + +```bash +# 1. Does the Python venv exist? +test -d venv && echo "venv:ok" || echo "venv:missing" + +# 2. Are backend dependencies installed? (spot-check key packages) +venv/Scripts/python -c "import fastapi, groq, uvicorn" 2>/dev/null && echo "deps:ok" || echo "deps:missing" + +# 3. Does backend/.env exist and have GROQ_API_KEY set? +test -f backend/.env && grep -q "GROQ_API_KEY=." backend/.env && echo "env:ok" || echo "env:missing" + +# 4. Are frontend node_modules installed? +test -d frontend/node_modules && echo "node:ok" || echo "node:missing" + +# 7. Has the user confirmed Groq Orpheus terms? (cannot check programmatically — ask explicitly) +# See Step 2 below for how to handle this. + +# 5. Is port 8000 already in use? +netstat -an 2>/dev/null | grep ":8000" | grep LISTEN && echo "port8000:busy" || echo "port8000:free" + +# 6. Is port 3000 already in use? +netstat -an 2>/dev/null | grep ":3000" | grep LISTEN && echo "port3000:busy" || echo "port3000:free" +``` + +On Windows use PowerShell equivalents: +```powershell +Test-Path venv +venv\Scripts\python -c "import fastapi, groq, uvicorn" +Test-Path backend\.env +(Get-Content backend\.env) -match "GROQ_API_KEY=." +Test-Path frontend\node_modules +netstat -an | Select-String ":8000.*LISTENING" +netstat -an | Select-String ":3000.*LISTENING" +``` + +--- + +## Step 2 — Fix missing prerequisites + +Run only what is needed based on Step 1 results. Do not re-run steps that already passed. + +### venv missing +```bash +python -m venv venv +``` + +### Python dependencies missing +```bash +source venv/Scripts/activate # Windows Git Bash +# venv/bin/activate # macOS / Linux +pip install -r backend/requirements.txt +``` +This may take several minutes on first run (torch, transformers are large). Inform the user. + +### backend/.env missing or GROQ_API_KEY not set +**Stop and tell the user.** Do not attempt to start the backend. +Message: "backend/.env is missing or GROQ_API_KEY is not set. Please add your key to backend/.env before running. See backend/.env.example for the format." + +### Groq Orpheus TTS terms not accepted +This cannot be checked programmatically. **Always ask during the confirmation step** (before Step 1 checks run): + +> "Before I start — have you accepted the Groq Orpheus TTS terms for your account? This is a one-time step required for the interviewer voice to work. Visit https://console.groq.com/playground?model=canopylabs%2Forpheus-v1-english and click Accept if you haven't yet." + +- If the user says **yes / already done**: proceed normally. +- If the user says **no / not yet**: direct them to accept first, then re-confirm before starting. +- If the user is **unsure**: tell them to visit the URL and check — it's safe to visit even if terms are already accepted (the banner simply won't appear). + +**Symptom if skipped:** the backend starts without error, but every call to `POST /speech/tts` returns a 400 and the frontend shows "Could not load audio — check the backend is running." The recording flow still works; only the interviewer voice is silent. + +### frontend/node_modules missing +```bash +cd frontend && pnpm install +``` + +### Port already in use +Inform the user which port is occupied and by what process if determinable. Ask whether to proceed (the existing process may already be the running app). + +--- + +## Step 3 — Start the backend + +Run in background. The emotion model loads at startup — first run downloads ~1 GB and takes longer. + +```bash +# Bash +source venv/Scripts/activate && cd backend && uvicorn app:app --reload +``` + +```powershell +# PowerShell +venv\Scripts\activate; cd backend; uvicorn app:app --reload +``` + +Use `run_in_background: true`. Wait for the `Application startup complete` message before starting the frontend. Monitor the output — if startup fails (missing env var, model load error, port conflict), report the error and stop. + +--- + +## Step 4 — Start the frontend + +Run in background after backend startup is confirmed. + +```bash +cd frontend && pnpm dev +``` + +Use `run_in_background: true`. + +--- + +## Step 5 — Report to the user + +Once both are running, report: + +``` +Backend: http://localhost:8000 (API docs: http://localhost:8000/docs) +Frontend: http://localhost:3000 +``` + +If the emotion model is downloading for the first time, note that the backend will be slow to respond until the download completes (~1 GB). + +--- + +## Stopping the project + +If the user says "stop the project", "stop the app", or "kill the servers": + +1. Find uvicorn and the Next.js dev server processes +2. Terminate them + +```bash +# Bash — find and kill by port +lsof -ti:8000 | xargs kill -9 +lsof -ti:3000 | xargs kill -9 +``` + +```powershell +# PowerShell +Stop-Process -Id (Get-NetTCPConnection -LocalPort 8000).OwningProcess -Force +Stop-Process -Id (Get-NetTCPConnection -LocalPort 3000).OwningProcess -Force +``` + +--- + +## Error reference + +| Symptom | Likely cause | Fix | +|---------|-------------|-----| +| `ModuleNotFoundError: No module named 'fastapi'` | venv not activated or deps not installed | Run Step 2 | +| `OSError: [Errno 98] Address already in use` | Port occupied | Check existing process; kill or use different port | +| `groq.AuthenticationError` | GROQ_API_KEY missing or wrong | Check `backend/.env` | +| `ERR_PNPM_OUTDATED_LOCKFILE` | lockfile out of sync | Run `cd frontend && pnpm install` | +| Backend starts but emotion model never loads | First run downloading weights | Wait — normal on first run, ~1 GB | +| TTS returns 400 or 502 | Orpheus terms not accepted for this Groq account | Visit `https://console.groq.com/playground?model=canopylabs%2Forpheus-v1-english` and click Accept | +| Frontend shows "Could not load audio" | Orpheus terms not accepted, or `GROQ_API_KEY` wrong | Check terms first (see above), then verify key in `backend/.env` | diff --git a/docs/agent-workflows/test-suite-quality-review.md b/docs/agent-workflows/test-suite-quality-review.md index f30ee52..0e083d7 100644 --- a/docs/agent-workflows/test-suite-quality-review.md +++ b/docs/agent-workflows/test-suite-quality-review.md @@ -6,7 +6,7 @@ Reviews the **quality and simplicity of the existing test suite** — whether te **Does not:** check for test coverage gaps in newly written code (that is handled by `code-quality-review.md`). Does not run tests or generate new tests. Scope is existing test quality only, not coverage metrics. -**Note:** Interview Coach may not have a frontend test runner configured yet. If no tests exist, state that plainly and limit the review to Storybook story quality per `frontend/AGENTS.md` when `.stories.tsx` files are in scope. +**Note:** Interview Coach may not have a frontend test runner configured yet. If no tests exist, state that plainly and limit the review to Storybook story quality per `AGENTS.md` when `.stories.tsx` files are in scope. --- diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..490efbc --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,221 @@ +# Interview Coach — Architecture & Data Flow + +## Overview + +Interview Coach is a full-stack AI application that conducts mock job interviews. It speaks questions aloud, records the candidate's answer, transcribes the audio, scores each spoken segment for emotional delivery, and (coming) generates structured feedback using an LLM. + +The frontend orchestrates the pipeline. The backend exposes discrete API endpoints — each service does one job and returns structured data. The frontend holds all session state and calls services in sequence. + +--- + +## System Diagram + +``` +┌─────────────────────────────────────────────────────────┐ +│ Browser (Next.js) │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ │ +│ │ Question │ │ Interview │ │ Scorecard │ │ +│ │ + Voice │ │ Recording │ │ Display │ │ +│ │ Selector │ │ + VAD │ │ (coming) │ │ +│ └──────┬───────┘ └──────┬───────┘ └──────┬──────┘ │ +│ │ │ │ │ +└─────────┼──────────────────┼───────────────────┼─────────┘ + │ POST /speech/tts │ POST /speech/ │ POST /feedback/ + │ │ transcribe │ generate (coming) + ▼ ▼ ▼ +┌─────────────────────────────────────────────────────────┐ +│ FastAPI Backend │ +│ │ +│ ┌──────────────────┐ ┌──────────────────────────────┐ │ +│ │ speech_to_text │ │ tone_delivery_analyzer │ │ +│ │ │ │ │ │ +│ │ /speech/tts │ │ wav2vec2 emotion model │ │ +│ │ Groq Orpheus │ │ (loaded at startup, │ │ +│ │ TTS → WAV │ │ runs locally on CPU/GPU) │ │ +│ │ │ │ │ │ +│ │ /speech/ │──▶│ per-segment scoring │ │ +│ │ transcribe │ │ arousal/dominance/valence │ │ +│ │ Groq Whisper │ │ │ │ +│ │ → segments[] │ └──────────────────────────────┘ │ +│ └──────────────────┘ │ +│ │ +│ ┌──────────────────┐ │ +│ │ llm (coming) │ │ +│ │ /feedback/ │ │ +│ │ generate │ │ +│ │ Anthropic Claude│ │ +│ └──────────────────┘ │ +└───────────────────────────┬─────────────────────────────┘ + │ + ┌─────────────┴──────────────┐ + │ Groq Cloud │ + │ whisper-large-v3-turbo │ + │ canopylabs/orpheus-v1- │ + │ english (TTS) │ + └────────────────────────────┘ +``` + +--- + +## Interview Pipeline — Step by Step + +### Stage 1 — Question Selection + +- On page load the frontend picks a random question from `lib/prompts/questions.ts` +- First question is always the intro question ("Tell me about yourself") +- Subsequent questions are drawn without repetition from a pool of 20 (10 technical, 10 behavioral) +- The selected interviewer persona maps to a Groq Orpheus voice + +### Stage 2 — Text-to-Speech (Interviewer Introduction) + +**Frontend → `POST /speech/tts`** + +``` +Request: { "text": "Hi, I'm Hannah...", "voice": "hannah" } +Response: audio/wav binary +``` + +- Backend calls Groq Orpheus (`canopylabs/orpheus-v1-english`) +- Returns raw WAV bytes +- Frontend plays via `Audio` element +- When playback ends, recording starts automatically + +### Stage 3 — Text-to-Speech (Question) + +Same endpoint and flow as Stage 2, using the question text. +Question text is revealed on screen only after TTS finishes playing. + +### Stage 4 — Audio Recording + VAD + +- Frontend uses `MediaRecorder` API to capture microphone audio as WebM +- Web Audio API `AnalyserNode` monitors RMS levels continuously +- Recording never stops until the user has spoken at least once (`hasSpokeRef`) +- After 3 seconds of silence post-speech → "Are you done?" prompt appears +- User confirms with **I'm Done Answering** or dismisses with **Keep Going** +- Prompt auto-stops after 8 seconds if ignored + +### Stage 5 — Transcription + Emotion Scoring + +**Frontend → `POST /speech/transcribe`** + +``` +Request: multipart/form-data { file: audio.webm } + +Response: [ + { "start": 0.0, "end": 2.4, "text": "...", "arousal": 0.61, "dominance": 0.55, "valence": 0.32 }, + ... +] +``` + +**Inside the backend:** + +1. Audio file saved to temp disk +2. Sent to Groq `whisper-large-v3-turbo` with `response_format=verbose_json` and segment-level timestamps +3. Full audio loaded into memory with `librosa` (resampled to 16 kHz mono) +4. Audio sliced at each Whisper segment boundary +5. Each slice run through `audeering/wav2vec2-large-robust-12-ft-emotion-msp-dim` +6. Scores clamped to [0, 1] and rounded +7. Segments shorter than 0.1s are skipped (insufficient audio for the model) + +### Stage 6 — Results Display + +- Segments rendered in the interview card, each showing timestamp, text, and A/D/V scores +- User can move to the next question or (coming) request LLM feedback + +### Stage 7 — LLM Feedback *(coming)* + +**Frontend → `POST /feedback/generate`** + +``` +Request: { + "question": { ... }, + "segments": [ { "text": "...", "arousal": ..., ... }, ... ] +} + +Response: { + "summary": "...", + "strengths": [...], + "improvements": [...], + "deliveryNotes": "..." +} +``` + +- Backend sends assembled context to Anthropic Claude +- Structured feedback returned and rendered on the scorecard + +--- + +## Data Schemas + +### Segment (output of `/speech/transcribe`) + +```typescript +interface Segment { + start: number; // seconds from recording start + end: number; // seconds from recording start + text: string; // transcribed spoken text + arousal: number; // 0–1 (calm → excited/agitated) + dominance: number; // 0–1 (hesitant → assertive/confident) + valence: number; // 0–1 (negative/stressed → positive/enthusiastic) +} +``` + +### Interviewer + +```typescript +interface Interviewer { + voice: string; // Groq Orpheus voice name + name: string; // display name + title: string; // job title shown in dropdown +} +``` + +Available voices: `autumn`, `diana`, `hannah`, `austin`, `daniel`, `troy` + +### InterviewQuestion + +```typescript +interface InterviewQuestion { + id: number; + text: string; +} +``` + +--- + +## Key Design Decisions + +**Frontend as orchestrator** — The frontend calls each backend service in sequence and holds all session state. Backend services are stateless and single-purpose. This makes each service independently testable and lets different team members own different endpoints. + +**Segment-level scoring** — Rather than scoring the entire recording as one block, Whisper's natural sentence segments (~2–5 seconds each) are each scored independently. This lets the feedback layer pinpoint specific moments ("your confidence dropped when you discussed X") rather than giving a single overall score. + +**No chunked streaming** — The full recording is sent in one request after the user finishes speaking. Chunked-while-recording was evaluated but discarded because WebM chunk boundaries make each chunk an invalid audio file without the container headers. The current approach is simpler and more reliable. + +**Emotion model loaded at startup** — The wav2vec2 model (~1 GB) is loaded once when the backend starts and kept in memory for the lifetime of the process. This avoids a 10–30 second cold-start penalty on every request. + +**VAD without a library** — Voice activity detection uses the Web Audio API `AnalyserNode` to compute RMS levels, with a prompt-before-stop UX rather than hard auto-stop. No VAD library dependency needed; the user retains control. + +--- + +## Environment Variables + +All secrets live in `backend/.env` (gitignored). See [backend/.env.example](../backend/.env.example). + +| Variable | Required | Used by | +|--------------------|----------|--------------------------------| +| `GROQ_API_KEY` | Yes | `/speech/tts`, `/speech/transcribe` | +| `ANTHROPIC_API_KEY`| Coming | `/feedback/generate` | +| `OPENAI_API_KEY` | No | Reserved | + +--- + +## Local Development URLs + +| Service | URL | +|----------|------------------------------| +| Frontend | http://localhost:3000 | +| Backend | http://localhost:8000 | +| API docs | http://localhost:8000/docs | +| Dev — transcribe debug | http://localhost:3000/dev/transcribe | +| Dev — pipeline flow | http://localhost:3000/dev/flow | diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md deleted file mode 100644 index 2568218..0000000 --- a/frontend/AGENTS.md +++ /dev/null @@ -1,310 +0,0 @@ - - -# This is NOT the Next.js you know - -This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices. - - - -# AGENTS.md - -All frontend conventions, patterns, and agent instructions live here. `CLAUDE.md` is a thin pointer to this file. - -For full-project setup, architecture, and running both services, see the [root README](../README.md). - -## Repository instructions - -### Project overview - -**Interview Coach** is an AI-powered interview coaching platform. Users record a spoken answer to an interview question in the browser and receive structured feedback on delivery, tone, and answer quality. - -End-to-end pipeline (owned mostly by the Python backend): - -1. User records audio in the browser -2. Audio → OpenAI Whisper API → timestamped transcript -3. Audio + transcript → local Audeering wav2vec2 model → delivery scores (arousal, dominance, valence) -4. Transcript + scores + question → LLM → structured feedback -5. Frontend renders a scorecard - -The frontend is responsible for capture UX, calling backend APIs, loading states, and presenting results — not for transcription, emotion modeling, or LLM calls. - -### Tech stack - -| Area | Technology | -| --------------- | ----------------------------------------------------------- | -| Framework | Next.js 16 (App Router) | -| Language | TypeScript (`strict`) | -| UI | React 19, Tailwind CSS 4 | -| Package manager | pnpm | -| Lint / format | ESLint (`eslint-config-next`), Prettier | -| Component docs | Storybook (dependency present; add scripts when configured) | -| Git hooks | lefthook (installed via `pnpm install` prepare) | -| Deploy | Vercel (planned) | - -**Not in this repo (backend only):** FastAPI, Whisper, wav2vec2 emotion model, Claude/GPT feedback. See [backend/README.md](../backend/README.md). - -### Monorepo layout - -``` -finalAI/ - README.md — setup, architecture, how to run everything - backend/ — FastAPI (Python), http://localhost:8000 - frontend/ — this app, http://localhost:3000 - docs/ — proposal and agent workflow docs -``` - -Run the backend and frontend in separate terminals. Complete root README setup (Python venv, `backend/.env`, `pnpm install`) before developing UI against live APIs. - -### Project structure - -Current layout (early stage; grow under `app/` as features land): - -``` -frontend/ - app/ - layout.tsx — root layout, fonts, global styles - page.tsx — home (placeholder until coaching UI exists) - globals.css — Tailwind imports and CSS variables - public/ — static assets - package.json - tsconfig.json — @/* → repository root of frontend/ -``` - -Planned UI areas (names may change; colocate by feature under `app/`): - -- Question prompt and recording flow -- Upload / processing states while backend runs pipeline -- Scorecard: delivery scores, transcript highlights, LLM feedback sections - -Prefer feature folders (e.g. `app/record/`, `app/scorecard/`) with colocated components over a flat dump of files. - -### Backend integration - -- **Base URL (local):** `http://localhost:8000` -- **Interactive docs:** `http://localhost:8000/docs` - -| Method | Path | Status | Purpose | -| ------ | -------------------- | ------ | ----------------------------------------- | -| GET | `/health` | done | Liveness | -| GET | `/emotion/health` | done | Emotion model loaded | -| POST | `/emotion/analyze` | done | Audio file → arousal, dominance, valence | -| POST | `/speech/transcribe` | todo | Audio → transcript + word timestamps | -| POST | `/feedback/generate` | todo | Transcript + scores + question → feedback | - -Call these from the browser via `fetch` (or a small client module). Do not reimplement ML or LLM logic in the frontend. Use env-based API base URLs when wiring production (e.g. `NEXT_PUBLIC_API_URL`). - -Audio capture must produce a format the backend accepts (follow backend service docs when implementing upload). - -### Import conventions - -- `@/*` maps to the **frontend project root** (see `tsconfig.json`), e.g. `@/app/components/Scorecard`. -- Prefer `@/` over deep relative paths (`../../../`). - -### Hard rules (never violate) - -1. Never run `git commit` or `git push` without explicit user approval. -2. Do not put backend secrets or API keys in frontend code — keys belong in `backend/.env` only. -3. Do not duplicate pipeline logic (Whisper, emotion model, LLM) in the frontend; use backend endpoints. -4. Use `next/image` for images and `next/link` for internal navigation — not raw `` or `` for in-app routes. -5. Prefer `@/` imports over long relative paths when the alias applies. -6. No inline `style` props in JSX (exception: `*.stories.tsx` for Storybook decorators/layout; and dev-only diagnostic pages under `app/dev/**` if added). -7. Every new component that has any of its own styles requires `ComponentName.module.css` — no exceptions for "simple" components. A component with zero styles of its own (thin wrapper composing other styled components) does not need its own `.module.css`; the moment any style is added, create the module file. -8. Every new reusable component or component with meaningful visual states requires `ComponentName.stories.tsx` with a named story per visual state and real mock data (not empty defaults that rely on failed fetches). -9. If you modify a component's structural JSX, proactively update its `.module.css` and `.stories.tsx` when those files exist — do not wait to be asked. -10. Do not modify `AGENTS.md` without proposing the change first when the edit reflects new team conventions. - ---- - -## Development commands - -Run from `frontend/` unless noted. - -| Task | Command | -| ------------ | ------------------- | -| Dev server | `pnpm dev` → :3000 | -| Production | `pnpm build` | -| Start prod | `pnpm start` | -| Lint | `pnpm lint` | -| Type check | `pnpm typecheck` | -| Format | `pnpm format` | -| Format check | `pnpm format:check` | - -Install hooks: `pnpm install` (runs `lefthook install` via `prepare`). - -Git hooks (repo root `lefthook.yml`): - -- **pre-commit:** Prettier and Ruff format **staged** files and re-stage fixes (`stage_fixed: true`) — formatting is applied before the commit lands. -- **pre-push:** ESLint, Prettier check, and TypeScript on the frontend; Ruff check/format on the backend (catches `--no-verify` commits). Set `LEFTHOOK=0` to skip in CI if needed. - -Script reference: [frontend/README.md](./README.md). - ---- - -## Environment variables - -Backend keys live in **`backend/.env`** (gitignored): `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`. See root README. - -When the UI calls the API across environments, add frontend vars as needed, for example: - -| Variable | Purpose | -| --------------------- | ---------------------------------- | -| `NEXT_PUBLIC_API_URL` | FastAPI base URL (e.g. production) | - -Use a local default of `http://localhost:8000` in development when the var is unset. - ---- - -## Code organization - -- **API client:** Centralize backend `fetch` calls in a small module (e.g. `lib/api.ts` or `lib/interview-coach/`) with typed request/response shapes aligned with backend responses. -- **Types:** Colocate feature types in `types.ts` next to the feature, or next to the API client for shared DTOs. -- **Server vs client:** Use Server Components by default; add `'use client'` only for recording, media APIs, and interactive UI. -- **Errors:** Surface user-visible errors from failed API calls; log details with `console.error` in development. Do not swallow failures silently. -- **State:** Prefer React state and context for recording/session flow before adding external state libraries. - ---- - -## React and Next.js patterns - -### File naming - -- Routes: `page.tsx`, `layout.tsx` -- API routes (if added): `route.ts` -- Client-only subtrees: `*Client.tsx` with `'use client'` - -### Components and styling - -Every new UI component uses **three files** when it has its own styles or meaningful visual states: - -1. **`ComponentName.tsx`** — No inline `style` props (except in stories). Tailwind utilities in JSX for generic layout/spacing; `.module.css` for component-specific look and state variants. -2. **`ComponentName.module.css`** — Required when the component has any styles of its own (colors, borders, state variants, animations). Not required for zero-style wrappers that only compose other components. -3. **`ComponentName.stories.tsx`** — Required for reusable UI and any component with distinct visual states. Each state gets a **named story with explicit mock props** (recording, uploading, scorecard populated, API error, mic denied, etc.). - -**Stories must use real mock data.** Pass props directly. Do not rely on network calls, empty states from failed fetches, or bare `Default: Story = {}` as a substitute for real coverage. - -**Data-fetching components: presentational + container split** - -- **Presentational** (e.g. `Scorecard`, `RecordingControls`) — pure view; takes `loading`, `scores`, `transcript`, `error`, callbacks as props; owns JSX + `.module.css`; stories cover every visual state with mocks. -- **Container** (e.g. `ScorecardPanel`, `RecordPage`) — owns `fetch`, MediaRecorder, and session state; renders the presentational component with live or fetched data. Container stories may be thin; presentational stories carry the visual contract. - -**Tailwind vs module CSS** - -- **Tailwind in JSX:** generic layout (flex, grid, gap), spacing, display, alignment, visibility. -- **`.module.css`:** hover/focus/disabled/error states, animations, component-specific visual identity, anything that would clutter JSX with long class strings. -- A component using only Tailwind with no `.module.css` is missing its module file if it has component-specific styles anywhere. -- Do not put `group-hover:`, `peer-hover:`, or similar parent-context variants in JSX — use `:global(.group):hover .className` in the module file. Do not use `@apply group-hover:*` or `@apply peer-*:*` in `.module.css` (CSS module scoping breaks these). - -**Design tokens** - -- Prefer CSS variables from `app/globals.css` over hard-coded hex, spacing, or font sizes. -- If Figma or design uses a raw value, map to the nearest existing token; flag gaps rather than silently hard-coding. - -**Accessibility** - -- Recording controls: visible labels, keyboard operability, clear focus styles, permission-denied and error states in stories. -- Scorecard and feedback text: semantic headings, sufficient contrast; do not rely on color alone for state. - -**Hydration** - -- Do not use `suppressHydrationWarning` to silence mismatches — fix the root cause (defer browser-only values until after mount when needed). - -External links may use `` with `target="_blank"` and `rel="noopener noreferrer"`; internal routes use `next/link`. - -### Storybook - -- Storybook is listed in `package.json`; add `storybook` / `build-storybook` scripts when the config is initialized. -- Colocate `ComponentName.stories.tsx` next to the component. -- Interview Coach stories to plan for as UI grows: `RecordingControls` (idle, recording, paused, permission denied), `ProcessingStatus` (transcribing, analyzing, generating feedback), `DeliveryScores` (loading, populated), `Scorecard` (empty, partial, full, error). - -### Data fetching - -- For backend calls from Client Components, use `fetch` to `NEXT_PUBLIC_API_URL` or localhost default. -- For secrets or server-only aggregation, use Server Components or Route Handlers — never expose backend API keys to the client. - ---- - -## Agent limitations and escalation - -### Do not do autonomously - -- `git commit` or `git push` without explicit approval -- Deploy to Vercel or any environment unless asked -- Change `backend/` emotion model code (`emotion_model.py` is marked do not modify) -- Commit `.env` files or API keys - -### Stop and ask when - -- Requirements for recording format, session flow, or scorecard layout are ambiguous -- A change needs new backend contract fields not yet in [backend/README.md](../backend/README.md) -- Implementing features that depend on todo endpoints (`/speech/transcribe`, `/feedback/generate`) before they exist - ---- - -## Git conventions - -- Ask before committing or pushing. -- Hooks: pre-commit auto-formats staged files; pre-push runs lint, format check, and typecheck (see `lefthook.yml` at repo root). -- Keep commits focused; match existing message style on the branch when one exists. - ---- - -## Domain context - -- **Delivery scores:** Arousal, dominance, and valence from the local wav2vec2 model — communicate as delivery/tone signals, not as clinical or diagnostic labels unless product copy says otherwise. -- **Transcript:** Timestamped text from Whisper; UI may show highlights or pacing once word timestamps exist. -- **Feedback:** Structured LLM output (Claude or GPT-4o via backend) — render sections clearly; avoid presenting raw model JSON to users. -- **Recording UX:** Browser MediaRecorder (or equivalent); handle permission denial, unsupported browsers, and upload failures gracefully. - ---- - -## Testing - -- No frontend test runner is configured yet. When tests are added, colocate with the feature (`ComponentName.test.tsx` next to the component) or mirror paths under `__tests__/` at the frontend root. -- Prefer behavior-focused tests for recording flow, API client error handling, and scorecard rendering with mock props. -- Only add tests when they cover realistic regression risk — not trivial render-only smoke tests. - ---- - -## Key references - -| Topic | Location | -| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | -| Full stack setup | [../README.md](../README.md) | -| Backend endpoints | [../backend/README.md](../backend/README.md) | -| Emotion model | [../backend/services/tone_delivery_analyzer/README.md](../backend/services/tone_delivery_analyzer/README.md) | -| Frontend scripts | [README.md](./README.md) | -| CSS/component review workflow | [../docs/agent-workflows/css-and-component-standards-review.md](../docs/agent-workflows/css-and-component-standards-review.md) | -| Agent workflows | [../docs/agent-workflows/](../docs/agent-workflows/) | - ---- - -## Maintaining AGENTS.md - -After features or conventions change, update this file so new sessions stay accurate: - -- New routes, components, or folder layout → **Project structure** -- New `pnpm` scripts → **Development commands** -- New frontend env vars → **Environment variables** -- New or changed backend contracts → **Backend integration** -- New hard rules or team decisions → **Hard rules** or relevant section - -For task-specific reviews (PR text, branch audit, code quality), use workflows in `docs/agent-workflows/` rather than bloating this file. Invoke explicitly, e.g.: - -```md -Use docs/agent-workflows/pull-request-description-generator.md for this branch. -``` - -See [docs/agent-workflows/README.md](../docs/agent-workflows/README.md) for the full list. - -| Workflow | Use when | -| --------------------------------------- | ---------------------------------------------- | -| `pre-merge-full-review.md` | Full pre-merge review with phase gates | -| `branch-change-impact-audit.md` | What changed and regression risks vs `main` | -| `code-quality-review.md` | Correctness, conventions, API integration | -| `css-and-component-standards-review.md` | module.css, Tailwind placement, Storybook | -| `test-suite-quality-review.md` | Test simplicity (or Storybook if no tests yet) | -| `manual-qa-checklist-generator.md` | Manual QA for recording/scorecard flows | -| `pull-request-description-generator.md` | PR title and description from diff | -| `feature-implementation-planning.md` | Plan before non-trivial features | -| `feature-flag-gating-review.md` | Env/flag gating completeness | -| `figma-design-to-code.md` | Implement UI from Figma | diff --git a/frontend/app/InterviewClient.module.css b/frontend/app/InterviewClient.module.css new file mode 100644 index 0000000..048da15 --- /dev/null +++ b/frontend/app/InterviewClient.module.css @@ -0,0 +1,226 @@ +.page { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100vh; + padding: 2rem; + gap: 2rem; +} + +.card { + width: 100%; + max-width: 680px; + background: var(--color-card); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 16px; + padding: 2.5rem; + display: flex; + flex-direction: column; + gap: 1.75rem; +} + +.meta { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.1em; + opacity: 0.4; +} + +.question { + font-size: 1.2rem; + line-height: 1.65; + font-weight: 500; +} + +.statusRow { + display: flex; + align-items: center; + gap: 0.75rem; + font-size: 0.85rem; + min-height: 1.5rem; +} + +.dot { + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--color-muted); + flex-shrink: 0; +} + +.dot.playing { + background: var(--color-info); + animation: pulse 1s ease-in-out infinite; +} + +.dot.recording { + background: var(--color-danger); + animation: pulse 0.8s ease-in-out infinite; +} + +.dot.processing { + background: var(--color-warning); + animation: pulse 1.2s ease-in-out infinite; +} + +.dot.done { + background: var(--color-success); +} + +@keyframes pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.3; + } +} + +.actions { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; +} + +.btnPrimary { + padding: 0.6rem 1.4rem; + background: #fff; + color: #000; + border: none; + border-radius: 8px; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; +} + +.btnPrimary:disabled { + opacity: 0.35; + cursor: not-allowed; +} + +.btnSecondary { + padding: 0.6rem 1.4rem; + background: transparent; + color: inherit; + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 8px; + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; +} + +.btnSecondary:disabled { + opacity: 0.35; + cursor: not-allowed; +} + +.btnDanger { + padding: 0.6rem 1.4rem; + background: var(--color-danger); + color: #fff; + border: none; + border-radius: 8px; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; +} + +.segments { + display: flex; + flex-direction: column; + gap: 0.5rem; + max-height: 260px; + overflow-y: auto; +} + +.segment { + background: rgba(255, 255, 255, 0.04); + border-radius: 6px; + padding: 0.6rem 0.8rem; + font-size: 0.82rem; + line-height: 1.5; +} + +.segmentTime { + font-size: 0.68rem; + opacity: 0.4; + margin-bottom: 0.2rem; + font-family: monospace; +} + +.segmentScores { + display: flex; + gap: 1rem; + margin-top: 0.3rem; + font-size: 0.72rem; + font-family: monospace; + color: var(--color-text-score); +} + +.interviewerRow { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.interviewerLabel { + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.07em; + opacity: 0.5; + white-space: nowrap; +} + +.interviewerSelect { + flex: 1; + background: var(--color-surface); + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 6px; + color: var(--color-text-primary); + font-size: 0.88rem; + padding: 0.4rem 0.7rem; + cursor: pointer; +} + +.interviewerSelect option { + background: var(--color-surface); + color: var(--color-text-primary); +} + +.interviewerSelect:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.questionPlaceholder { + font-size: 1rem; + opacity: 0.35; + font-style: italic; + line-height: 1.6; +} + +.donePrompt { + display: flex; + flex-direction: column; + gap: 0.75rem; + background: rgba(239, 68, 68, 0.08); + border: 1px solid rgba(239, 68, 68, 0.3); + border-radius: 10px; + padding: 1rem 1.2rem; + font-size: 0.9rem; +} + +.promptActions { + display: flex; + gap: 0.6rem; +} + +.divider { + height: 1px; + background: rgba(255, 255, 255, 0.07); +} diff --git a/frontend/app/InterviewClient.tsx b/frontend/app/InterviewClient.tsx new file mode 100644 index 0000000..8db9e66 --- /dev/null +++ b/frontend/app/InterviewClient.tsx @@ -0,0 +1,370 @@ +"use client"; + +import { useCallback, useRef, useState } from "react"; +import { + INTRO_QUESTION, + pickRandomQuestion, + type InterviewQuestion, +} from "@/lib/prompts/questions"; +import { INTERVIEWERS, DEFAULT_INTERVIEWER, type Interviewer } from "@/lib/prompts/interviewers"; +import styles from "./InterviewClient.module.css"; + +const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8000"; +const CHUNK_INTERVAL_MS = 15_000; +const SILENCE_THRESHOLD = 0.015; +const SILENCE_BEFORE_PROMPT_MS = 3_000; +const SILENCE_AFTER_PROMPT_MS = 8_000; + +type Stage = "idle" | "playing" | "recording" | "processing" | "done"; + +interface Segment { + start: number; + end: number; + text: string; + arousal: number; + dominance: number; + valence: number; +} + +function buildIntro(interviewer: Interviewer): string { + return `Hi there, welcome. I'm ${interviewer.name}, ${interviewer.title}. Thank you so much for coming in today — we're really glad to have you. Let's go ahead and get started.`; +} + +async function speakWithGroq(text: string, voice: string, signal: AbortSignal): Promise { + const res = await fetch(`${API_BASE}/speech/tts`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ text, voice }), + signal, + }); + if (!res.ok) throw new Error(`TTS ${res.status}`); + const blob = await res.blob(); + const url = URL.createObjectURL(blob); + return new Promise((resolve, reject) => { + const audio = new Audio(url); + audio.onended = () => { + URL.revokeObjectURL(url); + resolve(); + }; + audio.onerror = () => { + URL.revokeObjectURL(url); + reject(new Error("Audio playback failed")); + }; + audio.play(); + }); +} + +export default function InterviewClient() { + const [interviewer, setInterviewer] = useState(DEFAULT_INTERVIEWER); + const [question, setQuestion] = useState(null); + const [questionNumber, setQuestionNumber] = useState(0); + const [stage, setStage] = useState("idle"); + const [showQuestion, setShowQuestion] = useState(false); + const [showDonePrompt, setShowDonePrompt] = useState(false); + const [segments, setSegments] = useState([]); + const [statusText, setStatusText] = useState( + "Select your interviewer and press Start Interview." + ); + + const mediaRecorderRef = useRef(null); + const accumulatedRef = useRef([]); + const audioCtxRef = useRef(null); + const silenceTimerRef = useRef | null>(null); + const autoStopTimerRef = useRef | null>(null); + const hasSpokeRef = useRef(false); + const rafRef = useRef(null); + const abortRef = useRef(null); + const usedIdsRef = useRef>(new Set()); + + const newAbort = useCallback((): AbortSignal => { + abortRef.current?.abort(); + const ctrl = new AbortController(); + abortRef.current = ctrl; + return ctrl.signal; + }, []); + + const clearTimers = useCallback(() => { + if (silenceTimerRef.current) { + clearTimeout(silenceTimerRef.current); + silenceTimerRef.current = null; + } + if (autoStopTimerRef.current) { + clearTimeout(autoStopTimerRef.current); + autoStopTimerRef.current = null; + } + if (rafRef.current !== null) { + cancelAnimationFrame(rafRef.current); + rafRef.current = null; + } + }, []); + + const stopRecording = useCallback(() => { + clearTimers(); + setShowDonePrompt(false); + mediaRecorderRef.current?.stop(); + mediaRecorderRef.current?.stream.getTracks().forEach((t) => t.stop()); + audioCtxRef.current?.close(); + audioCtxRef.current = null; + }, [clearTimers]); + + const sendAudio = useCallback( + async (blob: Blob) => { + setStage("processing"); + setStatusText("Transcribing and scoring your answer…"); + const form = new FormData(); + form.append("file", blob, "answer.webm"); + const signal = newAbort(); + try { + const res = await fetch(`${API_BASE}/speech/transcribe`, { + method: "POST", + body: form, + signal, + }); + if (!res.ok) throw new Error(`${res.status}`); + const data: Segment[] = await res.json(); + setSegments(data); + setStage("done"); + setStatusText("Answer received. Review your response or move to the next question."); + } catch (err) { + setStatusText(`Error: ${err}. Try again.`); + setStage("idle"); + } + }, + [newAbort] + ); + + const startRecording = useCallback(async () => { + let stream: MediaStream; + try { + stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + } catch { + alert("Microphone access denied."); + setStage("idle"); + return; + } + + accumulatedRef.current = []; + hasSpokeRef.current = false; + const mr = new MediaRecorder(stream, { mimeType: "audio/webm" }); + mediaRecorderRef.current = mr; + + const ctx = new AudioContext(); + audioCtxRef.current = ctx; + const source = ctx.createMediaStreamSource(stream); + const analyser = ctx.createAnalyser(); + analyser.fftSize = 512; + source.connect(analyser); + const buf = new Float32Array(analyser.fftSize); + + const showPrompt = () => { + setShowDonePrompt(true); + setStatusText("It looks like you've paused. Are you finished answering?"); + autoStopTimerRef.current = setTimeout(() => stopRecording(), SILENCE_AFTER_PROMPT_MS); + }; + + const checkSilence = () => { + if (!audioCtxRef.current) return; + analyser.getFloatTimeDomainData(buf); + const rms = Math.sqrt(buf.reduce((s, v) => s + v * v, 0) / buf.length); + if (rms >= SILENCE_THRESHOLD) { + hasSpokeRef.current = true; + setShowDonePrompt(false); + clearTimers(); + setStatusText('Recording… press "I\'m Done Answering" when finished.'); + } else if (hasSpokeRef.current && !silenceTimerRef.current) { + silenceTimerRef.current = setTimeout(showPrompt, SILENCE_BEFORE_PROMPT_MS); + } + rafRef.current = requestAnimationFrame(checkSilence); + }; + rafRef.current = requestAnimationFrame(checkSilence); + + mr.ondataavailable = (e) => { + if (e.data.size > 0) accumulatedRef.current.push(e.data); + }; + mr.onstop = () => { + const blob = new Blob(accumulatedRef.current, { type: "audio/webm" }); + if (blob.size > 0) sendAudio(blob); + }; + + mr.start(CHUNK_INTERVAL_MS); + setStage("recording"); + setStatusText('Recording… press "I\'m Done Answering" when finished.'); + }, [sendAudio, stopRecording, clearTimers]); + + const startInterview = useCallback(async () => { + usedIdsRef.current.clear(); + const signal = newAbort(); + const firstQuestion = INTRO_QUESTION; + setQuestion(firstQuestion); + setQuestionNumber(1); + setSegments([]); + setShowQuestion(false); + setStage("playing"); + setStatusText(`${interviewer.name} is introducing themselves…`); + + try { + await speakWithGroq(buildIntro(interviewer), interviewer.voice, signal); + setStatusText(`${interviewer.name} is asking the first question…`); + await speakWithGroq(firstQuestion.text, interviewer.voice, signal); + } catch { + setStatusText("Could not load audio — check the backend is running."); + setStage("idle"); + return; + } + + setShowQuestion(true); + startRecording(); + }, [interviewer, newAbort, startRecording]); + + const nextQuestion = useCallback(async () => { + stopRecording(); + const signal = newAbort(); + const next = pickRandomQuestion(usedIdsRef.current); + setQuestion(next); + setQuestionNumber((n) => n + 1); + setSegments([]); + setShowQuestion(false); + setShowDonePrompt(false); + setStage("playing"); + setStatusText(`${interviewer.name} is asking the next question…`); + + try { + await speakWithGroq(next.text, interviewer.voice, signal); + } catch { + setStatusText("Could not load audio — check the backend is running."); + setStage("idle"); + return; + } + + setShowQuestion(true); + startRecording(); + }, [interviewer, newAbort, stopRecording, startRecording]); + + const dotClass = [ + styles.dot, + stage === "playing" ? styles.playing : "", + stage === "recording" ? styles.recording : "", + stage === "processing" ? styles.processing : "", + stage === "done" ? styles.done : "", + ] + .filter(Boolean) + .join(" "); + + return ( +
+
+
+ Interview Coach + {questionNumber > 0 && ( + <> + · + Question {questionNumber} + + )} +
+ + {showQuestion && question &&

{question.text}

} + + {stage === "playing" && !showQuestion && ( +

{interviewer.name} is speaking…

+ )} + +
+ + +
+ +
+ +
+ + {statusText} +
+ + {showDonePrompt && ( +
+ It looks like you've paused. Are you finished answering? +
+ + +
+
+ )} + +
+ {stage === "idle" && ( + + )} + + {stage === "recording" && !showDonePrompt && ( + + )} + + {stage === "done" && ( + <> + + + + )} +
+ + {segments.length > 0 && ( + <> +
+
+ {segments.map((seg, i) => ( +
+
+ {seg.start.toFixed(2)}s – {seg.end.toFixed(2)}s +
+
{seg.text}
+
+ A {seg.arousal.toFixed(2)} + D {seg.dominance.toFixed(2)} + V {seg.valence.toFixed(2)} +
+
+ ))} +
+ + )} +
+
+ ); +} diff --git a/frontend/app/dev/transcribe/TranscribeDevClient.module.css b/frontend/app/dev/transcribe/TranscribeDevClient.module.css new file mode 100644 index 0000000..4633dfd --- /dev/null +++ b/frontend/app/dev/transcribe/TranscribeDevClient.module.css @@ -0,0 +1,132 @@ +.page { + padding: 2rem; + max-width: 900px; + margin: 0 auto; + font-family: monospace; +} + +.controls { + display: flex; + gap: 1rem; + align-items: center; + margin-bottom: 1.5rem; +} + +.startBtn { + padding: 0.6rem 1.4rem; + background: var(--color-success-alt); + color: #fff; + border: none; + border-radius: 6px; + font-size: 1rem; + cursor: pointer; +} + +.startBtn:disabled { + background: var(--color-muted); + cursor: not-allowed; +} + +.stopBtn { + padding: 0.6rem 1.4rem; + background: var(--color-danger-alt); + color: #fff; + border: none; + border-radius: 6px; + font-size: 1rem; + cursor: pointer; +} + +.stopBtn:disabled { + background: var(--color-muted); + cursor: not-allowed; +} + +.status { + font-size: 0.85rem; + color: var(--color-muted); +} + +.recording { + color: var(--color-danger-alt); + font-weight: bold; +} + +.columns { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1.5rem; +} + +.panel { + background: var(--color-surface-dark); + color: var(--color-text-muted); + border-radius: 8px; + padding: 1rem; + min-height: 400px; + overflow-y: auto; +} + +.panelTitle { + font-size: 0.75rem; + color: var(--color-muted-light); + text-transform: uppercase; + letter-spacing: 0.08em; + margin-bottom: 0.75rem; +} + +.chunkEntry { + border-top: 1px solid var(--color-border-surface); + padding: 0.6rem 0; + font-size: 0.8rem; +} + +.chunkEntry:first-of-type { + border-top: none; +} + +.chunkLabel { + color: var(--color-muted); + margin-bottom: 0.25rem; +} + +.chunkPending { + color: var(--color-warning); +} + +.chunkError { + color: var(--color-text-error); +} + +.segment { + margin: 0.4rem 0; + padding: 0.4rem 0.6rem; + background: var(--color-surface); + border-radius: 4px; + line-height: 1.5; +} + +.segmentTime { + color: var(--color-text-accent); + font-size: 0.72rem; + margin-bottom: 0.15rem; +} + +.segmentText { + color: var(--color-text-primary); + margin-bottom: 0.25rem; +} + +.scores { + display: flex; + gap: 0.75rem; + font-size: 0.7rem; + color: var(--color-text-score); +} + +.empty { + color: var(--color-muted-faint); + font-size: 0.85rem; + margin-top: 2rem; + text-align: center; +} diff --git a/frontend/app/dev/transcribe/TranscribeDevClient.tsx b/frontend/app/dev/transcribe/TranscribeDevClient.tsx new file mode 100644 index 0000000..c5fb1cd --- /dev/null +++ b/frontend/app/dev/transcribe/TranscribeDevClient.tsx @@ -0,0 +1,170 @@ +"use client"; + +import { useCallback, useRef, useState } from "react"; +import styles from "./TranscribeDevClient.module.css"; + +const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8000"; +// Emit a chunk every N milliseconds while recording. +const CHUNK_INTERVAL_MS = 15_000; + +interface Segment { + start: number; + end: number; + text: string; + arousal: number; + dominance: number; + valence: number; +} + +interface ChunkEntry { + id: number; + sentAt: string; + byteSize: number; + status: "pending" | "done" | "error"; + segments: Segment[]; + error?: string; +} + +export default function TranscribeDevClient() { + const [recording, setRecording] = useState(false); + const [chunks, setChunks] = useState([]); + const mediaRecorderRef = useRef(null); + const chunkIdRef = useRef(0); + // Accumulate all raw blobs so each send is a valid, complete WebM file. + const accumulatedRef = useRef([]); + + const sendChunk = useCallback(async (blob: Blob, id: number) => { + const form = new FormData(); + form.append("file", blob, `chunk-${id}.webm`); + + setChunks((prev) => prev.map((c) => (c.id === id ? { ...c, status: "pending" } : c))); + + try { + const res = await fetch(`${API_BASE}/speech/transcribe`, { + method: "POST", + body: form, + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(`${res.status}: ${text}`); + } + const segments: Segment[] = await res.json(); + setChunks((prev) => prev.map((c) => (c.id === id ? { ...c, status: "done", segments } : c))); + } catch (err) { + setChunks((prev) => + prev.map((c) => (c.id === id ? { ...c, status: "error", error: String(err) } : c)) + ); + } + }, []); + + const start = useCallback(async () => { + let stream: MediaStream; + try { + stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + } catch { + alert("Microphone access denied."); + return; + } + + const mr = new MediaRecorder(stream, { mimeType: "audio/webm" }); + mediaRecorderRef.current = mr; + setChunks([]); + chunkIdRef.current = 0; + accumulatedRef.current = []; + + mr.ondataavailable = (e) => { + if (e.data.size === 0) return; + accumulatedRef.current.push(e.data); + // Send the full accumulated audio so Groq always receives a valid WebM file. + const fullBlob = new Blob(accumulatedRef.current, { type: "audio/webm" }); + const id = ++chunkIdRef.current; + const entry: ChunkEntry = { + id, + sentAt: new Date().toLocaleTimeString(), + byteSize: fullBlob.size, + status: "pending", + segments: [], + }; + setChunks((prev) => [...prev, entry]); + sendChunk(fullBlob, id); + }; + + mr.start(CHUNK_INTERVAL_MS); + setRecording(true); + }, [sendChunk]); + + const stop = useCallback(() => { + mediaRecorderRef.current?.stop(); + mediaRecorderRef.current?.stream.getTracks().forEach((t) => t.stop()); + setRecording(false); + }, []); + + const sentPanel = chunks.map((c) => ( +
+
+ Chunk {c.id} — {c.sentAt} — {(c.byteSize / 1024).toFixed(1)} KB +
+ {c.status === "pending" &&
⏳ waiting…
} + {c.status === "error" &&
✗ {c.error}
} + {c.status === "done" &&
{c.segments.length} segment(s) returned
} +
+ )); + + // Always show segments from the latest chunk that has returned results. + const latestDone = [...chunks].reverse().find((c) => c.status === "done"); + const resultPanel = (latestDone?.segments ?? []).map((seg, i) => ( +
+
+ {seg.start.toFixed(2)}s – {seg.end.toFixed(2)}s +
+
{seg.text}
+
+ A {seg.arousal.toFixed(2)} + D {seg.dominance.toFixed(2)} + V {seg.valence.toFixed(2)} +
+
+ )); + + return ( +
+

Dev — Transcribe + Emotion

+

+ Chunks audio every {CHUNK_INTERVAL_MS / 1000}s and sends each to{" "} + POST /speech/transcribe while you speak. Results appear as each chunk returns. +

+ +
+ + + + {recording ? "● Recording" : "Idle"} + +
+ +
+
+
Sent chunks
+ {sentPanel.length === 0 ? ( +
Nothing sent yet
+ ) : ( + sentPanel + )} +
+ +
+
Returned segments
+ {resultPanel.length === 0 ? ( +
No results yet
+ ) : ( + resultPanel + )} +
+
+
+ ); +} diff --git a/frontend/app/dev/transcribe/page.tsx b/frontend/app/dev/transcribe/page.tsx new file mode 100644 index 0000000..7fc899a --- /dev/null +++ b/frontend/app/dev/transcribe/page.tsx @@ -0,0 +1,5 @@ +import TranscribeDevClient from "./TranscribeDevClient"; + +export default function TranscribeDevPage() { + return ; +} diff --git a/frontend/app/globals.css b/frontend/app/globals.css index a2dc41e..6845b6c 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -3,6 +3,26 @@ :root { --background: #ffffff; --foreground: #171717; + + /* Semantic color tokens */ + --color-surface: #1f2937; + --color-surface-dark: #111; + --color-muted: #6b7280; + --color-muted-light: #9ca3af; + --color-muted-faint: #4b5563; + --color-danger: #ef4444; + --color-danger-alt: #dc2626; + --color-success: #22c55e; + --color-success-alt: #16a34a; + --color-warning: #f59e0b; + --color-info: #3b82f6; + --color-text-primary: #f3f4f6; + --color-text-muted: #e5e7eb; + --color-text-accent: #60a5fa; + --color-text-score: #a3e635; + --color-text-error: #f87171; + --color-border-surface: #374151; + --color-card: #18181b; } @theme inline { diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 0d2813c..50b1c97 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -1,34 +1,5 @@ -import Link from "next/link"; +import InterviewClient from "./InterviewClient"; export default function Home() { - return ( -
-
-

- Interview Coach -

-

- Practice answers. Get structured feedback. -

-

- Frontend shells for the scorecard and pipeline are ready for integration. Use the dev flow - page to watch mock data move through each stage. -

-
- -
- ); + return ; } diff --git a/frontend/lib/prompts/interviewers.ts b/frontend/lib/prompts/interviewers.ts new file mode 100644 index 0000000..4e1c64c --- /dev/null +++ b/frontend/lib/prompts/interviewers.ts @@ -0,0 +1,16 @@ +export interface Interviewer { + voice: string; + name: string; + title: string; +} + +export const INTERVIEWERS: Interviewer[] = [ + { voice: "hannah", name: "Hannah", title: "Senior Engineering Manager" }, + { voice: "autumn", name: "Autumn", title: "Lead Software Engineer" }, + { voice: "diana", name: "Diana", title: "Director of Engineering" }, + { voice: "austin", name: "Austin", title: "Staff Engineer" }, + { voice: "daniel", name: "Daniel", title: "Engineering Team Lead" }, + { voice: "troy", name: "Troy", title: "Principal Engineer" }, +]; + +export const DEFAULT_INTERVIEWER = INTERVIEWERS[0]; diff --git a/frontend/lib/prompts/questions.ts b/frontend/lib/prompts/questions.ts new file mode 100644 index 0000000..d76b6f5 --- /dev/null +++ b/frontend/lib/prompts/questions.ts @@ -0,0 +1,102 @@ +export interface InterviewQuestion { + id: number; + text: string; +} + +export const INTRO_QUESTION: InterviewQuestion = { + id: 0, + text: "Tell me about yourself — walk me through your background, what got you interested in software development, what you've been working on recently, and what you're looking for in your next role.", +}; + +export const QUESTIONS: InterviewQuestion[] = [ + { + id: 1, + text: "Walk me through what happens from the moment a user types a URL into their browser until the webpage appears on their screen. Include as much detail as you can about each step in that process.", + }, + { + id: 2, + text: "Explain the concept of version control and describe your typical workflow when starting a new feature using Git, from creating a branch all the way through getting your code merged.", + }, + { + id: 3, + text: "Describe the difference between synchronous and asynchronous programming. Explain how async and await work in JavaScript and walk me through a real scenario where you would choose async over synchronous code.", + }, + { + id: 4, + text: "Explain what a REST API is, how it works, and describe the key principles that make an API RESTful. Then walk me through what a typical request and response cycle looks like.", + }, + { + id: 5, + text: "Walk me through the four main pillars of object-oriented programming — encapsulation, abstraction, inheritance, and polymorphism — and give me a concrete example of each one.", + }, + { + id: 6, + text: "Explain what a database index is, describe how it works under the hood, and walk me through the tradeoffs you would consider when deciding whether or not to add an index to a table.", + }, + { + id: 7, + text: "Describe the software development lifecycle. Walk me through each phase from requirements gathering through deployment and maintenance, and explain where testing fits into each stage.", + }, + { + id: 8, + text: "Explain the difference between a stack and a queue as data structures. Describe how each one works, give a real-world example of where each is used, and explain how you would implement them.", + }, + { + id: 9, + text: "Walk me through your approach to debugging a production issue where users are reporting that the application is running slowly. What steps would you take, what tools would you use, and how would you communicate along the way?", + }, + { + id: 10, + text: "Explain the concept of separation of concerns in software development. Describe what it means, why it matters, and walk me through what a codebase looks like when this principle is ignored versus when it is applied well.", + }, + // Soft skill / behavioral questions + { + id: 11, + text: "Tell me about a time you faced a significant technical challenge or made a mistake on a project. What happened, how did you handle it, and what did you learn from the experience?", + }, + { + id: 12, + text: "Describe a situation where you had to work closely with someone whose working style was very different from your own. How did you adapt, and what was the outcome?", + }, + { + id: 13, + text: "Tell me about a project you are particularly proud of. What was your contribution, what obstacles did you overcome, and why does it stand out to you?", + }, + { + id: 14, + text: "Describe a time when you had to learn a new technology or skill quickly under pressure. How did you approach it, what resources did you use, and what was the result?", + }, + { + id: 15, + text: "Tell me about a time you disagreed with a technical decision made by a teammate or manager. How did you handle the disagreement, and what happened in the end?", + }, + { + id: 16, + text: "How do you prioritize your work when you have multiple deadlines competing for your attention? Walk me through your process and give me a real example of a time you had to make tough prioritization decisions.", + }, + { + id: 17, + text: "Tell me about a time you received critical feedback on your work. How did you respond to it, and what did you do differently as a result?", + }, + { + id: 18, + text: "Describe a situation where requirements changed significantly partway through a project you were working on. How did you handle the change, and what did you learn from that experience?", + }, + { + id: 19, + text: "Tell me about a time you had to explain a complex technical concept to someone without a technical background. How did you approach it, and how did it go?", + }, + { + id: 20, + text: "Where do you see yourself professionally in three to five years, and what specific steps are you taking right now to get there?", + }, +]; + +export function pickRandomQuestion(usedIds: Set): InterviewQuestion { + const available = QUESTIONS.filter((q) => !usedIds.has(q.id)); + const pool = available.length > 0 ? available : QUESTIONS; + if (available.length === 0) usedIds.clear(); + const q = pool[Math.floor(Math.random() * pool.length)]; + usedIds.add(q.id); + return q; +}