diff --git a/.gitignore b/.gitignore index 055d4c6..6075186 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ yarn-error.log* pnpm-debug.log* lerna-debug.log* +.env node_modules dist dist-ssr diff --git a/Ralph/archive/2026-02-15-login-logo-refinements/prd.json b/Ralph/archive/2026-02-15-login-logo-refinements/prd.json new file mode 100644 index 0000000..021817c --- /dev/null +++ b/Ralph/archive/2026-02-15-login-logo-refinements/prd.json @@ -0,0 +1,185 @@ +{ + "project": "Portfolio — Login Logo & Blur Refinements", + "branchName": "ralph/login-logo-refinements", + "description": "Refine the login screen's CVMIS logo animation, backdrop blur coverage/intensity, and align visual details (border radius, shadows, colors, typography) with the dashboard design system.", + "userStories": [ + { + "id": "US-001", + "title": "Skip to login phase for dev iteration", + "description": "As a developer, I want to skip boot/ECG and land directly on the login screen so I can iterate on login changes quickly.", + "acceptanceCriteria": [ + "In src/App.tsx, change the initial Phase state from 'boot' to 'login'", + "The boot, ECG, and login phases remain in code — only the initial state changes", + "App loads directly to the login screen on refresh", + "Typecheck passes" + ], + "priority": 1, + "passes": true, + "notes": "Temporary — final story reverts this. Phase state is on line 47 of App.tsx." + }, + { + "id": "US-002", + "title": "Extract animation timing into named constants", + "description": "As a developer, I want all animation timing values in CvmisLogo.tsx exposed as named constants at the top of the file so I can quickly tune rise speed, fan speed, fan delay, and easing.", + "acceptanceCriteria": [ + "Named constants at the top of CvmisLogo.tsx for: rise duration (currently 500ms), fan delay after rise (currently 500ms), fan duration (currently 600ms), fan easing curve, fan rotation angle (currently ±50°), fan horizontal spacing (currently ±16px), right pill stagger delay (currently 30ms)", + "Additional named constants for overlap blend: OVERLAY_BLEND_START_PROGRESS (target 0.5), OVERLAP_BLEND_MAX_OPACITY (target 0.2), OVERLAP_BLEND_TRANSITION_DURATION", + "Component behaviour unchanged when constants retain current values", + "Constants are clearly named and grouped with a brief comment block", + "Typecheck passes" + ], + "priority": 2, + "passes": true, + "notes": "Read CvmisLogo.tsx carefully first — some timing values are inline in useEffect/motion props. Extract them ALL to top-level constants. The blend constants are new (for US-004) but should be defined now with sensible defaults." + }, + { + "id": "US-003", + "title": "Scale logo and branding block to ~50% of login card height", + "description": "As a visitor, I want the CVMIS logo and branding text to be larger and more prominent, occupying roughly half the login card's height.", + "acceptanceCriteria": [ + "Logo cssHeight scaled up from current clamp(48px, 4vw, 64px) — target approximately clamp(160px, 18vw, 280px), tune visually for balance", + "Width scales proportionally (SVG viewBox preserves aspect ratio)", + "The branding block (logo + CVMIS title + subtitle + spacing) occupies approximately 50% of the total login card height", + "Logo does not overflow or clip on mobile viewports (>=375px wide)", + "Typecheck passes", + "Verify in browser using dev-browser skill" + ], + "priority": 3, + "passes": true, + "notes": "CvmisLogo accepts cssHeight prop (string) for CSS clamp values. The branding block is in LoginScreen.tsx — the logo, title, and subtitle are in a flex column container. Adjust the cssHeight prop on the CvmisLogo component and check the ratio visually." + }, + { + "id": "US-004", + "title": "Increase branding text to match dashboard typography scale", + "description": "As a visitor, I want the CVMIS title and subtitle on the login screen to be larger and more in line with the dashboard's typography scale.", + "acceptanceCriteria": [ + "CVMIS title font size increased from 13px — target approximately 18-20px to match dashboard heading scale", + "CV Management Information System subtitle font size increased from 11px — target approximately 13-14px", + "Both remain in font-ui (Elvaro Grotesque) with appropriate weight hierarchy", + "Text remains visually balanced with the larger logo above and the login form below", + "Typecheck passes", + "Verify in browser using dev-browser skill" + ], + "priority": 4, + "passes": true, + "notes": "The title and subtitle are in LoginScreen.tsx in the branding section. Look for the CVMIS text and its fontSize style. Use clamp() for responsive sizing consistent with the card's responsive approach." + }, + { + "id": "US-005", + "title": "Add overlap blend effect on fanning capsules", + "description": "As a visitor, I want to see a subtle color blend where the fanning capsules overlap, matching the multiply-blend effect from the Remotion animation.", + "acceptanceCriteria": [ + "CSS mix-blend-mode: multiply applied to the fanning pill elements in CvmisLogo.tsx", + "Blend effect is not visible at the start of the fan animation", + "Blend fades in starting at ~50% of fan animation progress (using OVERLAY_BLEND_START_PROGRESS constant from US-002)", + "Blend reaches max intensity by end of fan (using OVERLAP_BLEND_MAX_OPACITY constant from US-002)", + "Max blend opacity approximately 0.2 (20%)", + "Blend is only perceptible where capsules actually overlap on light backgrounds", + "Blend transition feels smooth, not abrupt", + "Respects prefers-reduced-motion (no animation, show final state)", + "Typecheck passes", + "Verify in browser using dev-browser skill" + ], + "priority": 5, + "passes": true, + "notes": "Use framer-motion's useTransform or a progress-based approach to derive blend opacity from fan animation progress. The pill elements are groups inside the SVG. Apply mixBlendMode: 'multiply' as a style and animate the group's opacity using the timing constants from US-002. The blend should only be visible during/after the fan phase, not during the rise phase." + }, + { + "id": "US-006", + "title": "Extend backdrop blur to cover full dashboard including TopBar", + "description": "As a visitor, I want the frosted-glass blur behind the login card to cover the entire dashboard including the TopBar, so nothing behind the overlay is sharp.", + "acceptanceCriteria": [ + "Blur overlay z-index raised above TopBar z-index (TopBar is zIndex: 100, overlay is currently z-50). Overlay must be >= zIndex: 110 or similar", + "TopBar, Sidebar, and all dashboard content are uniformly blurred behind the overlay", + "Login card itself remains crisp and unblurred (card z-index above overlay)", + "Blur still fades out during the dissolve/exit transition", + "Typecheck passes", + "Verify in browser using dev-browser skill" + ], + "priority": 6, + "passes": true, + "notes": "LoginScreen outer overlay currently has 'fixed inset-0 z-50'. TopBar is zIndex: 100. The overlay needs z-index > 100 to cover it. The login card inside the overlay doesn't need its own z-index since it's a child of the overlay. Check that the dissolve exit animation (isExiting) still works after the z-index change." + }, + { + "id": "US-007", + "title": "Reduce backdrop blur intensity by ~50%", + "description": "As a visitor, I want the backdrop blur to be softer so the dashboard behind is slightly more visible while still providing contrast for the login card.", + "acceptanceCriteria": [ + "Blur value reduced from blur(20px) to approximately blur(10px)", + "The blur value is a named constant co-located with other LoginScreen timing constants for easy adjustment", + "Login card remains clearly readable against the softened backdrop", + "The dissolve exit animation still animates blur from 10px to 0px", + "Typecheck passes", + "Verify in browser using dev-browser skill" + ], + "priority": 7, + "passes": true, + "notes": "The blur is in two places in LoginScreen.tsx: the initial style (backdropFilter: blur(20px)) and the exit animation (animates from blur(20px) to blur(0px)). Extract the blur value to a constant like BACKDROP_BLUR_PX = 10, then reference it in both places." + }, + { + "id": "US-008", + "title": "Align login card border radius and shadow with dashboard design system", + "description": "As a visitor, I want the login card to feel like it belongs to the same design system as the dashboard by matching border radius and shadow tokens.", + "acceptanceCriteria": [ + "Login card border radius changed from 12px to 8px (matching var(--radius-card) / dashboard cards)", + "Login input fields and button border radius changed from 4px to 6px (matching var(--radius-sm) / dashboard inner elements)", + "Login card shadow upgraded from shadow-sm to shadow-lg (0 8px 32px rgba(26,43,42,0.12)) — appropriate for a floating modal over blurred backdrop", + "Use CSS custom property references (var(--radius-card), var(--radius-sm)) where available rather than hardcoded values", + "Typecheck passes", + "Verify in browser using dev-browser skill" + ], + "priority": 8, + "passes": true, + "notes": "Check index.css for whether --radius-card and --radius-sm exist as CSS custom properties. If not, use the hardcoded values (8px and 6px) directly. The card shadow is currently set via inline style — update to the shadow-lg value. The login card borderRadius is in the card's inline style object." + }, + { + "id": "US-009", + "title": "Replace hardcoded colors with design tokens", + "description": "As a developer, I want the login screen to reference the same CSS custom properties as the dashboard so palette changes propagate consistently.", + "acceptanceCriteria": [ + "Input text color changed from hardcoded #111827 to var(--text-primary, #1A2B2A)", + "Cursor/caret color changed from hardcoded #0D6E6E to var(--accent, #0D6E6E)", + "Button background colors changed from hardcoded #0D6E6E / #0A8080 / #085858 to var(--accent) / var(--accent-hover) / appropriate pressed variant using token references", + "Any other hardcoded color values in LoginScreen.tsx that have corresponding CSS custom properties use the token instead", + "No visual change (token values resolve to same colors currently)", + "Typecheck passes" + ], + "priority": 9, + "passes": true, + "notes": "Search LoginScreen.tsx for all hex color values (#xxxxxx) and check whether a corresponding CSS custom property exists in index.css. Some colors were already tokenized in the previous login rework (US-003 of previous run) — verify which ones are still hardcoded. The button has multiple color states (default, hover, pressed) — check all three." + }, + { + "id": "US-010", + "title": "Fix minor typography inconsistencies", + "description": "As a visitor, I want the login screen's typography weight and sizing to feel consistent with the dashboard's conventions.", + "acceptanceCriteria": [ + "Form label font weight increased from 500 to 600 (matching dashboard card header weight convention)", + "Input text mid-value aligned to ~14px to match dashboard body text", + "Button text mid-value aligned to ~15px", + "Connection status indicator gap increased from 6px to 8px (matching dashboard CardHeader gap)", + "No dramatic visual change — these are subtle alignment fixes", + "Typecheck passes" + ], + "priority": 10, + "passes": true, + "notes": "These are small inline style tweaks in LoginScreen.tsx. The labels, inputs, and button already use clamp() for responsive sizing — just adjust the mid-values. The connection indicator gap is in the flex container styling near the bottom of the component." + }, + { + "id": "US-011", + "title": "Re-enable boot sequence", + "description": "As a user, I want the full boot → ECG → login → dashboard experience restored.", + "acceptanceCriteria": [ + "In src/App.tsx, change the initial Phase state back from 'login' to 'boot'", + "Boot → ECG → Login → Dashboard sequence works end to end", + "Login screen shows blurred dashboard behind it with reduced blur and full TopBar coverage", + "Logo animation plays with blend effect, typing animation follows, connection indicator transitions, button pulses", + "Clicking login dissolves the overlay to reveal the dashboard", + "Typecheck passes", + "Verify in browser using dev-browser skill" + ], + "priority": 11, + "passes": true, + "notes": "Simple revert of US-001. Phase state is on line 47 of App.tsx." + } + ] +} diff --git a/Ralph/archive/2026-02-15-login-logo-refinements/progress.txt b/Ralph/archive/2026-02-15-login-logo-refinements/progress.txt new file mode 100644 index 0000000..1d3361e --- /dev/null +++ b/Ralph/archive/2026-02-15-login-logo-refinements/progress.txt @@ -0,0 +1,202 @@ +# Progress Log — Login Logo & Blur Refinements +# Branch: ralph/login-logo-refinements +# Started: 2026-02-15 + +## Codebase Patterns + +### Project Structure +- Components in `src/components/`, tiles in `src/components/tiles/` +- Data files in `src/data/` +- Types in `src/types/pmr.ts` and `src/types/index.ts` +- Hooks in `src/hooks/`, Contexts in `src/contexts/`, Lib in `src/lib/` +- Path alias: `@/` maps to `./src/` + +### Phase Management +- App.tsx controls phase: 'boot' -> 'ecg' -> 'login' -> 'pmr' +- BootSequence.tsx, ECGAnimation.tsx — LOCKED, do not modify +- LoginScreen.tsx bridges to dashboard + +### Typography +- Elvaro Grotesque (`font-ui`, `var(--font-ui)`) — primary UI font +- Blumir (`font-ui-alt`) — alternative variable font +- Geist Mono (`font-geist`, `var(--font-geist-mono)`) — timestamps, data values +- Fira Code (`font-mono`) — boot/ECG terminal only +- Do NOT use Inter, Roboto, DM Sans, or system defaults + +### Design Tokens (index.css CSS variables) +- --surface: #FFFFFF (card/topbar background) +- --bg-dashboard: #F0F5F4 (warm sage content background) +- --accent: #0D6E6E (teal primary) +- --accent-hover: #0A8080 +- --accent-pressed: #085858 +- --accent-light: rgba(10,128,128,0.08) +- --border: #D4E0DE (structural borders) +- --border-card: #E4EDEB (card/inner borders) +- --text-primary: #1A2B2A +- --text-secondary: #5B7A78 +- --text-tertiary: #8DA8A5 +- --sidebar-width: 304px +- --topbar-height: 56px + +### Known Dependencies +- React 18.3.1, TypeScript, Vite, Tailwind CSS +- Framer Motion 11.15.0, Lucide React 0.468.0, fuse.js 7.0.0 + +### Key Files for This Feature +- src/components/CvmisLogo.tsx — logo component with animation (timing constants to extract) +- src/components/LoginScreen.tsx — main login screen (overlay, blur, card styling) +- src/App.tsx — phase management (skip/restore boot sequence) +- src/index.css — CSS custom properties, design tokens + +### CvmisLogo Component (from previous run) +- `size` prop: numeric, sets SVG height attribute directly +- `cssHeight` prop: string, sets height via CSS style (use for clamp/responsive values) +- `animated` prop: boolean, enables framer-motion reveal animation (1000ms total) +- Logo animation: 500ms rise (green capsule) + 500ms fan-out (all three) = 1000ms total +- All timing values are named constants at top of file — tune there, not inline +- Blend constants (OVERLAY_BLEND_*) are exported for use by other components (US-005) + +### LoginScreen.tsx State (from previous run) +- Overlay: fixed inset-0 zIndex: 110, rgba(240, 245, 244, 0.7) + backdrop-filter: blur(20px) +- TopBar is zIndex: 100 — overlay now correctly covers it (fixed in US-006) +- Card borderRadius: 12px, inputs/button borderRadius: 4px +- Some colors already tokenized (--surface, --accent, --bg-dashboard) from previous run +- Some colors still hardcoded (#111827 input text, button bg states, caret color) + +--- + +## 2026-02-15 - US-001: Skip to login phase for dev iteration +- Changed initial Phase state from `'boot'` to `'login'` in `src/App.tsx` line 47 +- Files changed: `src/App.tsx` +- **Learnings for future iterations:** + - Phase state is a simple `useState` on line 47 of App.tsx + - All phase rendering logic (`boot`, `ecg`, `login`, `pmr`) remains intact — only initial value changes + - US-011 will revert this exact change back to `'boot'` +--- + +## 2026-02-15 - US-002: Extract animation timing into named constants +- Extracted all inline timing values in CvmisLogo.tsx to named constants at top of file +- Constants added: RISE_DURATION_MS, RISE_DURATION_S, RISE_OPACITY_DURATION_S, RISE_EASING, RISE_START_Y, FAN_DELAY_AFTER_RISE_MS, FAN_DURATION_S, FAN_ROTATION_DEG, FAN_HORIZONTAL_PX, FAN_RIGHT_STAGGER_S, TOTAL_ANIMATION_MS +- Added overlap blend constants for US-005: OVERLAY_BLEND_START_PROGRESS, OVERLAP_BLEND_MAX_OPACITY, OVERLAP_BLEND_TRANSITION_DURATION_S (exported) +- Files changed: `src/components/CvmisLogo.tsx` +- **Learnings for future iterations:** + - Blend constants are `export`ed because TypeScript strict mode flags unused `const` declarations — exporting avoids the TS6133 error while making them available for US-005 + - TOTAL_ANIMATION_MS is computed from FAN_DELAY_AFTER_RISE_MS + FAN_DURATION_S * 1000, so changing rise or fan timing automatically updates the done-timer + - FAN_EASING was already a named constant before this story; it was left in place and grouped with the new fan constants +--- + +## 2026-02-15 - US-003: Scale logo and branding block to ~50% of login card height +- Scaled CvmisLogo `cssHeight` prop from `clamp(80px, 8vw, 120px)` to `clamp(160px, 18vw, 280px)` +- Adjusted logo wrapper marginBottom from 10px to 12px for spacing balance +- Browser-verified: desktop ratio 51.3% (target 50% ±10%), mobile (375px) ratio 41.1% — both within tolerance +- No overflow or clipping on mobile viewport +- Files changed: `src/components/LoginScreen.tsx` +- **Learnings for future iterations:** + - The CvmisLogo `cssHeight` prop maps directly to a CSS `height` style on the SVG — `clamp()` values work well for responsive scaling + - At 375px viewport, `18vw = 67.5px` which triggers the clamp minimum of 160px — the logo remains a comfortable size on small screens + - The branding block ratio can be measured by comparing `brandingBlock.getBoundingClientRect().height + marginBottom` against `card.innerHeight - padding` + - The branding block container has class `flex flex-col items-center` — use this selector for programmatic measurement +--- + +## 2026-02-15 - US-004: Increase branding text to match dashboard typography scale +- Increased CVMIS title fontSize from `13px` to `clamp(16px, 1.4vw, 20px)` — renders 20px on desktop +- Increased subtitle fontSize from `11px` to `clamp(12px, 1vw, 14px)` — renders 14px on desktop +- Increased subtitle marginTop from `2px` to `3px` for better spacing with larger text +- Both remain in font-ui (Elvaro Grotesque) with weight 600 (title) and 400 (subtitle) +- Browser-verified: text is visually balanced with the larger logo and login form +- Files changed: `src/components/LoginScreen.tsx` +- **Learnings for future iterations:** + - The branding text clamp values use the same responsive pattern as the logo `cssHeight` — mid-values around 1-1.5vw work well for text + - Title and subtitle are `` elements inside the `.flex.flex-col.items-center` branding container + - Weight hierarchy (600 title, 400 subtitle) provides sufficient visual differentiation without needing size contrast as large +--- + +## 2026-02-15 - US-005: Add overlap blend effect on fanning capsules +- Added `blendActive` state to CvmisLogo, triggered by timer at `blendStartMs` (50% through fan animation) +- Added two blend overlay `` elements after the main pills: copies of left/right pill shapes with `mixBlendMode: 'multiply'` and opacity transitioning from 0 to 0.2 +- Blend overlays share the same `transform` and `transition` as their corresponding original pills, plus an opacity transition using `OVERLAP_BLEND_TRANSITION_DURATION_S` +- Reduced motion: `blendActive` starts `true`, `transition: 'none'` — final blend state shown immediately +- Browser-verified: blend darkening visible at pill overlap areas, opacity confirmed at 0.2 +- Files changed: `src/components/CvmisLogo.tsx` +- **Learnings for future iterations:** + - `mix-blend-mode` is not CSS-animatable — use overlay elements with animated opacity instead of trying to transition the blend mode + - Blend overlay approach: duplicate the pill shapes (rect only, no icons) as separate `` elements with `mixBlendMode: 'multiply'` and low opacity + - The `useMemo` for `blendStartMs` avoids recalculation — all timing constants are module-level so this is stable + - Combined CSS transition strings work in SVG `` style: `transform 0.6s cubic-bezier(...), opacity 0.3s ease-out` +--- + +## 2026-02-15 - US-006: Extend backdrop blur to cover full dashboard including TopBar +- Changed overlay from Tailwind `z-50` class to inline `zIndex: 110` to sit above TopBar (`zIndex: 100`) +- Browser-verified: TopBar, sidebar, and all content uniformly blurred; login card remains crisp +- Files changed: `src/components/LoginScreen.tsx` +- **Learnings for future iterations:** + - TopBar uses inline `zIndex: 100` (not a Tailwind class), so overlay needs inline zIndex > 100 + - Tailwind's `z-50` = z-index 50, which was below the TopBar — switched to inline style for precise control + - The login card doesn't need its own z-index since it's a child of the overlay and inherits stacking context +--- + +## 2026-02-15 - US-007: Reduce backdrop blur intensity by ~50% +- Added `BACKDROP_BLUR_PX = 10` constant at top of LoginScreen.tsx +- Replaced hardcoded `blur(20px)` in initial style with template literal using constant +- Exit animation still targets `blur(0px)` — Framer Motion interpolates from current 10px to 0px +- Files changed: `src/components/LoginScreen.tsx` +- **Learnings for future iterations:** + - The `BACKDROP_BLUR_PX` constant is in the "Login screen timing & visual constants" block at top of LoginScreen.tsx + - Framer Motion's `animate` prop interpolates from the element's current computed style, so the exit blur animation doesn't need the starting value explicitly + - Only the initial style needs the constant; the exit target (`blur(0px)`) is always 0 +--- + +## 2026-02-15 - US-008: Align login card border radius and shadow with dashboard design system +- Changed card borderRadius from `12px` to `var(--radius-card, 8px)` +- Changed card boxShadow from `shadow-sm` (`0 1px 2px rgba(26,43,42,0.05)`) to `var(--shadow-lg, 0 8px 32px rgba(26,43,42,0.12))` +- Changed username input, password input, and button borderRadius from `4px` to `var(--radius-sm, 6px)` +- All values use CSS custom property references with fallbacks +- Files changed: `src/components/LoginScreen.tsx` +- **Learnings for future iterations:** + - CSS tokens `--radius-card`, `--radius-sm`, `--shadow-sm`, `--shadow-md`, `--shadow-lg` are all defined in `index.css` `:root` — use `var()` references with fallback values + - The button has 18-space indentation vs 20-space for inputs — `replace_all` may not catch all instances if matching on indentation + - The spinner (`borderRadius: '50%'`) and status indicator dot should NOT be changed — they're circles, not card elements +--- + +## 2026-02-15 - US-009: Replace hardcoded colors with design tokens +- Replaced all hardcoded hex colors in LoginScreen.tsx with CSS custom property references (`var()` with fallbacks) +- Input text color: `#111827` → `var(--text-primary, #1A2B2A)` +- Cursor/caret color: `#0D6E6E` → `var(--accent, #0D6E6E)` (2 instances) +- Button backgrounds: `#0D6E6E` → `var(--accent)`, `#0A8080` → `var(--accent-hover)`, `#085858` → `var(--accent-pressed)` +- Spinner border/top: `#E4EDEB` → `var(--border-light)`, `#0D6E6E` → `var(--accent)` +- Input inactive borders: `#E4EDEB` → `var(--border-light)` (username + password fields) +- Card border: `#E4EDEB` → `var(--border-light)` +- Footer border: `#E4EDEB` → `var(--border-light)` +- Connection status colors: `#059669` → `var(--success)`, `#DC2626` → `var(--alert)` (dot bg + text) +- Focus ring: `ring-[#0D6E6E]/40` → `ring-accent/40` (Tailwind token) +- Added `--accent-pressed: #085858` token to `index.css` `:root` (completes the accent state trio) +- Files changed: `src/components/LoginScreen.tsx`, `src/index.css` +- **Learnings for future iterations:** + - `#FFFFFF` on button text is intentional for contrast — no `--text-on-accent` token exists; leave as hardcoded white + - `rgba(240, 245, 244, 0.7)` overlay bg is `--bg-dashboard` at 70% opacity — no token for this; leave as rgba + - Status dot glow `boxShadow` uses rgba variants of success/alert colors at 40% opacity — no token for these glow effects + - Tailwind config has `accent` and `accent-hover` color tokens, so `ring-accent/40` works in class names + - Always add fallback values in `var()` references (e.g., `var(--accent, #0D6E6E)`) for resilience +--- + +## 2026-02-15 - US-010: Fix minor typography inconsistencies +- Changed form label fontWeight from 500 to 600 (both Username and Password labels) to match dashboard card header weight convention +- Adjusted input text fontSize mid-value from `clamp(13px, 1.1vw, 15px)` to `clamp(13px, 1.2vw, 15px)` — renders ~14-15px on standard viewports +- Adjusted button text fontSize mid-value from `clamp(14px, 1.1vw, 16px)` to `clamp(14px, 1.2vw, 16px)` — renders ~15px on standard viewports +- Changed connection status indicator gap from 6px to 8px (matching dashboard CardHeader gap) +- Files changed: `src/components/LoginScreen.tsx` +- **Learnings for future iterations:** + - These are all subtle alignment tweaks — the clamp mid-value change from 1.1vw to 1.2vw shifts rendering by ~1px on 1280px viewports + - The label fontWeight 600 matches the dashboard's `CardHeader` convention (seen in `Card.tsx`) + - The connection indicator gap of 8px matches the standard icon-text gap used in dashboard card headers +--- + +## 2026-02-15 - US-011: Re-enable boot sequence +- Changed initial Phase state from `'login'` back to `'boot'` in `src/App.tsx` line 47 +- Reverts the dev shortcut from US-001, restoring the full boot → ECG → login → dashboard flow +- Typecheck and lint pass cleanly +- Files changed: `src/App.tsx` +- **Learnings for future iterations:** + - This is a simple one-line revert — the phase state controls the entire UI flow + - All 11 stories in this PRD are now complete +--- diff --git a/tasks/prd-semantic-search.md b/tasks/prd-semantic-search.md index 25a22ac..e4cb1bf 100644 --- a/tasks/prd-semantic-search.md +++ b/tasks/prd-semantic-search.md @@ -25,24 +25,36 @@ The portfolio's command palette currently uses Fuse.js for fuzzy string matching **Acceptance Criteria:** - [ ] Node script `scripts/generate-embeddings.ts` reads all palette data from `src/lib/search.ts` -- [ ] Calls OpenAI `text-embedding-3-small` API with a rich text representation of each item (title + subtitle + keywords + any extended context from data files) +- [ ] Uses the same ONNX model (`all-MiniLM-L6-v2` via `@xenova/transformers`) as the browser runtime to generate embeddings +- [ ] Builds a rich text representation of each item (title + subtitle + keywords + extended context from data files) - [ ] Outputs `src/data/embeddings.json` — array of `{ id: string, embedding: number[] }` - [ ] Script is runnable via `npm run generate-embeddings` -- [ ] Script requires `OPENAI_API_KEY` env var; fails gracefully with clear error if missing +- [ ] No external API key required — model runs locally via Node.js - [ ] Embeddings file is committed to repo (static asset, not generated per-build) - [ ] Typecheck passes -#### US-002: Client-side cosine similarity search +#### US-002: Preload ONNX model during boot sequence +**Description:** As a visitor, I want the semantic search model to be ready by the time I reach the dashboard, without slowing down the initial experience. + +**Acceptance Criteria:** +- [ ] Model download (`all-MiniLM-L6-v2` via `@xenova/transformers`) begins when `App.tsx` mounts (during `'boot'` phase) +- [ ] Download runs in background — does not block or affect boot/ECG/login animations +- [ ] Model is cached in browser (IndexedDB) — second visit loads from cache instantly +- [ ] A global ready state (React context or module-level promise) signals when model is available +- [ ] If model fails to load (network error, etc.), the app continues normally — no error shown to user +- [ ] Typecheck passes + +#### US-003: Client-side cosine similarity search **Description:** As a visitor, I want the command palette to understand what I mean, not just match strings. **Acceptance Criteria:** - [ ] New `src/lib/semantic-search.ts` module with cosine similarity function - [ ] Loads `embeddings.json` and provides a `semanticSearch(query: string, items: PaletteItem[])` function -- [ ] Query embedding is computed client-side using a lightweight approach (see Technical Considerations) +- [ ] Query embedding computed in-browser using `all-MiniLM-L6-v2` ONNX model via `@xenova/transformers` - [ ] Returns ranked `PaletteItem[]` with similarity scores - [ ] Typecheck passes -#### US-003: Integrate semantic search into command palette +#### US-004: Integrate semantic search into command palette **Description:** As a visitor, I want the command palette to use semantic search with Fuse.js as a fallback. **Acceptance Criteria:** @@ -53,7 +65,7 @@ The portfolio's command palette currently uses Fuse.js for fuzzy string matching - [ ] Typecheck passes - [ ] Verify in browser: search "data analysis" surfaces analytics-related roles/skills, not just items with "data" in the title -#### US-004: Enrich embedding content with deep context +#### US-005: Enrich embedding content with deep context **Description:** As a developer, I want embeddings to capture rich context beyond just titles, so semantic search is truly useful. **Acceptance Criteria:** @@ -69,7 +81,7 @@ The portfolio's command palette currently uses Fuse.js for fuzzy string matching ### Phase 2: AI Chat Widget -#### US-005: Chat widget UI — floating button +#### US-006: Chat widget UI — floating button **Description:** As a visitor, I see a floating chat button at the bottom-right of the dashboard that opens a chat panel. **Acceptance Criteria:** @@ -82,7 +94,7 @@ The portfolio's command palette currently uses Fuse.js for fuzzy string matching - [ ] Typecheck passes - [ ] Verify in browser using dev server -#### US-006: Chat panel UI +#### US-007: Chat panel UI **Description:** As a visitor, I want a chat panel that feels like a support chat — compact, positioned above the floating button. **Acceptance Criteria:** @@ -99,7 +111,7 @@ The portfolio's command palette currently uses Fuse.js for fuzzy string matching - [ ] Typecheck passes - [ ] Verify in browser using dev server -#### US-007: Gemini Flash integration +#### US-008: Gemini Flash integration **Description:** As a visitor, I can ask natural language questions and get intelligent answers about Andy's experience. **Acceptance Criteria:** @@ -112,7 +124,7 @@ The portfolio's command palette currently uses Fuse.js for fuzzy string matching - [ ] Loading state shown while waiting for response - [ ] Typecheck passes -#### US-008: Chat context and conversation history +#### US-009: Chat context and conversation history **Description:** As a visitor, I want multi-turn conversation so I can ask follow-up questions. **Acceptance Criteria:** @@ -125,20 +137,21 @@ The portfolio's command palette currently uses Fuse.js for fuzzy string matching ## Functional Requirements ### Phase 1 -- FR-1: Build script generates OpenAI `text-embedding-3-small` embeddings for all palette items +- FR-1: Build script generates embeddings using `all-MiniLM-L6-v2` ONNX model (same model used at runtime) - FR-2: Embeddings stored as committed static JSON (`src/data/embeddings.json`) - FR-3: Client-side cosine similarity ranks items by semantic relevance - FR-4: Command palette uses semantic search as primary, Fuse.js as fallback -- FR-5: Query embedding must be computed without a runtime API call (see Technical Considerations) +- FR-5: ONNX model preloaded during boot sequence (before dashboard renders) +- FR-6: Query embedding computed in-browser — no runtime API calls ### Phase 2 -- FR-6: Floating chat button rendered in DashboardLayout, bottom-right, above content -- FR-7: Chat panel opens/closes on button click with animation -- FR-8: User messages sent to Gemini Flash API with CV context as system prompt -- FR-9: Gemini responses parsed into answer text + relevant item IDs -- FR-10: Relevant items rendered as clickable cards using existing palette item styling and action routing -- FR-11: Streaming responses displayed progressively -- FR-12: Conversation state managed per-session (cleared on page reload) +- FR-7: Floating chat button rendered in DashboardLayout, bottom-right, above content +- FR-8: Chat panel opens/closes on button click with animation +- FR-9: User messages sent to Gemini Flash API with CV context as system prompt +- FR-10: Gemini responses parsed into answer text + relevant item IDs +- FR-11: Relevant items rendered as clickable cards using existing palette item styling and action routing +- FR-12: Streaming responses displayed progressively +- FR-13: Conversation state managed per-session (cleared on page reload) ## Non-Goals @@ -175,13 +188,25 @@ The portfolio's command palette currently uses Fuse.js for fuzzy string matching ## Technical Considerations -### Phase 1: Query Embedding Challenge -The main challenge is computing a query embedding client-side without an API call. Options: -- **Option A (Recommended):** Pre-compute embeddings for items only. At query time, use a lightweight client-side text similarity approach (e.g., TF-IDF or BM25 on the enriched text) combined with the embedding vectors for re-ranking. This avoids shipping a model to the browser. -- **Option B:** Use a small ONNX model in the browser (e.g., `all-MiniLM-L6-v2` via Transformers.js). ~23MB download, but gives true semantic matching. Could be lazy-loaded. -- **Option C:** Call OpenAI embedding API at query time. Adds latency (~200ms) and runtime cost, but simplest implementation. +### Phase 1: ONNX Model Strategy -**Decision needed at implementation time** — Option B gives the best semantic search quality. Option A is simpler but less semantic. Option C is simplest but has runtime costs. +**Decision: `all-MiniLM-L6-v2` via `@xenova/transformers` for both build-time and runtime embedding.** + +- Same model used everywhere — embeddings live in the same vector space, so cosine similarity works correctly +- No external API keys required for embedding generation or search +- Build script runs the model in Node.js to pre-compute item embeddings +- Browser loads the same model at runtime for query embedding + +**Preloading strategy:** +- Model download (~23MB, ONNX format) begins during the boot sequence phase (`'boot'` in App.tsx) +- The boot → ECG → login flow takes 8-10s, giving ample time for download + cache +- Model is cached by the browser (IndexedDB via Transformers.js) — subsequent visits load instantly +- If model hasn't finished loading when user opens command palette, fall back to Fuse.js silently + +**Embedding content:** +- Each palette item gets a natural-language paragraph for embedding, not just keywords +- E.g., a consultation becomes: "Senior Data Analyst at Norfolk and Waveney ICB, 2021 to present. Led medicines optimisation analytics, built Power BI dashboards for 200+ clinicians..." +- Richer text = better semantic matching ### Phase 2: Gemini Flash - Use `gemini-2.0-flash` (or latest) — fast, cheap, good for short-form Q&A @@ -205,7 +230,6 @@ The main challenge is computing a query embedding client-side without an API cal ## Open Questions -- **Phase 1 query embedding**: Which approach (A/B/C) gives the best tradeoff of quality vs. bundle size vs. complexity? This should be prototyped early. -- **Gemini API key exposure**: Is direct client-side exposure acceptable, or should we add a minimal edge function proxy? (User chose direct exposure — revisit if abuse becomes an issue.) - **Chat widget on mobile**: Should the chat panel be a full-screen modal on small screens, or a bottom sheet? - **Suggested questions**: Should the chat widget show 2-3 starter questions when first opened (e.g., "What's Andy's NHS experience?", "Tell me about his data skills")? +- **Model CDN**: Transformers.js downloads models from Hugging Face by default. Should we self-host the ONNX model files for reliability, or trust HF's CDN?