diff --git a/API-Surface.md b/API-Surface.md index 3b76d53..f09f61c 100644 --- a/API-Surface.md +++ b/API-Surface.md @@ -1,6 +1,6 @@ # API Surface -50 API endpoints grouped by domain. All served by FastAPI under `/api/v1/`. +61 API endpoints grouped by domain. All served by FastAPI under `/api/v1/`. ## Public Endpoints (10) @@ -26,11 +26,19 @@ title, slug, topic_category, topic_tags, summary, body_sections, body_sections_f | Method | Path | Response Shape | Notes | |--------|------|---------------|-------| | GET | `/api/v1/creators?sort=&genre=` | `{items, total, offset, limit}` | sort: random\|alpha\|views | -| GET | `/api/v1/creators/{slug}` | 16-field object | Includes genre_breakdown, techniques, social_links | +| GET | `/api/v1/creators/{slug}` | 16-field object | Includes genre_breakdown, techniques, social_links, follower_count, personality_profile | | GET | `/api/v1/topics` | `[{name, description, sub_topics}]` | ⚠️ Bare list (not paginated) | | GET | `/api/v1/topics/{cat}/{sub}` | `{items, total, offset, limit}` | Subtopic techniques | | GET | `/api/v1/topics/{cat}` | `{items, total, offset, limit}` | Category techniques | +## Chat Endpoint (1) + +| Method | Path | Auth | Purpose | +|--------|------|------|---------| +| POST | `/api/v1/chat` | None | Streaming Q&A — SSE response with sources, tokens, done event. See [[Chat-Engine]] | + +**Request fields:** `query` (required, 1-1000 chars), `creator` (optional slug/UUID), `conversation_id` (optional UUID for multi-turn threading) + ## Auth Endpoints (4) All under prefix `/api/v1/auth/`. JWT-protected except registration and login. @@ -42,6 +50,28 @@ All under prefix `/api/v1/auth/`. JWT-protected except registration and login. | GET | `/auth/me` | Bearer JWT | Current user profile. Returns UserResponse. | | PUT | `/auth/me` | Bearer JWT | Update display_name and/or password (requires current_password for password changes). Returns UserResponse. | +## Follow Endpoints (4) — M022/S02 + +All require Bearer JWT. + +| Method | Path | Purpose | +|--------|------|---------| +| POST | `/api/v1/follows/{creator_id}` | Follow a creator (idempotent via INSERT ON CONFLICT DO NOTHING) | +| DELETE | `/api/v1/follows/{creator_id}` | Unfollow a creator | +| GET | `/api/v1/follows/{creator_id}/status` | Check if current user follows this creator | +| GET | `/api/v1/follows/me` | List creators the current user follows | + +## Creator Highlight Endpoints (4) — M022/S01 + +Creator-scoped highlight review. Requires Bearer JWT with creator ownership. + +| Method | Path | Purpose | +|--------|------|---------| +| GET | `/api/v1/creator/highlights` | List highlights for authenticated creator (status/shorts_only filters) | +| GET | `/api/v1/creator/highlights/{id}` | Detail with score_breakdown and key_moment | +| PATCH | `/api/v1/creator/highlights/{id}/status` | Update status (approve/reject) | +| PATCH | `/api/v1/creator/highlights/{id}/trim` | Update trim_start/trim_end | + ## Consent Endpoints (5) All under prefix `/api/v1/consent/`. All require Bearer JWT. @@ -54,16 +84,6 @@ All under prefix `/api/v1/consent/`. All require Bearer JWT. | GET | `/consent/videos/{video_id}/history` | Creator (owner) or Admin | Versioned audit trail of consent changes for a video. | | GET | `/consent/admin/summary` | Admin only | Aggregate consent flag counts across all videos. | -### Consent Fields - -Three boolean consent flags per video, each independently toggleable: - -| Field | Default | Meaning | -|-------|---------|---------| -| `kb_inclusion` | false | Allow indexing into knowledge base | -| `training_usage` | false | Allow use for model training | -| `public_display` | true | Allow public display on site | - ## Report Endpoints (3) | Method | Path | Purpose | @@ -72,7 +92,9 @@ Three boolean consent flags per video, each independently toggleable: | GET | `/api/v1/admin/reports` | List all reports | | PATCH | `/api/v1/admin/reports/{id}` | Update report status | -## Pipeline Admin Endpoints (20+) +## Admin Endpoints + +### Pipeline Admin (20+) All under prefix `/api/v1/admin/pipeline/`. @@ -100,52 +122,20 @@ All under prefix `/api/v1/admin/pipeline/`. | POST | `/admin/pipeline/creator-profile/{creator_id}` | Update creator profile | | POST | `/admin/pipeline/avatar-fetch/{creator_id}` | Fetch creator avatar | -## Other Endpoints (2) +### Highlight Admin (4) -| Method | Path | Notes | -|--------|------|-------| -| POST | `/api/v1/ingest` | Transcript upload | -| GET | `/api/v1/videos` | ⚠️ Bare list (not paginated) | +| Method | Path | Purpose | +|--------|------|---------| +| POST | `/admin/highlights/detect/{video_id}` | Score all KeyMoments for a video | +| POST | `/admin/highlights/detect-all` | Score all videos | +| GET | `/admin/highlights/candidates` | Paginated candidate list | +| GET | `/admin/highlights/candidates/{id}` | Single candidate with score_breakdown | -## Response Conventions +### Personality Extraction (1) — M022/S06 -**Standard paginated response:** -```json -{ - "items": [...], - "total": 83, - "offset": 0, - "limit": 20 -} -``` - -**Known inconsistencies:** -- `GET /topics` returns bare list instead of paginated dict -- `GET /videos` returns bare list instead of paginated dict -- Search uses `items` key (not `results`) -- `/techniques/random` returns JSON `{slug}` (not HTTP redirect) - -**New endpoints should follow the `{items, total, offset, limit}` paginated pattern.** - -## Authentication - -JWT-based authentication added in M019. See [[Authentication]] for full details. - -- **Public endpoints** (search, browse, techniques) require no auth -- **Auth endpoints** (`/auth/register`, `/auth/login`) are open; `/auth/me` requires Bearer JWT -- **Consent endpoints** require Bearer JWT with ownership verification (creator must own the video, or be admin) -- **Admin endpoints** (`/admin/*`) are accessible to anyone with network access (auth planned for future milestone) - ---- - -*See also: [[Architecture]], [[Data-Model]], [[Frontend]], [[Authentication]]* -utput` | Delete all pipeline output | -| POST | `/admin/pipeline/optimize-prompt` | Trigger prompt optimization | -| POST | `/admin/pipeline/reindex-all` | Rebuild Qdrant index | -| GET | `/admin/pipeline/worker-status` | Celery worker health | -| GET | `/admin/pipeline/recent-activity` | Recent pipeline events | -| POST | `/admin/pipeline/creator-profile/{creator_id}` | Update creator profile | -| POST | `/admin/pipeline/avatar-fetch/{creator_id}` | Fetch creator avatar | +| Method | Path | Purpose | +|--------|------|---------| +| POST | `/api/v1/admin/creators/{slug}/extract-profile` | Queue personality profile extraction task | ## Other Endpoints (2) @@ -178,9 +168,11 @@ utput` | Delete all pipeline output | JWT-based authentication added in M019. See [[Authentication]] for full details. -- **Public endpoints** (search, browse, techniques) require no auth +- **Public endpoints** (search, browse, techniques, chat) require no auth - **Auth endpoints** (`/auth/register`, `/auth/login`) are open; `/auth/me` requires Bearer JWT -- **Consent endpoints** require Bearer JWT with ownership verification (creator must own the video, or be admin) +- **Follow endpoints** require Bearer JWT +- **Creator endpoints** (`/creator/*`) require Bearer JWT with creator ownership verification +- **Consent endpoints** require Bearer JWT with ownership verification - **Admin endpoints** (`/admin/*`) are accessible to anyone with network access (auth planned for future milestone) --- diff --git a/Chat-Engine.md b/Chat-Engine.md index 2d9a85e..8353f9a 100644 --- a/Chat-Engine.md +++ b/Chat-Engine.md @@ -1,29 +1,33 @@ # Chat Engine -Streaming question-answering interface backed by LightRAG retrieval and LLM completion. Added in M021/S03. +Streaming question-answering interface backed by LightRAG retrieval and LLM completion. Added in M021/S03, expanded with multi-turn memory in M022/S04 and chat widget in M022/S03. ## Architecture ``` -User types question in ChatPage +User types question in ChatPage or ChatWidget │ ▼ -POST /api/v1/chat { query: "...", creator?: "..." } +POST /api/v1/chat { query, creator?, conversation_id? } │ ▼ -ChatService.stream(query, creator?) +ChatService.stream(query, creator?, conversation_id?) │ - ├─ 1. Retrieve: SearchService.search(query, creator) + ├─ 1. Load history: Redis chrysopedia:chat:{conversation_id} + │ + ├─ 2. Retrieve: SearchService.search(query, creator) │ └─ Uses 4-tier cascade if creator provided (see [[Search-Retrieval]]) │ - ├─ 2. Prompt: Assemble numbered context block into encyclopedic system prompt + ├─ 3. Prompt: System prompt + history + numbered context + user message │ └─ Sources formatted as [1] Title — Summary for citation mapping │ - ├─ 3. Stream: openai.AsyncOpenAI with stream=True + ├─ 4. Stream: openai.AsyncOpenAI with stream=True │ └─ Tokens streamed as SSE events in real-time │ + ├─ 5. Save history: Append user message + assistant response to Redis + │ ▼ -SSE response → ChatPage renders tokens + citation links +SSE response → ChatPage/ChatWidget renders tokens + citation links ``` ## SSE Protocol @@ -34,10 +38,28 @@ The chat endpoint returns a `text/event-stream` response with four event types i |-------|---------|------| | `sources` | `[{title, slug, creator_name, summary}]` | First — citation metadata for link rendering | | `token` | `string` (text chunk) | Repeated — streamed LLM completion tokens | -| `done` | `{cascade_tier: "creator"\|"domain"\|"global"\|"none"\|""}` | Once — signals completion, includes which retrieval tier answered | +| `done` | `{cascade_tier, conversation_id}` | Once — signals completion, includes retrieval tier and conversation ID | | `error` | `{message: string}` | On failure — emitted if LLM errors mid-stream | -The `cascade_tier` in the `done` event reveals which tier of the retrieval cascade served the context (see [[Search-Retrieval]]). +The `cascade_tier` in the `done` event reveals which tier of the retrieval cascade served the context. The `conversation_id` enables the frontend to thread follow-up messages. + +## Multi-Turn Conversation Memory (M022/S04) + +### Redis Storage + +- **Key pattern:** `chrysopedia:chat:{conversation_id}` +- **Format:** Single JSON string containing a list of `{role, content}` message dicts +- **TTL:** 1 hour, refreshed on each interaction +- **Cap:** 10 turn pairs (20 messages) — oldest pairs trimmed when exceeded + +### Conversation Flow + +1. Client sends `conversation_id` in POST body (or omits for new conversation) +2. Server auto-generates UUID when `conversation_id` is omitted +3. History loaded from Redis and injected between system prompt and user message +4. Assistant response accumulated during streaming +5. User message + assistant response appended to history in Redis +6. `conversation_id` returned in SSE `done` event for threading ## Citation Format @@ -45,7 +67,7 @@ The LLM is instructed to reference sources using numbered citations `[N]` in its - `[1]` → links to `/techniques/:slug` for the corresponding source - Multiple citations supported: `[1][3]` or `[1,3]` -- Citation regex: `/\[(\d+)\]/g` parsed locally in ChatPage +- Citation regex: `/\[(\d+)\]/g` parsed locally in both ChatPage and ChatWidget ## API Endpoint @@ -55,6 +77,7 @@ The LLM is instructed to reference sources using numbered citations `[N]` in its |-------|------|----------|------------| | `query` | string | Yes | 1–1000 characters | | `creator` | string | No | Creator UUID or slug for scoped retrieval | +| `conversation_id` | string | No | UUID for multi-turn threading. Auto-generated if omitted. | **Response:** `text/event-stream` (SSE) @@ -65,9 +88,11 @@ The LLM is instructed to reference sources using numbered citations `[N]` in its Located in `backend/chat_service.py`. The retrieve-prompt-stream pipeline: -1. **Retrieve** — Calls `SearchService.search()` with the query and optional creator parameter. Gets back ranked technique page results with the cascade_tier. -2. **Prompt** — Builds a numbered context block from search results. System prompt instructs the LLM to act as a music production encyclopedia, cite sources with `[N]` notation, and stay grounded in the provided context. -3. **Stream** — Opens an async streaming completion via `openai.AsyncOpenAI` (configured to point at DGX Sparks Qwen or local Ollama). Yields SSE events as tokens arrive. +1. **Load History** — `_load_history()` reads from Redis key `chrysopedia:chat:{conversation_id}`. Returns empty list if key absent. +2. **Retrieve** — Calls `SearchService.search()` with the query and optional creator parameter. Gets back ranked technique page results with the cascade_tier. +3. **Prompt** — Builds message array: system prompt → conversation history → numbered context block → user message. System prompt instructs the LLM to act as a music production encyclopedia, cite sources with `[N]` notation, and stay grounded in the provided context. +4. **Stream** — Opens an async streaming completion via `openai.AsyncOpenAI`. Yields SSE events as tokens arrive. +5. **Save History** — `_save_history()` appends the user message and accumulated assistant response to Redis. Trims to 10 turn pairs if exceeded. Refreshes TTL to 1 hour. Error handling: If the LLM fails mid-stream (after some tokens have been sent), an `error` event is emitted so the frontend can display a failure message rather than leaving the response hanging. @@ -77,38 +102,63 @@ Route: `/chat` (lazy-loaded, code-split) ### Components -- **Text input + submit button** — Query entry with Enter-to-submit +- **Multi-message conversation UI** — Messages array with conversation bubble layout +- **Conversation threading** — `conversationId` state, "New conversation" button to reset - **Streaming message display** — Accumulates tokens with blinking cursor animation during streaming -- **Citation markers** — `[N]` parsed to superscript links targeting `/techniques/:slug` -- **Source list** — Numbered sources with creator attribution displayed below the response -- **States:** Loading (streaming indicator), error (message display), empty (placeholder prompt) +- **Typing indicator** — Three-dot animation while streaming +- **Citation markers** — `[N]` parsed to superscript links targeting `/techniques/:slug` (per-message) +- **Source list** — Numbered sources with creator attribution displayed below each response +- **Auto-scroll** — Scrolls to bottom as new tokens arrive ### SSE Client Located in `frontend/src/api/chat.ts`. Uses `fetch()` + `ReadableStream` with typed callbacks: ```typescript -streamChat(query, creator?, { +streamChat(query, { onSources: (sources) => void, onToken: (token) => void, - onDone: (data) => void, + onDone: (data: ChatDoneMeta) => void, onError: (error) => void, -}) +}, creatorName?, conversationId?) ``` +`ChatDoneMeta` type includes `cascade_tier` and `conversation_id` fields. + +## Frontend: ChatWidget (M022/S03) + +Floating chat bubble on creator detail pages. Fixed-position bottom-right. + +### Behavior + +- **Bubble** → click → **slide-up panel** with conversation UI +- Creator-scoped: passes `creatorName` to `streamChat()` for retrieval cascade +- **Suggested questions** generated client-side from technique titles and categories +- **Typing indicator** — three-dot animation during streaming +- **Citation links** — parsed from response, linked to technique pages +- **Responsive** — full-width below 640px, 400px panel on desktop +- **Conversation threading** — `conversationId` generated via `crypto.randomUUID()` on first send, threaded through `streamChat()`, updated from done event +- **Reset on close** — messages and conversationId cleared when panel closes + ## Key Files -- `backend/chat_service.py` — ChatService retrieve-prompt-stream pipeline -- `backend/routers/chat.py` — POST /api/v1/chat endpoint -- `frontend/src/api/chat.ts` — SSE client utility -- `frontend/src/pages/ChatPage.tsx` — Chat UI page component -- `frontend/src/pages/ChatPage.module.css` — Chat page styles +- `backend/chat_service.py` — ChatService with history load/save, retrieve-prompt-stream pipeline +- `backend/routers/chat.py` — POST /api/v1/chat endpoint with conversation_id support +- `backend/tests/test_chat.py` — 13 tests (6 streaming + 7 conversation memory) +- `frontend/src/api/chat.ts` — SSE client with conversationId param and ChatDoneMeta type +- `frontend/src/pages/ChatPage.tsx` — Multi-message conversation UI +- `frontend/src/pages/ChatPage.module.css` — Conversation bubble layout styles +- `frontend/src/components/ChatWidget.tsx` — Floating chat widget component +- `frontend/src/components/ChatWidget.module.css` — Widget styles (38 custom property refs) ## Design Decisions -- **Standalone ASGI test client pattern** — Tests use mocked DB to avoid PostgreSQL dependency, enabling fast CI runs -- **Patch `openai.AsyncOpenAI` constructor** rather than instance attribute for reliable test mocking -- **Local citation regex** in ChatPage rather than importing from utils — link targets differ from technique page citations +- **Redis JSON string** — Conversation history stored as single JSON value (atomic read/write) rather than Redis list type +- **Auto-generate conversation_id** — Server creates UUID when client omits it, ensuring consistent `done` event shape +- **Widget resets on close** — Clean slate UX; no persistence across open/close cycles +- **Client-side suggested questions** — Generated from technique titles/categories without API call +- **Citation parsing duplicated** — ChatPage and ChatWidget each parse citations independently (extracted utility deferred) +- **Standalone ASGI test client** — Tests use mocked DB to avoid PostgreSQL dependency --- diff --git a/Data-Model.md b/Data-Model.md index fdfb15d..05b321a 100644 --- a/Data-Model.md +++ b/Data-Model.md @@ -1,6 +1,6 @@ # Data Model -18 SQLAlchemy models in `backend/models.py`. +20 SQLAlchemy models in `backend/models.py`. ## Entity Relationship Overview @@ -17,6 +17,8 @@ Creator (1) ──→ (N) SourceVideo (1) ──→ (N) TranscriptSegment │ ├──→ (N) RelatedTechniqueLink │ └──→ (M:N) SourceVideo (via TechniquePageVideo) │ + ├──→ (N) CreatorFollow ←── User + │ └──→ (0..1) User ──→ (N) InviteCode (created_by) ``` @@ -34,6 +36,7 @@ Creator (1) ──→ (N) SourceVideo (1) ──→ (N) TranscriptSegment | bio | Text | Admin-editable | | social_links | JSONB | Platform → URL mapping | | featured | Boolean | For homepage spotlight | +| personality_profile | JSONB | LLM-extracted personality data (M022/S06). See [[Personality-Profiles]] | ### SourceVideo @@ -101,6 +104,33 @@ Creator (1) ──→ (N) SourceVideo (1) ──→ (N) TranscriptSegment | content_snapshot | JSONB | Full page state at version time | | pipeline_metadata | JSONB | Prompt SHA-256 hashes, model config | +### HighlightCandidate + +| Field | Type | Notes | +|-------|------|-------| +| id | UUID PK | | +| key_moment_id | FK → KeyMoment | Unique constraint | +| source_video_id | FK → SourceVideo | Indexed | +| score | Float | Composite score 0.0–1.0 | +| score_breakdown | JSONB | Per-dimension scores (10 fields, see [[Highlights]]) | +| duration_secs | Float | Cached from KeyMoment | +| status | Enum(HighlightStatus) | candidate / approved / rejected | +| trim_start | Float | Nullable — trim offset in seconds (M022/S01) | +| trim_end | Float | Nullable — trim offset in seconds (M022/S01) | +| created_at | Timestamp | | +| updated_at | Timestamp | | + +### CreatorFollow (M022/S02) + +| Field | Type | Notes | +|-------|------|-------| +| id | UUID PK | | +| user_id | FK → User | Part of unique constraint | +| creator_id | FK → Creator | Part of unique constraint | +| created_at | Timestamp | | + +Unique constraint on `(user_id, creator_id)`. Idempotent follow via `INSERT ON CONFLICT DO NOTHING`. + ## Authentication & User Models ### User @@ -192,20 +222,17 @@ Append-only versioned record of per-field consent changes. | **HighlightStatus** | candidate, approved, rejected (M021/S04) | | **ChapterStatus** | draft, approved, hidden (M021/S06) | +## Migrations + +| Migration | Description | +|-----------|-------------| +| 019 | Add highlight_candidates table | +| 021 | Add trim_start/trim_end to highlight_candidates (M022/S01) | +| 022 | Add creator_follows table (M022/S02) | +| 023 | Add personality_profile JSONB to creators (M022/S06) | + ## Schema Notes -- **No Alembic migrations** — schema changes currently require manual DDL -- **body_sections_format** discriminator enables v1/v2 format coexistence (D024) -- **topic_category casing** is inconsistent across records (e.g., "Sound design" vs "Sound Design") — known data quality issue -- **Stage 4 classification data** (per-moment topic_tags) stored in Redis with 24h TTL, not DB columns -- **Timestamp convention:** `datetime.now(timezone.utc).replace(tzinfo=None)` — asyncpg rejects timezone-aware datetimes for TIMESTAMP WITHOUT TIME ZONE columns (D002) -- **User passwords** are stored as bcrypt hashes via `bcrypt.hashpw()` -- **Consent audit** uses version numbers assigned in application code (`max(version) + 1` per video_consent_id) - ---- - -*See also: [[Architecture]], [[API-Surface]], [[Pipeline]], [[Authentication]]* - changes currently require manual DDL - **body_sections_format** discriminator enables v1/v2 format coexistence (D024) - **topic_category casing** is inconsistent across records (e.g., "Sound design" vs "Sound Design") — known data quality issue - **Stage 4 classification data** (per-moment topic_tags) stored in Redis with 24h TTL, not DB columns diff --git a/Decisions.md b/Decisions.md index 4357bec..e99a1f4 100644 --- a/Decisions.md +++ b/Decisions.md @@ -31,12 +31,26 @@ Architectural and pattern decisions made during Chrysopedia development. Append- | D034 | Documentation strategy | Forgejo wiki, KB slice at end of every milestone | Incremental docs stay current; final pass in M025 | | D035 | File/object storage | MinIO (S3-compatible) self-hosted | Docker-native, signed URLs, fits existing infrastructure | -## M021 Decisions +## Authentication & Infrastructure Decisions | # | When | Decision | Choice | Rationale | |---|------|----------|--------|-----------| -| D039 | M021/S01 | LightRAG scoring strategy | Position-based (1.0 → 0.5 descending), sequential Qdrant fallback | `/query/data` has no numeric relevance score; retrieval order is the only signal | -| D040 | M021/S02 | Creator-scoped retrieval strategy | 4-tier cascade: creator → domain → global → none | Progressive widening ensures results while preferring creator context; `ll_keywords` for soft scoping; 3x oversampling for post-filter survival | +| D036 | M019/S02 | JWT auth configuration | HS256 with existing app_secret_key, 24h expiry, OAuth2PasswordBearer | Reuses existing secret; integrates with FastAPI dependency injection | +| D037 | — | Search impressions query | Exact case-insensitive title match via EXISTS subquery against SearchLog | MVP approach; expandable to ILIKE later | +| D038 | — | Primary git remote | git.xpltd.co (Forgejo) instead of github.com | Consolidating on self-hosted Forgejo; wiki already there | + +## Search & Retrieval Decisions + +| # | When | Decision | Choice | Rationale | +|---|------|----------|--------|-----------| +| D039 | M021/S01 | LightRAG scoring strategy | Position-based (1.0 → 0.5 descending), sequential Qdrant fallback | `/query/data` has no numeric relevance score | +| D040 | M021/S02 | Creator-scoped retrieval | 4-tier cascade: creator → domain → global → none | Progressive widening; `ll_keywords` for soft scoping; 3x oversampling for post-filter survival | + +## M022 Decisions + +| # | When | Decision | Choice | Rationale | +|---|------|----------|--------|-----------| +| D041 | M022/S05 | Highlight scorer weight distribution | 10 dimensions: original 7 reduced proportionally, 3 audio proxy dims get 0.22 total weight. Neutral fallback (0.5) when word_timings unavailable. | Audio proxy signals from word-level timing data; neutral fallback preserves backward compatibility | ## UI/UX Decisions diff --git a/Frontend.md b/Frontend.md index 77ef29b..9f6eec6 100644 --- a/Frontend.md +++ b/Frontend.md @@ -10,10 +10,13 @@ React 18 + TypeScript + Vite SPA. No UI library, no state management library, no | `/search` | SearchResults | Public | Sort, highlights, partial matches | | `/techniques/:slug` | TechniquePage | Public | v2 body sections, ToC sidebar, citations | | `/creators` | CreatorsBrowse | Public | Random default sort, genre filters | -| `/creators/:slug` | CreatorDetail | Public | Avatar, stats, technique list | +| `/creators/:slug` | CreatorDetail | Public | Avatar, stats, technique list, follow button, personality profile, chat widget | | `/topics` | TopicsBrowse | Public | 7 category cards, expandable sub-topics | | `/topics/:category/:subtopic` | SubTopicPage | Public | Creator-grouped techniques | +| `/chat` | ChatPage | Public | Multi-message conversation UI with threading | | `/about` | About | Public | Static project info | +| `/creator/highlights` | HighlightQueue | Creator JWT | Highlight review queue with filter tabs (M022/S01) | +| `/creator/tiers` | CreatorTiers | Creator JWT | Free/Pro/Premium tier cards with Coming Soon modals (M022/S02) | | `/admin/reports` | AdminReports | Admin* | Content reports | | `/admin/pipeline` | AdminPipeline | Admin* | Pipeline management | | `/admin/techniques` | AdminTechniquePages | Admin* | Technique page admin | @@ -38,6 +41,51 @@ React 18 + TypeScript + Vite SPA. No UI library, no state management library, no | CopyLinkButton | Clipboard copy with tooltip | | SocialIcons | Social media link icons (9 platforms) | | ReportIssueModal | Content report submission | +| ChatWidget | Floating chat bubble on creator pages — SSE streaming, citations, suggested questions (M022/S03) | +| PersonalityProfile | Collapsible creator personality display — 3 sub-cards (Teaching Style, Vocabulary, Style) (M022/S06) | + +## Feature Pages (M022) + +### HighlightQueue (M022/S01) + +Creator-scoped highlight review page at `/creator/highlights`. + +- **Filter tabs** — All / Shorts / Approved / Rejected +- **Candidate cards** — Title, duration, composite score, status badge +- **Score breakdown bars** — 10-dimension visual bars (fetched lazily on expand) +- **Action buttons** — Approve / Discard with ownership verification +- **Inline trim panel** — Validated trim_start / trim_end inputs +- **Files:** `HighlightQueue.tsx`, `HighlightQueue.module.css`, `highlights.ts` (API) + +### CreatorTiers (M022/S02) + +Tier configuration at `/creator/tiers`. + +- **Three cards** — Free (active), Pro, Premium +- **Coming Soon modals** — Styled placeholders per D033 (Stripe deferred to Phase 3) +- **Files:** `CreatorTiers.tsx`, `CreatorTiers.module.css` + +### ChatWidget (M022/S03) + +Floating chat on creator detail pages. + +- **Fixed-position bubble** (bottom-right) → slide-up conversation panel +- **Creator-scoped** — passes creatorName to streamChat() for retrieval cascade +- **Suggested questions** — client-side from technique titles/categories +- **Streaming SSE** — tokens, citations, typing indicator +- **Responsive** — full-width below 640px, 400px panel on desktop +- **Conversation threading** — conversationId via crypto.randomUUID(), resets on close +- **Files:** `ChatWidget.tsx`, `ChatWidget.module.css` + +### PersonalityProfile (M022/S06) + +Collapsible personality display on creator detail pages. + +- **Grid-template-rows animation** — 0fr → 1fr for smooth expand/collapse +- **Three sub-cards:** Teaching Style, Vocabulary, Style +- **Pill badges** for phrases/terms, checkmark/cross for boolean markers +- **Gracefully hidden** when profile is null +- **Files:** `PersonalityProfile.tsx`, styles in `App.css` ## Hooks @@ -45,19 +93,22 @@ React 18 + TypeScript + Vite SPA. No UI library, no state management library, no |------|---------| | useCountUp | Animated counter for homepage stats | | useSortPreference | Persists sort preference in localStorage | -| useDocumentTitle | Sets `