docs: add interview prep Vue SPA design spec
This commit is contained in:
parent
b9ef1f631e
commit
0e1dd29938
1 changed files with 184 additions and 0 deletions
184
docs/superpowers/specs/2026-03-20-interview-prep-vue-design.md
Normal file
184
docs/superpowers/specs/2026-03-20-interview-prep-vue-design.md
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
# Interview Prep Page — Vue SPA Design
|
||||
|
||||
## Goal
|
||||
|
||||
Port the Streamlit Interview Prep page (`app/pages/6_Interview_Prep.py`) to a Vue 3 SPA view at `/prep/:id`, with a two-column layout, research brief generation, reference tabs, and localStorage call notes.
|
||||
|
||||
## Scope
|
||||
|
||||
**In scope:**
|
||||
- Job header with stage badge + interview date countdown
|
||||
- Research brief display with generate / refresh / polling
|
||||
- All research sections: Talking Points, Company Overview, Leadership & Culture, Tech Stack (conditional), Funding (conditional), Red Flags (conditional), Inclusion & Accessibility (conditional)
|
||||
- Reference panel: Job Description tab, Email History tab, Cover Letter tab
|
||||
- Call Notes (localStorage, per job)
|
||||
- Navigation: `/prep` redirects to `/interviews`; Interviews kanban adds "Prep →" link on active-stage cards
|
||||
|
||||
**Explicitly deferred:**
|
||||
- Practice Q&A (LLM mock interviewer chat — needs streaming chat endpoint, deferred to a future sprint)
|
||||
- "Draft reply to last email" LLM button in Email tab (present in Streamlit, requires additional LLM endpoint, deferred to a future sprint)
|
||||
- Layout B / C options (full-width tabbed, accordion) — architecture supports future layout preference stored in localStorage
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Routing
|
||||
|
||||
- `/prep/:id` — renders `InterviewPrepView.vue` with the specified job
|
||||
- `/prep` (no id) — redirects to `/interviews`
|
||||
- On mount: if job id is missing, or job is not in `phone_screen` / `interviewing` / `offer`, redirect to `/interviews`
|
||||
- Router already has both routes defined (`/prep` and `/prep/:id`)
|
||||
|
||||
### Backend — `dev-api.py` (4 new endpoints)
|
||||
|
||||
| Method | Path | Purpose |
|
||||
|--------|------|---------|
|
||||
| `GET` | `/api/jobs/{id}/research` | Returns `company_research` row for job, or 404 if none |
|
||||
| `POST` | `/api/jobs/{id}/research/generate` | Submits `company_research` background task via `submit_task()`; returns `{task_id, is_new}` |
|
||||
| `GET` | `/api/jobs/{id}/research/task` | Latest task status from `background_tasks`: `{status, stage, message}` — matches `cover_letter_task` response shape (`message` maps the `error` column) |
|
||||
| `GET` | `/api/jobs/{id}/contacts` | Returns all `job_contacts` rows for this job, ordered by `received_at` desc |
|
||||
|
||||
Reuses existing patterns: `submit_task()` (same as cover letter), `background_tasks` query (same as `cover_letter_task`), `get_contacts()` (same as Streamlit). No schema changes.
|
||||
|
||||
### Store — `web/src/stores/prep.ts`
|
||||
|
||||
```ts
|
||||
interface ResearchBrief {
|
||||
company_brief: string | null
|
||||
ceo_brief: string | null
|
||||
talking_points: string | null
|
||||
tech_brief: string | null // confirmed present in company_research (used in Streamlit 6_Interview_Prep.py:178)
|
||||
funding_brief: string | null // confirmed present in company_research (used in Streamlit 6_Interview_Prep.py:185)
|
||||
red_flags: string | null
|
||||
accessibility_brief: string | null
|
||||
generated_at: string | null
|
||||
// raw_output is returned by the API but not used in the UI — intentionally omitted from interface
|
||||
}
|
||||
|
||||
interface Contact {
|
||||
id: number
|
||||
direction: 'inbound' | 'outbound'
|
||||
subject: string | null
|
||||
from_addr: string | null
|
||||
body: string | null
|
||||
received_at: string | null
|
||||
}
|
||||
|
||||
interface TaskStatus {
|
||||
status: 'queued' | 'running' | 'completed' | 'failed' | 'none' | null
|
||||
stage: string | null
|
||||
message: string | null // maps the background_tasks.error column; matches cover_letter_task shape
|
||||
}
|
||||
```
|
||||
|
||||
State: `research: ResearchBrief | null`, `contacts: Contact[]`, `taskStatus: TaskStatus`, `loading: boolean`, `error: string | null`, `currentJobId: number | null`.
|
||||
|
||||
Methods:
|
||||
- `fetchFor(jobId)` — clears state if `jobId !== currentJobId`, fires three parallel requests: `GET /research`, `GET /contacts`, `GET /research/task`. Stores results. If task status from the task fetch is `queued` or `running`, calls `pollTask(jobId)` to start the polling interval.
|
||||
- `generateResearch(jobId)` — `POST /research/generate`, then calls `pollTask(jobId)`
|
||||
- `pollTask(jobId)` — `setInterval` at 3s; stops when status is `completed` or `failed`; on `completed` re-calls `fetchFor(jobId)` to pull in fresh research
|
||||
- `clear()` — cancels any active poll interval, resets all state
|
||||
|
||||
### Component — `web/src/views/InterviewPrepView.vue`
|
||||
|
||||
**Mount / unmount:**
|
||||
- Reads `route.params.id`; redirects to `/interviews` if missing
|
||||
- Looks up job in `interviewsStore.jobs`; redirects to `/interviews` if job status not in active stages
|
||||
- Calls `prepStore.fetchFor(jobId)` on mount
|
||||
- Calls `prepStore.clear()` on unmount (`onUnmounted`)
|
||||
|
||||
**Layout (desktop ≥1024px): two-column**
|
||||
|
||||
Left column (40%):
|
||||
1. Job header
|
||||
- Company + title (`h1`)
|
||||
- Stage badge (pill)
|
||||
- Interview date + countdown (🔴 TODAY / 🟡 TOMORROW / 🟢 in N days / grey "was N days ago")
|
||||
- "Open job listing ↗" link button (if `job.url`)
|
||||
2. Research controls
|
||||
- State: `no research + no task` → "Generate research brief" primary button
|
||||
- State: `task queued/running` → spinner + stage label (e.g. "Scraping company site…"), polling active
|
||||
- State: `research loaded` → "Generated: {timestamp}" caption + "Refresh" button (disabled while task running)
|
||||
- State: `task failed` → inline error + "Retry" button
|
||||
3. Research sections (render only if non-empty string):
|
||||
- 🎯 Talking Points
|
||||
- 🏢 Company Overview
|
||||
- 👤 Leadership & Culture
|
||||
- ⚙️ Tech Stack & Product *(conditional)*
|
||||
- 💰 Funding & Market Position *(conditional)*
|
||||
- ⚠️ Red Flags & Watch-outs *(conditional; styled as warning block; skip if text contains "no significant red flags")*
|
||||
- ♿ Inclusion & Accessibility *(conditional; privacy caption: "For your personal evaluation — not disclosed in any application.")*
|
||||
|
||||
Right column (60%):
|
||||
1. Tabs: Job Description | Email History | Cover Letter
|
||||
- **JD tab**: match score badge (🟢 ≥70% / 🟡 ≥40% / 🔴 <40%), keyword gaps, description rendered as markdown
|
||||
- **Email tab**: list of contacts — icon (📥/📤) + subject + date + from_addr + first 500 chars of body; empty state if none
|
||||
- **Letter tab**: cover letter markdown; empty state if none
|
||||
2. Call Notes
|
||||
- Textarea below tabs
|
||||
- `v-model` bound to computed getter/setter reading `localStorage.getItem('cf-prep-notes-{jobId}')`
|
||||
- Auto-saved via debounced `watch` (300ms)
|
||||
- Caption: "Notes are saved locally — they won't sync between devices."
|
||||
- **Intentional upgrade from Streamlit**: Streamlit stored notes in `session_state` only (lost on navigation). localStorage persists across page refreshes and sessions.
|
||||
|
||||
**Mobile (≤1023px):** single column — left panel content first (scrollable), then tabs panel below.
|
||||
|
||||
### Navigation addition — `InterviewsView.vue`
|
||||
|
||||
Add a "Prep →" `RouterLink` to `/prep/:id` on each job card in `phone_screen`, `interviewing`, and `offer` columns. Not shown in `applied`, `survey`, `hired`, or `interview_rejected`.
|
||||
|
||||
---
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
User navigates to /prep/:id
|
||||
→ InterviewPrepView mounts
|
||||
→ redirect check (job in active stage?)
|
||||
→ prepStore.fetchFor(id)
|
||||
├─ GET /api/jobs/{id}/research (parallel)
|
||||
├─ GET /api/jobs/{id}/contacts (parallel)
|
||||
└─ GET /api/jobs/{id}/research/task (parallel — to check if a task is already running)
|
||||
→ if task running: pollTask(id) starts interval
|
||||
→ user clicks "Generate" / "Refresh"
|
||||
→ POST /api/jobs/{id}/research/generate
|
||||
→ pollTask(id) starts
|
||||
→ GET /api/jobs/{id}/research/task every 3s
|
||||
→ on completed: fetchFor(id) re-fetches research
|
||||
User navigates away
|
||||
→ prepStore.clear() cancels interval
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Research fetch 404 → `research` stays null, show generate button
|
||||
- Research fetch network/5xx → show inline error in left column
|
||||
- Contacts fetch error → show "Could not load email history" in Email tab
|
||||
- Generate task failure → `taskStatus.message` shown with "Retry" button
|
||||
- Job not found / wrong stage → redirect to `/interviews` (no error flash)
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
New test files:
|
||||
- `tests/test_dev_api_prep.py` — covers all 4 endpoints: research GET (found/not-found), generate (new/duplicate), task status, contacts GET
|
||||
- `web/src/stores/prep.test.ts` — unit tests for `fetchFor`, `generateResearch`, `pollTask` (mock `useApiFetch`), `clear` cancels interval
|
||||
|
||||
No new DB migrations. All DB access uses existing `scripts/db.py` helpers.
|
||||
|
||||
---
|
||||
|
||||
## Files Changed
|
||||
|
||||
| Action | Path |
|
||||
|--------|------|
|
||||
| Modify | `dev-api.py` — 4 new endpoints |
|
||||
| Create | `tests/test_dev_api_prep.py` |
|
||||
| Create | `web/src/stores/prep.ts` |
|
||||
| Modify | `web/src/views/InterviewPrepView.vue` — full implementation |
|
||||
| Modify | `web/src/views/InterviewsView.vue` — add "Prep →" links |
|
||||
| Create | `web/src/stores/prep.test.ts` |
|
||||
Loading…
Reference in a new issue