Compare commits

..

7 Commits

33 changed files with 1376 additions and 1604 deletions
+216
View File
@@ -0,0 +1,216 @@
---
name: ralph-setup
description: Set up autonomous AI development tasks using the Ralph Wiggum technique. Use when the user wants to create a RALPH orchestration — either a simple looping prompt or a multi-hat coordinated workflow. Interviews the user to understand requirements, decides the appropriate mode, and generates all necessary configuration files (ralph.yml, hats.yml, PROMPT.md). Triggers on mentions of "ralph", "autonomous loop", "hat-based", "orchestration", or requests to set up iterative AI agent tasks.
---
# Ralph Setup Skill
Set up autonomous AI development tasks using the Ralph Wiggum technique — either as a simple iterating prompt or a coordinated hat-based workflow.
## Background
Ralph implements the Ralph Wiggum technique: give an AI agent a task, loop it until it's done. The orchestrator is deliberately thin — it trusts the agent to do the work and enforces quality through backpressure (tests, lint, typecheck must pass).
There are two modes:
| Mode | What It Does | Best For |
|------|-------------|----------|
| **Traditional (Simple Prompt)** | Single loop — agent iterates until LOOP_COMPLETE | Quick tasks, single-concern work, anything one agent can handle in a straight line |
| **Hat-Based** | Specialised personas coordinate through typed events | Complex workflows, multi-step processes, tasks needing distinct planning/building/reviewing phases |
## Core Tenets (Apply to Both Modes)
These six tenets guide every RALPH setup. Reference them when making decisions:
1. **Fresh Context Is Reliability** — Each iteration clears context. The prompt must be self-contained enough to re-read, re-plan, and re-execute every cycle.
2. **Backpressure Over Prescription** — Don't prescribe HOW to do the work. Create gates that reject bad work (tests pass, lint clean, types check).
3. **The Plan Is Disposable** — Regeneration costs one planning loop. Cheap. Don't over-invest in preserving plans.
4. **Disk Is State, Git Is Memory** — Files are the handoff mechanism between iterations. Git provides checkpointing and rollback.
5. **Steer With Signals, Not Scripts** — Add signs (success criteria, quality gates), not step-by-step scripts.
6. **Let Ralph Ralph** — Sit ON the loop, not IN it. The orchestrator coordinates; the agent does the work.
## Workflow
### Phase 1: Interview the User
Before generating anything, you need to understand the task. Ask targeted questions to fill in these blanks:
**Essential information:**
- What is the task? (Be specific — "build an API" is too vague; "build a REST API for user management with Express.js and TypeScript" is good)
- What does "done" look like? (Measurable success criteria — tests pass, endpoints respond, specific files exist)
- What language/framework/tools are involved?
- Does the project already exist, or is this greenfield?
- Are there existing tests, linting, or type-checking set up?
**Information that helps you decide the mode:**
- How many distinct phases or concerns does this task have? (1-2 = simple prompt; 3+ = consider hats)
- Does the task need planning before building? (If yes, hat-based is likely better)
- Does the task need a review/QA step separate from building? (If yes, hat-based)
- Is there a spec or design document to follow? (Spec-driven development suits hats well)
- How complex is the codebase? (Large existing codebase with multiple modules = hat-based)
**Don't over-interview.** If the user gives you a clear, well-scoped task, you may have enough after 1-2 questions. If the task is vague, probe until you can write a crisp PROMPT.md.
### Phase 2: Decide the Mode
Use this decision framework:
**Choose Simple Prompt when:**
- The task is a single concern (add a feature, fix a bug, write a script)
- One agent can handle it start to finish without distinct phases
- The success criteria are straightforward (tests pass, script runs)
- The user explicitly wants something quick and simple
- The task can be fully described in a PROMPT.md under ~50 lines
**Choose Hat-Based when:**
- The task has 3+ distinct phases (plan → build → test → review)
- Different phases need different "mindsets" (architect vs implementer vs reviewer)
- The task involves spec-driven development (spec → implement → verify)
- There's a TDD workflow (write tests → implement → verify)
- The task is large enough that a single prompt would be overwhelming
- Multiple files/modules need coordinated changes
- The user explicitly asks for hats or a structured workflow
**When in doubt:** Start with Simple Prompt. You can always add hats later. Simpler is more robust.
### Phase 3: Generate the Files
Generate the appropriate files into the user's project directory. Always explain what you're creating and why.
Read the appropriate reference file before generating:
- For Simple Prompt: `references/simple-prompt-reference.md`
- For Hat-Based: `references/hat-based-reference.md`
#### Files to Generate
**Both modes:**
- `ralph.yml` — Main configuration
- `PROMPT.md` — The task definition
**Hat-Based mode additionally:**
- `hats.yml` — Hat definitions with triggers, publishes, and instructions
### Phase 4: Review with the User
After generating the files, walk the user through what you created:
- Summarise the task as you understood it
- Explain the mode choice and why
- Highlight the success criteria / completion promise
- For hat-based: explain the event flow between hats
- Ask if anything needs adjusting before they run it
Then tell them how to run it:
```bash
# Simple prompt
ralph run
# Hat-based
ralph run --config hats.yml
# With iteration limit
ralph run --max-iterations 50
```
## Writing Good Prompts (PROMPT.md)
The PROMPT.md is the most important file. It must be:
**Self-contained:** Every iteration starts fresh. The prompt must contain everything the agent needs to understand the task, check progress, and continue.
**Outcome-focused:** Define WHAT, not HOW. Let the agent figure out the approach.
**Measurable:** Include concrete success criteria the agent can verify:
- "All tests pass" (not "write good tests")
- "The /users endpoint returns 200 with valid JSON" (not "make the API work")
- "TypeScript compiles with zero errors" (not "fix the types")
**Structured but not prescriptive:** Use sections like Task, Requirements, Success Criteria, Constraints. Don't write step-by-step instructions.
### Prompt Template (Simple)
```markdown
# Task: [Clear, specific title]
[2-3 sentence description of what needs to be built/done]
## Requirements
- [Specific requirement 1]
- [Specific requirement 2]
- [Specific requirement 3]
## Success Criteria
All of the following must be true:
- [ ] [Measurable criterion 1]
- [ ] [Measurable criterion 2]
- [ ] [Measurable criterion 3]
## Constraints
- [Technology constraints]
- [Style/convention constraints]
- [Performance constraints if any]
## Status
Track your progress here. Mark items complete as you go.
When all success criteria are met, print LOOP_COMPLETE.
```
## Designing Hat Systems
When creating hats, follow these principles:
**Each hat should have a single responsibility.** Don't create a hat that plans AND builds.
**Events flow forward.** The event chain should be a clear pipeline: task.start → plan.ready → build.done → review.complete → task.done.
**Instructions should be specific to the hat's role.** The planner hat gets planning instructions, the builder gets building instructions.
**Keep it minimal.** 2-4 hats is typical. More than 5 is usually overengineered.
### Common Hat Patterns
**Plan → Build (2 hats):**
Good for tasks that need architectural thinking before coding.
**Plan → Build → Review (3 hats):**
Good for tasks that need quality assurance.
**Spec → Implement → Verify (3 hats):**
Good for spec-driven development.
**Test → Implement → Verify (3 hats):**
Good for TDD workflows.
See `references/hat-based-reference.md` for full configuration examples.
## Backpressure Configuration
Backpressure gates reject incomplete work. Common gates:
```yaml
backpressure:
gates:
- name: "tests"
command: "npm test"
on_fail: "retry"
- name: "lint"
command: "npm run lint"
on_fail: "retry"
- name: "typecheck"
command: "npx tsc --noEmit"
on_fail: "retry"
```
Only add gates for tools that exist in the project. If there are no tests yet, don't add a test gate (unless the task IS to create tests).
## Cost and Safety
Always configure iteration limits. Remind the user:
- Default max iterations: 100
- Default max runtime: 4 hours
- A 50-iteration cycle on a large codebase can cost $50-100+ in API credits
- Recommend starting with `--max-iterations 30` for new setups and increasing if needed
- Git checkpointing is on by default — the user can always roll back
@@ -0,0 +1,335 @@
# Hat-Based Reference
## Overview
Hat-based mode uses specialised personas ("hats") that coordinate through typed events. Each hat triggers on specific events and publishes new events when done, creating a pipeline of distinct phases.
Use this when the task genuinely benefits from separating concerns — e.g., planning separately from building, or reviewing separately from implementing.
## hats.yml Structure
```yaml
cli:
backend: "claude"
event_loop:
starting_event: "task.start" # First event that kicks off the pipeline
completion_promise: "LOOP_COMPLETE" # String that signals completion
max_iterations: 100 # Safety limit
hats:
hat_name:
name: "Human-Readable Name"
triggers: ["event.that.activates.this.hat"]
publishes: ["event.this.hat.emits.when.done"]
instructions: |
Detailed instructions for what this hat should do.
Must be self-contained — the hat gets fresh context each time.
Should reference PROMPT.md for the overall task.
Should specify what "done" means for this hat.
```
### Key Rules
- **triggers**: List of events that activate this hat. A hat runs when ANY of its trigger events fire.
- **publishes**: List of events this hat emits when it completes its work.
- **instructions**: The prompt for this hat. Must be specific to the hat's role.
- Events flow forward through the pipeline. Avoid circular event chains.
- The last hat in the pipeline should print LOOP_COMPLETE when the overall task is done.
## Common Patterns
### Pattern 1: Plan → Build (2 Hats)
Best for tasks that need architectural thinking before coding.
```yaml
cli:
backend: "claude"
event_loop:
starting_event: "task.start"
completion_promise: "LOOP_COMPLETE"
hats:
planner:
name: "Planner"
triggers: ["task.start"]
publishes: ["plan.ready"]
instructions: |
You are the Planner. Read PROMPT.md to understand the task.
Your job:
1. Analyse the requirements and existing codebase
2. Create a clear implementation plan in .ralph/plan.md
3. Break the work into concrete steps with file-level detail
4. Identify any risks or unknowns
Write the plan to .ralph/plan.md then emit plan.ready.
Do NOT write any code. Planning only.
builder:
name: "Builder"
triggers: ["plan.ready"]
publishes: ["task.done"]
instructions: |
You are the Builder. Read PROMPT.md for the task and .ralph/plan.md
for the implementation plan.
Your job:
1. Follow the plan step by step
2. Write clean, tested code
3. Run tests after each significant change
4. Update .ralph/plan.md to mark completed steps
When all success criteria from PROMPT.md are met and all tests pass,
print LOOP_COMPLETE.
```
### Pattern 2: Plan → Build → Review (3 Hats)
Adds a review phase for quality assurance.
```yaml
cli:
backend: "claude"
event_loop:
starting_event: "task.start"
completion_promise: "LOOP_COMPLETE"
hats:
planner:
name: "Planner"
triggers: ["task.start", "review.changes_requested"]
publishes: ["plan.ready"]
instructions: |
You are the Planner. Read PROMPT.md to understand the task.
If triggered by review.changes_requested, read .ralph/review.md
for feedback and update the plan accordingly.
Create or update .ralph/plan.md with a clear implementation plan.
Emit plan.ready when done. Do NOT write code.
builder:
name: "Builder"
triggers: ["plan.ready"]
publishes: ["build.done"]
instructions: |
You are the Builder. Read PROMPT.md and .ralph/plan.md.
Implement the plan. Write tests. Run them.
When implementation is complete, emit build.done.
Do NOT assess overall quality — that's the Reviewer's job.
reviewer:
name: "Reviewer"
triggers: ["build.done"]
publishes: ["review.approved", "review.changes_requested"]
instructions: |
You are the Reviewer. Read PROMPT.md for requirements.
Review the current state of the codebase against the success criteria:
1. Do all tests pass?
2. Are all requirements met?
3. Is the code clean and following project conventions?
4. Are there edge cases not covered?
If everything passes, write your review to .ralph/review.md
and print LOOP_COMPLETE.
If changes are needed, write specific feedback to .ralph/review.md
and emit review.changes_requested.
```
### Pattern 3: Spec → Implement → Verify (3 Hats)
For spec-driven development — good when working from a design document.
```yaml
cli:
backend: "claude"
event_loop:
starting_event: "task.start"
completion_promise: "LOOP_COMPLETE"
hats:
spec_writer:
name: "Spec Writer"
triggers: ["task.start", "verify.gaps_found"]
publishes: ["spec.ready"]
instructions: |
You are the Spec Writer. Read PROMPT.md for the high-level task.
If triggered by verify.gaps_found, read .ralph/verification.md
for gaps and update the spec to address them.
Write a detailed technical specification to .ralph/spec.md:
- API contracts (endpoints, request/response shapes)
- Data models
- Error handling behaviour
- Test scenarios
Emit spec.ready when done. Do NOT write implementation code.
implementer:
name: "Implementer"
triggers: ["spec.ready"]
publishes: ["implementation.done"]
instructions: |
You are the Implementer. Read .ralph/spec.md for the specification.
Implement exactly what the spec describes. Write tests that verify
each specification point. Run tests after each change.
Emit implementation.done when the spec is fully implemented.
verifier:
name: "Verifier"
triggers: ["implementation.done"]
publishes: ["verify.passed", "verify.gaps_found"]
instructions: |
You are the Verifier. Read .ralph/spec.md and PROMPT.md.
Verify that the implementation matches the spec:
1. Run all tests — they must pass
2. Check each spec point against the code
3. Verify success criteria from PROMPT.md
If everything checks out, print LOOP_COMPLETE.
If there are gaps, write them to .ralph/verification.md
and emit verify.gaps_found.
```
### Pattern 4: TDD — Test → Implement → Verify (3 Hats)
For test-driven development workflows.
```yaml
cli:
backend: "claude"
event_loop:
starting_event: "task.start"
completion_promise: "LOOP_COMPLETE"
hats:
test_writer:
name: "Test Writer"
triggers: ["task.start", "verify.tests_needed"]
publishes: ["tests.ready"]
instructions: |
You are the Test Writer. Read PROMPT.md for requirements.
Write failing tests FIRST that describe the desired behaviour.
Tests should be comprehensive and cover edge cases.
If triggered by verify.tests_needed, read .ralph/verification.md
for the specific test gaps to fill.
Write tests, verify they fail (red phase), then emit tests.ready.
Do NOT write implementation code.
implementer:
name: "Implementer"
triggers: ["tests.ready"]
publishes: ["implementation.done"]
instructions: |
You are the Implementer. Your goal is to make the tests pass.
Read the test files to understand what behaviour is expected.
Write the minimum code to make all tests pass (green phase).
Run tests after each change. When all tests pass,
emit implementation.done.
verifier:
name: "Verifier"
triggers: ["implementation.done"]
publishes: ["verify.passed", "verify.tests_needed"]
instructions: |
You are the Verifier. Read PROMPT.md for the full requirements.
Check:
1. All tests pass
2. Test coverage is adequate for the requirements
3. All success criteria from PROMPT.md are met
4. Code is clean (refactor phase if needed)
If complete, print LOOP_COMPLETE.
If more tests are needed, write gaps to .ralph/verification.md
and emit verify.tests_needed.
```
## Backpressure with Hats
Backpressure gates can be applied globally or per-hat:
```yaml
# Global backpressure — applies to all hats
backpressure:
gates:
- name: "tests"
command: "npm test"
on_fail: "retry"
- name: "lint"
command: "npm run lint"
on_fail: "retry"
# Per-hat backpressure
hats:
builder:
triggers: ["plan.ready"]
publishes: ["build.done"]
backpressure:
gates:
- name: "typecheck"
command: "npx tsc --noEmit"
on_fail: "retry"
instructions: |
...
```
## Memories
Hats can use persistent memories stored in `.ralph/agent/memories.md`. These survive across iterations and sessions:
```yaml
hats:
builder:
memory:
path: ".ralph/agent/memories.md"
scope: "hat" # or "global" to share across hats
```
Memories are useful for capturing lessons learned, recording decisions, and avoiding repeated mistakes.
## Running Hat-Based Workflows
```bash
# Run with hats config
ralph run --config hats.yml
# With iteration limit
ralph run --config hats.yml --max-iterations 50
# Resume interrupted session
ralph run --config hats.yml --continue
```
## Anti-Patterns
**Too many hats.** If you have more than 5, you're probably overengineering. Each hat adds coordination overhead.
**Circular event chains without an exit.** Every cycle must have a path to LOOP_COMPLETE. If planner → builder → reviewer → planner, the reviewer must sometimes emit completion instead of always cycling back.
**Hats that duplicate work.** If the builder is also doing planning, your planner hat is wasted.
**Overly prescriptive hat instructions.** The instructions should say WHAT to achieve, not HOW. Let the agent figure out the approach.
**Missing the PROMPT.md reference.** Hat instructions should always tell the agent to read PROMPT.md for the overall task context. Without it, hats lose sight of the bigger picture.
@@ -0,0 +1,167 @@
# Simple Prompt Reference
## Overview
Traditional mode is Ralph at its simplest: a single agent loops against a PROMPT.md until it outputs LOOP_COMPLETE or hits the iteration limit. No hats, no events — just a loop.
This is the right choice for most tasks. Don't reach for hats unless you genuinely need distinct phases with different mindsets.
## ralph.yml Configuration
```yaml
cli:
backend: "claude" # or: kiro, gemini, codex, amp, copilot, opencode
event_loop:
completion_promise: "LOOP_COMPLETE"
max_iterations: 50 # Start conservative, increase if needed
```
### Backend Options
| Backend | CLI Tool | Notes |
|---------|----------|-------|
| claude | Claude Code | Recommended. Best reasoning, large context window |
| kiro | Kiro | AWS-integrated |
| gemini | Gemini CLI | Cost-effective |
| codex | Codex | OpenAI agent |
| amp | Amp | Sourcegraph agent |
| copilot | Copilot CLI | GitHub integrated |
| opencode | OpenCode | Open source |
## PROMPT.md Examples
### Example 1: Build a Feature
```markdown
# Task: Add User Authentication to Express API
Add JWT-based authentication to the existing Express.js API.
## Requirements
- POST /auth/login accepts email + password, returns JWT
- POST /auth/register creates a new user account
- Middleware protects all /users/* routes
- Tokens expire after 24 hours
- Passwords are hashed with bcrypt
## Success Criteria
All of the following must be true:
- [ ] POST /auth/register creates a user and returns 201
- [ ] POST /auth/login returns a valid JWT for correct credentials
- [ ] POST /auth/login returns 401 for incorrect credentials
- [ ] Protected routes return 401 without a valid token
- [ ] Protected routes work normally with a valid token
- [ ] All existing tests still pass
- [ ] New tests cover all auth endpoints
- [ ] TypeScript compiles with zero errors
## Constraints
- Use jsonwebtoken for JWT handling
- Use bcrypt for password hashing
- Follow existing code patterns in src/
- Do not modify existing endpoint behaviour
## Status
Track progress here. When all success criteria are met, print LOOP_COMPLETE.
```
### Example 2: Fix a Bug
```markdown
# Task: Fix Race Condition in WebSocket Handler
The WebSocket message handler has a race condition where concurrent connections
can corrupt shared state. Messages are being delivered to wrong clients.
## Current Behaviour
When 2+ clients send messages simultaneously, responses sometimes go to the
wrong client. See issue #247 for reproduction steps.
## Expected Behaviour
Each client receives only their own responses, regardless of concurrency.
## Success Criteria
- [ ] Concurrent WebSocket test passes (test/ws-concurrent.test.ts)
- [ ] Existing WebSocket tests still pass
- [ ] No shared mutable state between connection handlers
- [ ] Load test with 50 concurrent connections shows zero cross-talk
## Constraints
- Do not change the public WebSocket API
- Fix must work with the existing Redis pub/sub setup
## Status
Track progress here. When all success criteria are met, print LOOP_COMPLETE.
```
### Example 3: Write a Script
```markdown
# Task: CSV Data Migration Script
Create a Python script that migrates data from the legacy CSV format to the
new database schema.
## Requirements
- Read CSV files from data/legacy/*.csv
- Transform fields according to the mapping in docs/migration-map.md
- Insert into PostgreSQL using the existing SQLAlchemy models
- Handle duplicates by updating existing records
- Log all skipped/failed rows to migration_errors.log
## Success Criteria
- [ ] Script processes all CSV files in data/legacy/
- [ ] All valid rows are inserted or updated in the database
- [ ] Duplicate handling works correctly (update, don't duplicate)
- [ ] Error log captures all skipped rows with reasons
- [ ] Script completes without unhandled exceptions
- [ ] Unit tests cover the transformation logic
## Constraints
- Python 3.11+
- Use existing SQLAlchemy models from src/models/
- Must be idempotent (safe to run multiple times)
## Status
Track progress here. When all success criteria are met, print LOOP_COMPLETE.
```
## Running
```bash
# Basic run
ralph run
# With iteration limit
ralph run --max-iterations 30
# Resume an interrupted session
ralph run --continue
# Quiet mode (no TUI)
ralph run -q
```
## When to Upgrade to Hats
If you find the simple prompt struggling because:
- The agent keeps flip-flopping between planning and coding
- It loses track of the overall architecture while implementing details
- It writes code but never stops to review/test properly
- The task is too large for a single coherent prompt
...then consider switching to hat-based mode. But try simplifying the prompt first — often the issue is a vague prompt, not a need for hats.
+13 -7
View File
@@ -1,11 +1,11 @@
# Session Handoff # Session Handoff
_Generated: 2026-02-16 10:43:45 UTC_ _Generated: 2026-02-16 11:04:21 UTC_
## Git Context ## Git Context
- **Branch:** `codex/kpi` - **Branch:** `codex/projects`
- **HEAD:** 24ffe03: chore: auto-commit before merge (loop primary) - **HEAD:** 78e994e: chore: auto-commit before merge (loop primary)
## Tasks ## Tasks
@@ -13,22 +13,28 @@ _Generated: 2026-02-16 10:43:45 UTC_
- [x] Compact Latest Results KPI section - [x] Compact Latest Results KPI section
- [x] Validate KPI objective and close loop - [x] Validate KPI objective and close loop
- [x] Rename Active Projects language to Significant Interventions
- [x] Add autoplay + reduced-motion behavior for carousel
- [x] Responsive polish and full verification for interventions carousel
- [x] Implement Embla carousel in ProjectsTile
- [x] Add autoplay + reduced-motion behavior for carousel
- [x] Responsive polish and full verification for interventions carousel
## Key Files ## Key Files
Recently modified: Recently modified:
- `.ralph/agent/handoff.md`
- `.ralph/agent/memories.md` - `.ralph/agent/memories.md`
- `.ralph/agent/memories.md.lock`
- `.ralph/agent/scratchpad.md` - `.ralph/agent/scratchpad.md`
- `.ralph/agent/summary.md` - `.ralph/agent/summary.md`
- `.ralph/agent/tasks.jsonl` - `.ralph/agent/tasks.jsonl`
- `.ralph/agent/tasks.jsonl.lock`
- `.ralph/current-events` - `.ralph/current-events`
- `.ralph/current-loop-id` - `.ralph/current-loop-id`
- `.ralph/events-20260216-103430.jsonl` - `.ralph/events-20260216-105626.jsonl`
- `.ralph/history.jsonl`
- `.ralph/loop.lock`
- `package-lock.json`
## Next Session ## Next Session
+20
View File
@@ -2,6 +2,22 @@
## Patterns ## Patterns
### mem-1771239841-81ef
> ProjectsTile responsive layout now uses cards-per-view width calc plus flex gap instead of slide padding to prevent overflow/cropping across breakpoints.
<!-- tags: ui, carousel, responsive | created: 2026-02-16 -->
### mem-1771239746-fb8e
> Embla autoplay in ProjectsTile uses playOnInit=false with explicit play/stop tied to prefers-reduced-motion to avoid first-render motion flicker.
<!-- tags: ui, carousel, accessibility | created: 2026-02-16 -->
### mem-1771239639-a457
> ProjectsTile now uses Embla carousel with autoplay disabled under prefers-reduced-motion and preserves project detail panel activation via click/keyboard.
<!-- tags: ui, carousel, accessibility | created: 2026-02-16 -->
### mem-1771239522-007a
> Projects terminology baseline updated: dashboard tile, subnav label, and search palette section now use 'Significant Interventions' instead of 'Active Projects'/'Projects'.
<!-- tags: ui, search, naming | created: 2026-02-16 -->
### mem-1771238197-12d0 ### mem-1771238197-12d0
> Latest Results KPI tile now uses a dedicated responsive grid class: mobile defaults to 1 column and md+ forces 4 columns; coachmark/pulse behavior removed from PatientSummaryTile and related CSS. > Latest Results KPI tile now uses a dedicated responsive grid class: mobile defaults to 1 column and md+ forces 4 columns; coachmark/pulse behavior removed from PatientSummaryTile and related CSS.
<!-- tags: ui, layout, kpi | created: 2026-02-16 --> <!-- tags: ui, layout, kpi | created: 2026-02-16 -->
@@ -10,6 +26,10 @@
## Fixes ## Fixes
### mem-1771239420-0b3f
> failure: cmd=sed -n '1,220p' Ralph/PROMPT.md and sed -n '1,220p' .ralph/agent/scratchpad.md, exit=2, error=path mismatch (Ralph/prompt.md is lowercase) and missing scratchpad file, next=use correct lowercase prompt path and recreate scratchpad before proceeding
<!-- tags: tooling, error-handling, ralph | created: 2026-02-16 -->
### mem-1771238608-ecff ### mem-1771238608-ecff
> failure: cmd=git commit -m 'chore: document KPI objective verification', exit=128, error=.git/index.lock exists due concurrent git operations, next=run git commands sequentially and remove stale lock after confirming no active git process > failure: cmd=git commit -m 'chore: document KPI objective verification', exit=128, error=.git/index.lock exists due concurrent git operations, next=run git commands sequentially and remove stale lock after confirming no active git process
<!-- tags: tooling, error-handling, git | created: 2026-02-16 --> <!-- tags: tooling, error-handling, git | created: 2026-02-16 -->
+97 -12
View File
@@ -1,16 +1,101 @@
## 2026-02-16T10:43:30Z # Scratchpad
Started new loop iteration for `Ralph/PROMPT.md` objective (Latest Results KPI compaction). Reviewed objective, handoff, summary, and current implementation.
Observation: implementation in `src/components/tiles/PatientSummaryTile.tsx` and `src/index.css` already appears to satisfy the requested changes (coachmark removed, helper text moved into header row, responsive 1-column mobile and 4-column md+ grid, compact metric card spacing). ## 2026-02-16T10:57:00Z
Objective loaded from `Ralph/prompt.md`: replace the current Active Projects list with a Significant Interventions Embla carousel with autoplay/reduced-motion handling, preserve panel-open behavior, and update related labels.
Plan for this iteration: Current baseline:
1) Create one runtime task to validate objective state and close loop. - `src/components/tiles/ProjectsTile.tsx` is still a static vertical list with heading `ACTIVE PROJECTS`.
2) Run required verification commands (`npm run typecheck`, `npm run lint`, `npm run build`). - `src/components/SubNav.tsx` still labels the section `Projects`.
3) If checks pass, close task and emit `LOOP_COMPLETE`. - `src/lib/search.ts` still uses palette section label `Active Projects` and section order references.
- Embla dependencies are not present in `package.json`.
Verification run completed in this iteration: Plan for looped atomic execution:
- `npm run typecheck`: pass 1. Create runtime tasks covering naming updates, carousel implementation, and validation.
- `npm run lint`: pass (2 existing warnings in context files only, no errors) 2. Execute one atomic task this iteration: rename user-facing labels from Active Projects/Projects to Significant Interventions in the prompt-specified files (`ProjectsTile`, `SubNav`, `search`).
- `npm run build`: pass (existing bundle-size warning only) 3. Verify with targeted checks (`npm run typecheck` at minimum for this text/code change), commit atomically, close task, and emit completion event for this iteration.
Decision: treat objective as complete because acceptance criteria are already present in source and validation is green. Confidence in this decomposition: 92/100 (high). Proceeding autonomously.
## 2026-02-16T10:59:30Z
Completed task `task-1771239437-05c3`.
Implemented changes:
- `src/components/tiles/ProjectsTile.tsx`: tile header updated to `SIGNIFICANT INTERVENTIONS`.
- `src/components/SubNav.tsx`: section label updated to `Significant Interventions`.
- `src/lib/search.ts`: palette section type/value/order/comments updated from `Active Projects` to `Significant Interventions`.
Verification:
- `npm run lint` passed with existing baseline warnings only (no errors).
- `npm run typecheck` passed.
- `npm run build` passed.
Commit:
- `98d767f``feat: rename Active Projects references to Significant Interventions`.
Remaining open tasks are implementation-focused (`ProjectsTile` Embla carousel + autoplay/responsive polish).
## 2026-02-16T11:00:29Z
Picked ready task `task-1771239437-6f55` (P1): implement Embla carousel in `ProjectsTile` while preserving panel-open behavior.
Implementation completed in one atomic pass:
- Installed dependencies: `embla-carousel-react`, `embla-carousel-autoplay`.
- Refactored `src/components/tiles/ProjectsTile.tsx` from static vertical list to Embla viewport/container/slide structure.
- Added per-slide thumbnail placeholder region (asset-ready later), preserved click + Enter/Space activation path to `openPanel({ type: 'project', investigation: project })`.
- Added autoplay plugin defaults (delay, pause on hover/focus, resume after interaction) and reduced-motion detection via `matchMedia('(prefers-reduced-motion: reduce)')` to disable autoplay.
- Added responsive slide sizing logic: mobile 1-up, tablet 2-up, desktop 3-up.
Verification:
- `npm run lint` passed with pre-existing warnings only.
- `npm run typecheck` passed.
- `npm run build` passed.
Confidence: 93/100. Ready to commit and close task.
## 2026-02-16T11:01:20Z
Task `task-1771239437-6f55` completed and committed as `5fa01b8` (`feat: implement Embla carousel in ProjectsTile`).
Post-close ready queue now exposes follow-up tasks:
- `task-1771239444-811f` (autoplay + reduced-motion behavior tuning)
- `task-1771239444-850d` (responsive polish + full verification)
Stopping after one atomic task per loop contract.
## 2026-02-16T11:02:18Z
Picked ready task `task-1771239444-811f` (P2): autoplay + reduced-motion behavior tuning for the interventions Embla carousel.
Adjustment made in `src/components/tiles/ProjectsTile.tsx`:
- Switched autoplay plugin creation to a stable `useRef` instance.
- Set `playOnInit: false` to prevent motion before user preference is known.
- Initialized `prefersReducedMotion` from `matchMedia('(prefers-reduced-motion: reduce)')` during first render.
- Added effect that explicitly `play()`/`stop()`s autoplay based on current reduced-motion preference once Embla API is available.
Result:
- Carousel autoplay remains enabled by default for motion-allowed users.
- Autoplay is disabled reliably for reduced-motion users, including initial render path.
Verification:
- `npm run lint` passed (existing baseline warnings only in context providers).
- `npm run typecheck` passed.
- `npm run build` passed.
Confidence in task completion: 94/100.
## 2026-02-16T11:08:10Z
Picked ready task `task-1771239444-850d` (P2): responsive polish and full verification for interventions carousel.
Responsive polish implemented in `src/components/tiles/ProjectsTile.tsx`:
- Replaced per-slide right-padding + container negative margin spacing with flex `gap: 12px` to avoid width bleed/cropping at narrow widths.
- Changed slide sizing from raw percent basis to computed width formula using cards-per-view breakpoints:
- mobile: 1 card
- tablet: 2 cards
- desktop: 3 cards
Width expression: `calc((100% - totalGap) / cardsPerView)`.
- Added focus/blur card styling parity with hover styling for keyboard users.
- Tuned Embla options for smoother snap behavior across breakpoints (`dragFree: false`, `slidesToScroll: 1`).
Verification (full):
- `npm run lint` passed (existing baseline warnings only in context provider files).
- `npm run typecheck` passed.
- `npm run build` passed.
Confidence in completion: 95/100.
+3 -3
View File
@@ -1,8 +1,8 @@
# Loop Summary # Loop Summary
**Status:** Completed successfully **Status:** Completed successfully
**Iterations:** 1 **Iterations:** 4
**Duration:** 1m 35s **Duration:** 7m 17s
## Tasks ## Tasks
@@ -14,4 +14,4 @@ _No events recorded._
## Final Commit ## Final Commit
e5c7d9b: chore: document KPI objective verification 68f92fb: feat: polish interventions carousel responsiveness
+6
View File
@@ -1,2 +1,8 @@
{"id":"task-1771238094-7dc9","title":"Compact Latest Results KPI section","description":"Remove coachmark/pulse, move instruction text to heading row right area, enforce 1x4 mobile and 4x1 md+ KPI layout, reduce KPI card whitespace in PatientSummaryTile while preserving content/interactions.","status":"closed","priority":1,"blocked_by":[],"loop_id":"primary-20260216-103430","created":"2026-02-16T10:34:54.490955020+00:00","closed":"2026-02-16T10:36:37.836478822+00:00"} {"id":"task-1771238094-7dc9","title":"Compact Latest Results KPI section","description":"Remove coachmark/pulse, move instruction text to heading row right area, enforce 1x4 mobile and 4x1 md+ KPI layout, reduce KPI card whitespace in PatientSummaryTile while preserving content/interactions.","status":"closed","priority":1,"blocked_by":[],"loop_id":"primary-20260216-103430","created":"2026-02-16T10:34:54.490955020+00:00","closed":"2026-02-16T10:36:37.836478822+00:00"}
{"id":"task-1771238560-5ec5","title":"Validate KPI objective and close loop","description":"Run typecheck/lint/build and confirm Latest Results KPI compaction acceptance criteria remain satisfied before LOOP_COMPLETE event.","status":"closed","priority":1,"blocked_by":[],"loop_id":"primary-20260216-104201","created":"2026-02-16T10:42:40.351948381+00:00","closed":"2026-02-16T10:43:32.976626807+00:00"} {"id":"task-1771238560-5ec5","title":"Validate KPI objective and close loop","description":"Run typecheck/lint/build and confirm Latest Results KPI compaction acceptance criteria remain satisfied before LOOP_COMPLETE event.","status":"closed","priority":1,"blocked_by":[],"loop_id":"primary-20260216-104201","created":"2026-02-16T10:42:40.351948381+00:00","closed":"2026-02-16T10:43:32.976626807+00:00"}
{"id":"task-1771239437-05c3","title":"Rename Active Projects language to Significant Interventions","description":"Update Projects tile heading, SubNav label, and search palette section naming per Ralph prompt.","status":"closed","priority":1,"blocked_by":[],"loop_id":"primary-20260216-105626","created":"2026-02-16T10:57:17.787908637+00:00","closed":"2026-02-16T10:58:29.985946409+00:00"}
{"id":"task-1771239437-64c3","title":"Add autoplay + reduced-motion behavior for carousel","description":"Install Embla autoplay plugin, pause on interaction, disable autoplay for prefers-reduced-motion users.","status":"closed","priority":2,"blocked_by":["task-placeholder"],"loop_id":"primary-20260216-105626","created":"2026-02-16T10:57:17.812228675+00:00","closed":"2026-02-16T10:57:24.084148333+00:00"}
{"id":"task-1771239437-67bc","title":"Responsive polish and full verification for interventions carousel","description":"Tune mobile/desktop viewport behavior and run lint/typecheck/build before closure.","status":"closed","priority":2,"blocked_by":["task-placeholder"],"loop_id":"primary-20260216-105626","created":"2026-02-16T10:57:17.812991662+00:00","closed":"2026-02-16T10:57:24.085921620+00:00"}
{"id":"task-1771239437-6f55","title":"Implement Embla carousel in ProjectsTile","description":"Replace list layout with Embla carousel slides, preserve click/keyboard panel opening, add thumbnail placeholder region.","status":"closed","priority":1,"blocked_by":[],"loop_id":"primary-20260216-105626","created":"2026-02-16T10:57:17.814939655+00:00","closed":"2026-02-16T11:00:49.359576691+00:00"}
{"id":"task-1771239444-811f","title":"Add autoplay + reduced-motion behavior for carousel","description":"Install Embla autoplay plugin, pause on interaction, disable autoplay for prefers-reduced-motion users.","status":"closed","priority":2,"blocked_by":["task-1771239437-6f55"],"loop_id":"primary-20260216-105626","created":"2026-02-16T10:57:24.098597492+00:00","closed":"2026-02-16T11:02:34.691389297+00:00"}
{"id":"task-1771239444-850d","title":"Responsive polish and full verification for interventions carousel","description":"Tune mobile/desktop viewport behavior and run lint/typecheck/build before closure.","status":"closed","priority":2,"blocked_by":["task-1771239437-6f55"],"loop_id":"primary-20260216-105626","created":"2026-02-16T10:57:24.099597797+00:00","closed":"2026-02-16T11:04:10.599296057+00:00"}
+1 -1
View File
@@ -1 +1 @@
.ralph/events-20260216-104201.jsonl .ralph/events-20260216-105626.jsonl
+1 -1
View File
@@ -1 +1 @@
primary-20260216-104201 primary-20260216-105626
+6
View File
@@ -0,0 +1,6 @@
{"ts":"2026-02-16T10:56:26.167394244+00:00","iteration":0,"hat":"loop","topic":"task.start","triggered":"planner","payload":"Ralph/PROMPT.md"}
{"payload":"completed: rename Significant Interventions labels, verify lint/typecheck/build, commit 98d767f; next ready task: task-1771239437-6f55","topic":"task.done","ts":"2026-02-16T10:58:44.650889107+00:00"}
{"payload":"completed: task-1771239437-6f55, commit: 5fa01b8, checks: lint/typecheck/build pass, next ready: task-1771239444-811f","topic":"task.done","ts":"2026-02-16T11:00:56.293728172+00:00"}
{"payload":"completed: task-1771239444-811f, commit: be7a65e, checks: lint/typecheck/build pass, next ready: task-1771239444-850d","topic":"task.done","ts":"2026-02-16T11:02:38.562433372+00:00"}
{"payload":"objective complete: Significant Interventions carousel implemented with autoplay/reduced-motion, responsive polish, and lint/typecheck/build passing","topic":"LOOP_COMPLETE","ts":"2026-02-16T11:04:17.800172602+00:00"}
{"ts":"2026-02-16T11:04:21.786823515+00:00","iteration":4,"hat":"loop","topic":"loop.terminate","payload":"## Reason\ncompleted\n\n## Status\nAll tasks completed successfully.\n\n## Summary\n- Iterations: 4\n- Duration: 7m 17s\n- Exit code: 0"}
+2
View File
@@ -2,3 +2,5 @@
{"ts":"2026-02-16T10:36:47.670503849Z","type":{"kind":"loop_completed","reason":"completion_promise"}} {"ts":"2026-02-16T10:36:47.670503849Z","type":{"kind":"loop_completed","reason":"completion_promise"}}
{"ts":"2026-02-16T10:42:01.215892851Z","type":{"kind":"loop_started","prompt":"Ralph/PROMPT.md"}} {"ts":"2026-02-16T10:42:01.215892851Z","type":{"kind":"loop_started","prompt":"Ralph/PROMPT.md"}}
{"ts":"2026-02-16T10:43:44.925586089Z","type":{"kind":"loop_completed","reason":"completion_promise"}} {"ts":"2026-02-16T10:43:44.925586089Z","type":{"kind":"loop_completed","reason":"completion_promise"}}
{"ts":"2026-02-16T10:56:26.267912429Z","type":{"kind":"loop_started","prompt":"Ralph/PROMPT.md"}}
{"ts":"2026-02-16T11:04:21.788867135Z","type":{"kind":"loop_completed","reason":"completion_promise"}}
+2 -8
View File
@@ -1,11 +1,5 @@
{ {
<<<<<<< HEAD "pid": 892085,
"pid": 864891, "started": "2026-02-16T10:56:26.145878153Z",
"started": "2026-02-16T10:14:58.914587907Z",
"prompt": "[no prompt]"
=======
"pid": 883596,
"started": "2026-02-16T10:42:01.108766214Z",
"prompt": "Ralph/PROMPT.md" "prompt": "Ralph/PROMPT.md"
>>>>>>> codex/kpi
} }
+85
View File
@@ -0,0 +1,85 @@
# Task: Sidebar-First Navigation Refactor (Remove Top Navbar/Subnav)
Refactor the dashboard so navigation is fully sidebar-driven, with clear recruiter-facing labels and robust responsive behavior. The current layout is still tied to an older navbar/subnav model and shows incorrect scroll behavior in the sidebar area.
## Context
Current implementation has separate top navigation (`TopBar`, `SubNav`) and a desktop-only sidebar. On upward scrolling in the sidebar, hidden space becomes visible in a way that implies layered layout offsets from the old top navbar/subnav structure.
## Requirements
- Remove top navbar/subnav from the rendered dashboard flow and migrate section navigation into the sidebar.
- Replace section labels with recruiter-facing content labels (no GP/internal metaphors as labels):
- Overview
- Projects
- Experience
- Education
- Skills
- Keep iconography that can still evoke the GP-system metaphor, but labels must match actual portfolio content.
- Add a `Navigation` subheader area in the sidebar for section links.
- Keep a separate `My Data` area above `Navigation` in expanded sidebar mode.
- Ensure the sidebar no longer reveals hidden spacing/artifacts when scrolling upward.
- Implement mobile sidebar behavior (currently missing):
- Sidebar is collapsed by default.
- A hamburger control appears at the top and toggles expanded/collapsed state.
- In collapsed mode, render a compact vertical rail with:
- hamburger control at the top
- the five section icons directly beneath for one-tap section jumping
- In expanded mode, reveal full sidebar content:
- `My Data` block
- `Navigation` links with icon + text labels
- tags, alerts, and highlights sections
- Preserve or improve accessibility:
- Keyboard operable controls
- Correct `aria-*` labels for menu toggle and navigation regions
- Visible focus states
- Preserve smooth section scrolling/anchor behavior from navigation actions.
## Suggested GP-Metaphor Icon Mapping (labels remain recruiter-facing)
Use these concrete icon targets (or closest equivalents from existing icon library):
- Overview: `UserRound` (profile summary)
- Projects: `Pill` (interventions/medications metaphor)
- Experience: `Workflow` (pathway/Sankey metaphor)
- Education: `GraduationCap` (training/education)
- Skills: `Wrench` (capabilities/tools)
Label text must stay recruiter-facing:
- `Overview`, `Projects`, `Experience`, `Education`, `Skills`
## Likely Files In Scope
- `src/components/DashboardLayout.tsx`
- `src/components/Sidebar.tsx`
- `src/components/SubNav.tsx`
- `src/components/TopBar.tsx`
- `src/index.css`
- Any related hooks/types/styles needed for section activity and responsive state
## Success Criteria
All of the following must be true:
- [ ] No top navbar/subnav is rendered in the final dashboard layout.
- [ ] Sidebar contains the five required recruiter-facing nav labels under a `Navigation` subheader.
- [ ] Expanded sidebar includes a distinct `My Data` area above `Navigation`.
- [ ] Sidebar scrolling no longer exposes hidden top spacing/artifacts when scrolling upward.
- [ ] Desktop navigation from sidebar correctly jumps/scrolls to each section.
- [ ] On mobile, sidebar is collapsed by default with hamburger at top and five icon shortcuts visible.
- [ ] On mobile expand, sidebar shows `My Data`, full navigation links (icon + text), and tags/alerts/highlights.
- [ ] Navigation controls are keyboard accessible with appropriate ARIA semantics.
- [ ] `npm run lint` passes.
- [ ] `npm run typecheck` passes.
- [ ] `npm run build` passes.
## Constraints
- Use the existing project stack and conventions (TypeScript + React + current design language).
- Do not reintroduce GP-style labels like "Significant Interventions" or "Patient Summary" for the sidebar nav text.
- Keep changes focused on layout/navigation behavior; avoid unrelated refactors.
## Status
Track implementation progress in this file or `.ralph/plan.md`.
When all success criteria are met, print LOOP_COMPLETE.
-53
View File
@@ -1,53 +0,0 @@
# Significant Interventions Carousel (Ralph Prompt)
## Goal
Replace the current one-column **Active Projects** list with a **Significant Interventions** carousel that supports thumbnail cards and auto-scroll behavior (Embla-based), while preserving panel-open behavior on card click.
## Scope
- Rename all relevant UI/content references from **Active Projects** to **Significant Interventions**.
- Replace `ProjectsTile` list layout with an Embla carousel.
- Use auto-scroll as the default carousel behavior.
- Keep room for thumbnails now; real thumbnail assets will be added later.
## Implementation Task List
- [ ] Install carousel dependencies:
- `embla-carousel-react`
- `embla-carousel-autoplay`
- [ ] Update tile heading in `src/components/tiles/ProjectsTile.tsx`:
- `ACTIVE PROJECTS` -> `SIGNIFICANT INTERVENTIONS`
- [ ] Refactor `ProjectsTile` in `src/components/tiles/ProjectsTile.tsx`:
- Replace vertical list container with Embla viewport/container/slides
- Convert each project item to a carousel slide card
- Add thumbnail region in each slide (use placeholder block/image container for now)
- Keep keyboard activation (`Enter`/`Space`) and click-to-open detail panel
- [ ] Implement auto-scroll behavior:
- Use Embla autoplay plugin with sensible defaults (continuous feel, pauses on hover/focus)
- Respect reduced motion (`prefers-reduced-motion`) by disabling autoplay
- [ ] Responsive behavior:
- Mobile: single-card view
- Tablet/Desktop: multi-card visible area (based on available width)
- Ensure overflow clipping and smooth transitions
- [ ] Update navigation/search labels to match naming:
- `src/components/SubNav.tsx`: `Projects` -> `Significant Interventions`
- `src/lib/search.ts`: `Active Projects` -> `Significant Interventions` (section type and related labels/comments)
- [ ] Keep detail panel integration unchanged:
- Clicking a carousel card still calls `openPanel({ type: 'project', investigation: project })`
- [ ] Styling pass:
- Align with current dashboard tokens (`--surface`, `--border-light`, `--accent`, etc.)
- Ensure cards remain readable without thumbnails
## Acceptance Criteria
- The dashboard section title displays **Significant Interventions**.
- The old one-column projects list is replaced by a working carousel.
- Carousel auto-scrolls by default and pauses appropriately on interaction.
- In reduced-motion environments, carousel does not auto-scroll.
- Clicking or keyboard-activating a card opens the existing project detail panel.
- Layout works on mobile and desktop without overflow bugs.
- Search/navigation language no longer references **Active Projects**.
## Notes for Implementation
- Thumbnail assets are intentionally deferred; implement with placeholders now.
- Keep the component name `ProjectsTile` for this pass to minimize refactor risk; rename component/file in a later cleanup task if desired.
-120
View File
@@ -1,120 +0,0 @@
# Reference: Task 1 — Design Tokens and Tailwind Config
## Overview
Update the design system from the dark-sidebar NHS Blue palette to the GP System concept's light teal palette. The concept reference is `References/GPSystemconcept.html`.
## CSS Custom Properties (`src/index.css`)
Add/update these variables in the PMR section (keep boot/ECG/login variables unchanged):
```css
/* GP System Dashboard tokens */
--bg: #F0F5F4;
--surface: #FFFFFF;
--sidebar-bg: #F7FAFA;
--text-primary: #1A2B2A;
--text-secondary: #5B7A78;
--text-tertiary: #8DA8A5;
--accent: #0D6E6E;
--accent-hover: #0A8080;
--accent-light: rgba(10,128,128,0.08);
--accent-border: rgba(10,128,128,0.18);
--amber: #D97706;
--amber-light: rgba(217,119,6,0.08);
--amber-border: rgba(217,119,6,0.18);
--success: #059669;
--success-light: rgba(5,150,105,0.08);
--success-border: rgba(5,150,105,0.18);
--alert: #DC2626;
--alert-light: rgba(220,38,38,0.08);
--alert-border: rgba(220,38,38,0.18);
--border: #D4E0DE;
--border-light: #E4EDEB;
--sidebar-width: 272px;
--topbar-height: 48px;
--radius: 8px;
--radius-sm: 6px;
--shadow-sm: 0 1px 2px rgba(26,43,42,0.05);
--shadow-md: 0 2px 8px rgba(26,43,42,0.08);
--shadow-lg: 0 8px 32px rgba(26,43,42,0.12);
--font-body: var(--font-ui);
--font-mono: 'Geist Mono', 'Fira Code', monospace;
```
## Tailwind Config (`tailwind.config.js`)
Update the `extend` section:
### Colors
```js
colors: {
'pmr-bg': '#F0F5F4',
'pmr-surface': '#FFFFFF',
'pmr-sidebar': '#F7FAFA',
'pmr-accent': '#0D6E6E',
'pmr-accent-hover': '#0A8080',
'pmr-text-primary': '#1A2B2A',
'pmr-text-secondary': '#5B7A78',
'pmr-text-tertiary': '#8DA8A5',
'pmr-border': '#D4E0DE',
'pmr-border-light': '#E4EDEB',
'pmr-success': '#059669',
'pmr-amber': '#D97706',
'pmr-alert': '#DC2626',
'pmr-purple': '#7C3AED',
// Keep pmr-nhsblue for backward compat during transition
'pmr-nhsblue': '#005EB8',
// Keep pmr-content as fallback
'pmr-content': '#F0F5F4',
}
```
### Shadows
```js
boxShadow: {
'pmr-sm': '0 1px 2px rgba(26,43,42,0.05)',
'pmr-md': '0 2px 8px rgba(26,43,42,0.08)',
'pmr-lg': '0 8px 32px rgba(26,43,42,0.12)',
// Keep old pmr shadow as alias during transition
'pmr': '0 1px 2px rgba(26,43,42,0.05)',
}
```
### Border Radius
```js
borderRadius: {
'card': '8px', // was 4px — now 8px per concept
'card-sm': '6px', // inner elements
'login': '12px', // login card exception
}
```
## Existing Tokens to Replace/Update
The Tailwind config and CSS already have tokens from the old PMR design. Task 1 needs to UPDATE these, not just add new ones alongside:
**Existing Tailwind shadow tokens (replace with new three-tier system):**
- `pmr`: `'0 1px 2px rgba(0,0,0,0.04), 0 4px 12px rgba(0,0,0,0.03)'` → replace with `pmr-sm`
- `pmr-hover`: `'0 2px 4px rgba(0,0,0,0.06), 0 8px 16px rgba(0,0,0,0.04)'` → replace with `pmr-md`
- `pmr-banner`: `'0 2px 8px rgba(0,0,0,0.12)'` → remove (no banner in new design)
**Existing Tailwind color tokens (keep during transition, Task 21 cleans up):**
- `pmr-nhsblue: '#005EB8'` — keep for login screen (still uses NHS blue)
- `pmr-content: '#F5F7FA'` → update to `pmr-content: '#F0F5F4'` (new bg color)
- `pmr-sidebar: '#1E293B'` → update to `pmr-sidebar: '#F7FAFA'` (light sidebar)
**Existing CSS custom properties (in `--pmr-*` namespace):**
- Previous iterations added `--pmr-*` variables. The new tokens use shorter names (e.g., `--bg`, `--surface`, `--accent`). Add the new tokens AND keep `--pmr-*` aliases during transition so existing components don't break before they're rebuilt.
**Existing border-radius tokens:**
- `card: '4px'` → update to `card: '8px'`
- `login: '12px'` — keep unchanged
## What NOT to Change
- Boot phase variables (`--matrix-*`, `--terminal-*`)
- ECG phase variables
- Login phase background (`#1E293B` — handled by transition)
- Font declarations (Elvaro, Blumir, Geist Mono, Fira Code already set up correctly)
- Breakpoint values
-203
View File
@@ -1,203 +0,0 @@
# Reference: Task 2 — Data Files and Types
## Overview
Create new data files for dashboard-specific content and update the type system. All CV content must match `References/CV_v4.md` exactly.
## New Data Files
### `src/data/profile.ts`
```typescript
export const personalStatement = `Healthcare leader combining clinical pharmacy expertise with proficiency in Python, SQL, and data analytics, self-taught over the past decade through a drive to find root causes in data and build the most efficient solutions to complex problems. Currently leading population health analytics for NHS Norfolk & Waveney ICB, serving a population of 1.2 million. Experienced in working with messy, real-world prescribing data at scale to deliver actionable insights—from financial scenario modelling and pharmaceutical rebate negotiation to algorithm design and population-level pathway development. Proven track record of identifying and prioritising efficiency programmes worth £14.6M+ through automated, data-driven analysis. Skilled at translating complex clinical, financial, and analytical requirements into clear recommendations for executive stakeholders.`
```
### `src/data/tags.ts`
```typescript
import type { Tag } from '@/types/pmr'
export const tags: Tag[] = [
{ label: 'Pharmacist', colorVariant: 'teal' },
{ label: 'Data Lead', colorVariant: 'teal' },
{ label: 'NHS', colorVariant: 'teal' },
{ label: 'Population Health', colorVariant: 'amber' },
{ label: 'BI & Analytics', colorVariant: 'green' },
]
```
### `src/data/alerts.ts`
```typescript
import type { Alert } from '@/types/pmr'
export const alerts: Alert[] = [
{
message: '£14.6M SAVINGS IDENTIFIED',
severity: 'alert',
icon: 'AlertTriangle', // lucide-react icon name
},
{
message: '£220M BUDGET OVERSIGHT',
severity: 'amber',
icon: 'AlertCircle', // lucide-react icon name
},
]
```
### `src/data/kpis.ts`
```typescript
import type { KPI } from '@/types/pmr'
export const kpis: KPI[] = [
{
id: 'budget',
value: '£220M',
label: 'Budget Oversight',
sub: 'NHS prescribing',
colorVariant: 'green',
explanation: 'Managed the ICB\'s total prescribing budget with sophisticated forecasting models identifying cost pressures and enabling proactive financial planning across Norfolk & Waveney.',
},
{
id: 'savings',
value: '£14.6M',
label: 'Efficiency Savings',
sub: 'Identified & tracked',
colorVariant: 'amber',
explanation: 'Identified and prioritised a £14.6M efficiency programme through comprehensive data analysis; achieved over-target performance through targeted, evidence-based interventions across the integrated care system.',
},
{
id: 'years',
value: '9+',
label: 'Years in NHS',
sub: 'Since 2016',
colorVariant: 'teal',
explanation: 'Continuous NHS service since August 2016, progressing from community pharmacy through prescribing data analysis to system-level population health data leadership.',
},
{
id: 'team',
value: '12',
label: 'Team Size Led',
sub: 'Cross-functional',
colorVariant: 'green',
explanation: 'Led a cross-functional team of 12 spanning data analysts, population health specialists, and pharmacists across data, analytics, and population health workstreams.',
},
]
```
### `src/data/skills.ts`
Skills presented as "medications" with frequency (user-specified values) and years of experience.
```typescript
import type { SkillMedication } from '@/types/pmr'
export const skills: SkillMedication[] = [
{
id: 'data-analysis',
name: 'Data Analysis',
frequency: 'Twice daily',
startYear: 2016,
yearsOfExperience: 9,
proficiency: 95,
category: 'Technical',
status: 'Active',
icon: 'BarChart3',
},
{
id: 'python',
name: 'Python',
frequency: 'Daily',
startYear: 2019,
yearsOfExperience: 6,
proficiency: 90,
category: 'Technical',
status: 'Active',
icon: 'Code2',
},
{
id: 'sql',
name: 'SQL',
frequency: 'Daily',
startYear: 2018,
yearsOfExperience: 7,
proficiency: 88,
category: 'Technical',
status: 'Active',
icon: 'Database',
},
{
id: 'power-bi',
name: 'Power BI',
frequency: 'Once weekly',
startYear: 2020,
yearsOfExperience: 5,
proficiency: 92,
category: 'Technical',
status: 'Active',
icon: 'PieChart',
},
{
id: 'javascript-typescript',
name: 'JavaScript / TypeScript',
frequency: 'When required',
startYear: 2022,
yearsOfExperience: 3,
proficiency: 70,
category: 'Technical',
status: 'Active',
icon: 'FileCode2',
},
]
```
Note: Additional domain/leadership skills can be added later. Start with the 5 technical skills the user specified frequencies for.
## Type Updates (`src/types/pmr.ts`)
Add these interfaces (keep all existing types):
```typescript
export interface Tag {
label: string
colorVariant: 'teal' | 'amber' | 'green'
}
export interface Alert {
message: string
severity: 'alert' | 'amber'
icon: string
}
export interface KPI {
id: string
value: string
label: string
sub: string
colorVariant: 'green' | 'amber' | 'teal'
explanation: string
}
export interface SkillMedication {
id: string
name: string
frequency: string
startYear: number
yearsOfExperience: number
proficiency: number
category: 'Technical' | 'Domain' | 'Leadership'
status: 'Active' | 'Historical'
icon: string
}
```
## Existing Data — No Changes
These files remain untouched:
- `src/data/patient.ts`
- `src/data/consultations.ts`
- `src/data/medications.ts`
- `src/data/problems.ts`
- `src/data/investigations.ts`
- `src/data/documents.ts`
-147
View File
@@ -1,147 +0,0 @@
# Reference: Tasks 4-6 — TopBar and Sidebar
## Concept Reference
All specs below are derived from `References/GPSystemconcept.html`. Open it in a browser for visual reference.
---
## Task 4: TopBar Component
### File: `src/components/TopBar.tsx`
### Structure
```
┌─────────────────────────────────────────────────────────────┐
│ [🏠] Headhunt Medical Center Remote │ [🔍 Search... Ctrl+K] │ Dr. A.CHARLWOOD · Active Session · 12:23 [Ctrl+K] │
└─────────────────────────────────────────────────────────────┘
```
### Specs
**Container:**
- `position: fixed`, `top: 0`, `left: 0`, `right: 0`
- `height: var(--topbar-height)` (48px)
- `background: var(--surface)` (#FFFFFF)
- `border-bottom: 1px solid var(--border)` (#D4E0DE)
- `display: flex`, `align-items: center`, `justify-content: space-between`
- `padding: 0 20px`
- `z-index: 100`
**Brand (left):**
- `Home` icon from lucide-react (18px, accent color)
- Text: "Headhunt Medical Center" — 13px, font-ui, 600 weight, text-primary
- Version badge: "Remote" — 11px, 400 weight, text-tertiary, margin-left 2px
**Search bar (center):**
- Wrapper: `max-width: 560px`, `min-width: 400px`
- Container: `height: 42px`, `border: 1.5px solid var(--border)`, `border-radius: var(--radius)` (8px), `padding: 0 14px`, white bg
- Search icon (16px, tertiary) + input + "Ctrl+K" kbd badge
- Input: 13px, font-body, placeholder "Search records, experience, skills... (Ctrl+K)"
- Hover: `border-color: var(--accent-border)`
- Focus: `border-color: var(--accent)`, `box-shadow: 0 0 0 3px rgba(13,110,110,0.12)`
- **On click/focus: opens Command Palette** (Task 18). Does NOT do inline search.
- Kbd badge: mono font, 10px, tertiary, bg: var(--bg), border, padding 2px 6px, radius 4px
**Session info (right):**
- Text: "Dr. A.CHARLWOOD · Active Session · [time]" — 12px, text-secondary
- Session pill: mono 11px, tertiary, `background: var(--accent-light)`, `padding: 3px 10px`, radius 4px, `border: 1px solid var(--accent-border)`
- Ctrl+K shortcut badge (same style as search bar badge)
**Responsive:**
- Mobile (<768px): hide center search bar. Show only brand + session info (or hamburger).
- Tablet: search bar may shrink.
---
## Task 5: Sidebar — PersonHeader
### File: `src/components/Sidebar.tsx`
### Overall Sidebar Container
- `width: var(--sidebar-width)` (272px)
- `min-width: var(--sidebar-width)`
- `background: var(--sidebar-bg)` (#F7FAFA)
- `border-right: 1px solid var(--border)` (#D4E0DE)
- `overflow-y: auto`, custom scrollbar (4px width, transparent track, border-colored thumb)
- `padding: 20px 16px`
- `display: flex`, `flex-direction: column`, `gap: 2px`
### PersonHeader Section
Bordered below: `border-bottom: 2px solid var(--accent)`, `padding-bottom: 16px`, `margin-bottom: 6px`
**Avatar:**
- 52px × 52px circle
- `background: linear-gradient(135deg, var(--accent), #0A8080)`
- White text "AC", 700 weight, 18px, centered
- `box-shadow: 0 2px 8px rgba(13,110,110,0.25)`
- `margin-bottom: 12px`
**Name:**
- "CHARLWOOD, Andrew"
- 15px, 700 weight, text-primary, `letter-spacing: -0.01em`
**Title:**
- "Pharmacy Data Technologist"
- 11.5px, mono font, 400 weight, text-secondary
- `margin-top: 2px`
**Status badge:**
- Inline-flex, gap 5px
- `margin-top: 8px`
- 11px, 500 weight, success color (#059669)
- `background: var(--success-light)`, `border: 1px solid var(--success-border)`
- `padding: 3px 9px`, `border-radius: 20px` (pill)
- Animated dot: 6px circle, success color, `animation: pulse 2s infinite` (opacity 1→0.4→1)
- Text: "Open to Opportunities"
**Details grid:**
- `display: grid`, `grid-template-columns: 1fr`, `gap: 6px`, `margin-top: 12px`
- Each row: `display: flex`, `justify-content: space-between`, `align-items: center`, 11.5px, `padding: 2px 0`
- Label: text-tertiary, 400 weight
- Value: text-primary, 500 weight, text-align right
- GPhC No. value: mono font, 11px, `letter-spacing: 0.12em` → "2211810"
- Education value: "MPharm 2.1 (Hons)"
- Location: "Norwich, Norfolk"
- Phone: link in accent color, `text-decoration: none`, underline on hover → "07795 553 088"
- Email: link → "andy@charlwood.xyz"
- Registered: "August 2016"
**Data source:** `src/data/patient.ts`
---
## Task 6: Sidebar — Tags + Alerts
### Section Title Component
Reusable within sidebar. Used for "Tags", "Alerts / Highlights", and any future sections.
- `font-size: 10px`, `font-weight: 600`, `text-transform: uppercase`, `letter-spacing: 0.08em`
- Color: text-tertiary
- `margin-bottom: 10px`
- Flex row with `::after` pseudo-element: `flex: 1`, `height: 1px`, `background: var(--border-light)`, `gap: 6px`
### Tags Section
- Container: `display: flex`, `flex-wrap: wrap`, `gap: 5px`
- Each tag: 10.5px, 500 weight, `padding: 3px 8px`, `border-radius: 4px`, inline-flex, `line-height: 1.3`
- **Color variants:**
- `teal`: `background: var(--accent-light)`, `color: var(--accent)`, `border: 1px solid var(--accent-border)`
- `amber`: `background: var(--amber-light)`, `color: var(--amber)`, `border: 1px solid var(--amber-border)`
- `green`: `background: var(--success-light)`, `color: var(--success)`, `border: 1px solid var(--success-border)`
- **Data source:** `src/data/tags.ts`
### Alerts / Highlights Section
- Container: `display: flex`, `flex-direction: column`, `gap: 6px`
- Each flag item: `display: flex`, `align-items: center`, `gap: 8px`
- 11px, 700 weight, `padding: 7px 10px`, `border-radius: var(--radius-sm)` (6px), `letter-spacing: 0.02em`
- **Alert variant** (red):
- `background: var(--alert-light)`, `color: var(--alert)`, `border: 1px solid var(--alert-border)`
- Icon: `AlertTriangle` from lucide-react (14px, 2.5 stroke-width)
- **Amber variant:**
- `background: var(--amber-light)`, `color: var(--amber)`, `border: 1px solid var(--amber-border)`
- Icon: `AlertCircle` from lucide-react (14px, 2.5 stroke-width)
- Icon container: 16px square, flex center, flex-shrink-0
- **Data source:** `src/data/alerts.ts`
### Section Padding
Each sidebar section: `padding: 14px 0 6px`
-163
View File
@@ -1,163 +0,0 @@
# Reference: Task 7 — DashboardLayout
## Overview
Create the main layout component that replaces `PMRInterface.tsx`. This is the container that houses TopBar, Sidebar, and the scrollable card grid of tiles.
## File: `src/components/DashboardLayout.tsx`
### Layout Structure
```
┌────────────────────────────────────────────────────┐
│ TopBar (fixed, z-100, height: 48px) │
├──────────┬─────────────────────────────────────────┤
│ │ │
│ Sidebar │ <main> — scrollable card grid │
│ (272px) │ padding: 24px 28px 40px │
│ fixed │ │
│ │ grid: 1fr 1fr, gap: 16px │
│ │ │
│ │ [PatientSummary — full] │
│ │ [LatestResults] [CoreSkills] │
│ │ [LastConsultation — full] │
│ │ [CareerActivity — full] │
│ │ [Education — full] │
│ │ [Projects — full] │
│ │ │
└──────────┴─────────────────────────────────────────┘
```
### CSS Layout
```
.layout {
display: flex;
margin-top: var(--topbar-height); /* 48px */
height: calc(100vh - var(--topbar-height));
}
.sidebar {
/* See ref-03-topbar-sidebar.md for sidebar specs */
width: var(--sidebar-width);
min-width: var(--sidebar-width);
/* ... */
}
.main {
flex: 1;
overflow-y: auto;
padding: 24px 28px 40px;
}
.card-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
@media (max-width: 900px) {
.card-grid {
grid-template-columns: 1fr;
}
}
```
Use Tailwind classes for all of this — the CSS above is for reference only.
### Framer Motion Entrance Animations
Staggered entrance when dashboard first renders (after login):
1. **TopBar**: slides down from `-48px`, 200ms ease-out
2. **Sidebar**: slides from `-272px` left, 250ms ease-out, 50ms delay
3. **Main content**: fades in (opacity 0→1), 300ms, 150ms delay
```typescript
const topbarVariants = {
hidden: { y: -48, opacity: 0 },
visible: { y: 0, opacity: 1, transition: { duration: 0.2, ease: 'easeOut' } }
}
const sidebarVariants = {
hidden: { x: -272, opacity: 0 },
visible: { x: 0, opacity: 1, transition: { duration: 0.25, ease: 'easeOut', delay: 0.05 } }
}
const contentVariants = {
hidden: { opacity: 0 },
visible: { opacity: 1, transition: { duration: 0.3, delay: 0.15 } }
}
```
With `prefers-reduced-motion`: all durations → 0, no delays.
### Tile Ordering in Grid
The card grid renders tiles in this order:
1. `PatientSummaryTile``grid-column: 1 / -1` (full width)
2. `LatestResultsTile` — single column (left)
3. `CoreSkillsTile` — single column (right)
4. `LastConsultationTile``grid-column: 1 / -1` (full width)
5. `CareerActivityTile``grid-column: 1 / -1` (full width)
6. `EducationTile``grid-column: 1 / -1` (full width)
7. `ProjectsTile``grid-column: 1 / -1` (full width)
### App.tsx Wiring
In `src/App.tsx`, the PMR phase currently renders `<PMRInterface />`. Change it to render `<DashboardLayout />`.
```typescript
// In App.tsx phase switch:
case 'pmr':
return <DashboardLayout />
```
Keep all other phases (boot, ecg, login) unchanged. The SkipButton that skips to login should still work.
### Scrollbar Styling
Main content area scrollbar (matches concept):
- Width: 6px
- Track: transparent
- Thumb: var(--border) (#D4E0DE), border-radius 3px
### Command Palette Integration
The DashboardLayout should render the `CommandPalette` component (from Task 18) at the layout level, so it overlays the entire dashboard when triggered. For now (Task 7), just add a placeholder comment or empty div where it will go. The TopBar search bar's click handler should be wired to open the palette (but the palette itself comes in Task 18).
### Background Color Transition
The login screen has background `#1E293B`. The dashboard has background `#F0F5F4`. This transition should happen smoothly. Options:
1. The DashboardLayout entrance animation covers the transition (content fades in over the dark background, replacing it)
2. A brief CSS transition on the body/root background color
3. Handle it in App.tsx with a state-based background
The simplest approach is option 1 — the dashboard's entrance animation effectively replaces the dark login background with the light dashboard.
---
## Established Patterns (from previous iterations)
These patterns were established across 16 iterations of the old PMR build. Reuse them:
### Phase name is `'pmr'`
The Phase type in `src/types/index.ts` is `'boot' | 'ecg' | 'login' | 'pmr'`. The `'pmr'` case renders the dashboard. Do NOT rename the phase — just change what it renders.
### Module-scope `prefersReducedMotion`
All animation components should compute this once at module level, not per render:
```typescript
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
```
This is the established pattern across all existing view components.
### Pre-existing ESLint warning
`AccessibilityContext.tsx` has 1 pre-existing ESLint warning. This is expected — do not attempt to fix it. Quality checks pass with this warning present.
### Callback ref pattern for Framer Motion
If you need a ref to a `motion.*` element (e.g., for scroll detection), use `useState` + callback ref instead of `useRef`. Framer Motion elements may not be in the DOM when `useEffect` first runs:
```typescript
const [scrollContainer, setScrollContainer] = useState<HTMLElement | null>(null)
// On the element: ref={el => { if (el) setScrollContainer(el) }}
```
This avoids null ref issues with animated mount timing.
-144
View File
@@ -1,144 +0,0 @@
# Reference: Tasks 8-11 — Card Component and Top Tiles
## Task 8: Reusable Card Component
### File: `src/components/Card.tsx`
### Base Card
```typescript
interface CardProps {
children: React.ReactNode
full?: boolean // spans both grid columns
className?: string
}
```
**Styling:**
- `background: var(--surface)` (#FFFFFF)
- `border: 1px solid var(--border-light)` (#E4EDEB)
- `border-radius: var(--radius)` (8px)
- `padding: 20px`
- `box-shadow: var(--shadow-sm)` (0 1px 2px rgba(26,43,42,0.05))
- Hover: `box-shadow: var(--shadow-md)`, `border-color: var(--border)` (#D4E0DE)
- `transition: box-shadow 0.2s, border-color 0.2s`
- Full variant: `grid-column: 1 / -1`
### CardHeader Sub-component
```typescript
interface CardHeaderProps {
dotColor: 'teal' | 'amber' | 'green' | 'alert' | 'purple'
title: string
rightText?: string
}
```
**Styling:**
- `display: flex`, `align-items: center`, `gap: 8px`, `margin-bottom: 16px`
- Dot: 8px circle, `border-radius: 50%`, flex-shrink-0
- teal: `#0D6E6E`, amber: `#D97706`, green: `#059669`, alert: `#DC2626`, purple: `#7C3AED`
- Title: 12px, 600 weight, uppercase, `letter-spacing: 0.06em`, text-secondary (#5B7A78)
- Right text (optional): 10px, 400 weight, normal case, no tracking, text-tertiary, mono font, `margin-left: auto`
---
## Task 9: PatientSummary Tile
### File: `src/components/tiles/PatientSummaryTile.tsx`
**Layout:** Full-width card, first in grid.
**Content:**
- CardHeader: teal dot + "PATIENT SUMMARY"
- Body: personal statement text from `src/data/profile.ts`
- Typography: 13px, font-ui, `line-height: 1.6` (leading-relaxed), text-primary
- No interactive elements — read-only
**Data:** `import { personalStatement } from '@/data/profile'`
This is a simple tile. No expansion, no interactivity.
---
## Task 10: LatestResults Tile
### File: `src/components/tiles/LatestResultsTile.tsx`
**Layout:** Half-width card (single grid column). Sits in the LEFT column.
**Content:**
- CardHeader: teal dot + "LATEST RESULTS" + right text "Updated May 2025"
- 2×2 metric grid inside
**Metric Grid:**
- `display: grid`, `grid-template-columns: 1fr 1fr`, `gap: 12px`
**Each Metric Card:**
- `padding: 14px`, `border-radius: var(--radius-sm)` (6px)
- `border: 1px solid var(--border-light)`, `background: var(--bg)` (#F0F5F4)
- Value: 22px, 700 weight, `letter-spacing: -0.02em`, `line-height: 1.2`
- Color by variant: green=#059669, amber=#D97706, teal=#0D6E6E
- Label: 11px, text-secondary, 500 weight, `margin-top: 3px`
- Sub: 10px, text-tertiary, mono font, `margin-top: 4px`
**Data:** `import { kpis } from '@/data/kpis'`
**KPI flip prep:** Each metric card should accept a `data-kpi-id` or an `onClick` prop placeholder — Task 17 will add the flip interaction. For now, render as static display.
**Values:**
| Value | Label | Sub | Color |
|-------|-------|-----|-------|
| £220M | Budget Oversight | NHS prescribing | green |
| £14.6M | Efficiency Savings | Identified & tracked | amber |
| 9+ | Years in NHS | Since 2016 | teal |
| 12 | Team Size Led | Cross-functional | green |
---
## Task 11: CoreSkills Tile ("Repeat Medications")
### File: `src/components/tiles/CoreSkillsTile.tsx`
**Layout:** Half-width card (single grid column). Sits in the RIGHT column, next to LatestResults.
**Content:**
- CardHeader: amber dot + "REPEAT MEDICATIONS"
- Vertical list of skill items, `gap: 10px`
**Each Skill Item:**
Matches the concept's `.dev-item` pattern:
- `display: flex`, `align-items: center`, `gap: 10px`
- 12.5px font, `padding: 10px 12px`
- `background: var(--bg)` (#F0F5F4), `border-radius: var(--radius-sm)` (6px)
- `border: 1px solid var(--border-light)`
**Item structure:**
- **Icon container** (28px square, 6px radius):
- `background: var(--accent-light)`, `color: var(--accent)` (teal)
- Lucide icon inside (14px): `BarChart3` for Data Analysis, `Code2` for Python, `Database` for SQL, `PieChart` for Power BI, `FileCode2` for JS/TS
- **Text block** (flex: 1):
- Name: 600 weight, text-primary (e.g., "Data Analysis")
- Frequency + years: 11px, text-tertiary, mono font (e.g., "Twice daily · Since 2016 · 9 yrs")
- **Optional status badge**: 10px, 500 weight, pill shape (padding 3px 8px, border-radius 20px), flex-shrink-0
- Could show proficiency or "Active" status
**Medication metaphor format:**
```
[📊] Data Analysis Active
Twice daily · Since 2016 · 9 yrs
[💻] Python Active
Daily · Since 2019 · 6 yrs
[🗄️] SQL Active
Daily · Since 2018 · 7 yrs
[📈] Power BI Active
Once weekly · Since 2020 · 5 yrs
[📝] JavaScript / TypeScript Active
When required · Since 2022 · 3 yrs
```
**Data:** `import { skills } from '@/data/skills'`
**Expansion prep:** Each item should accept an onClick prop placeholder — Task 16 will add expansion to show prescribing history (from existing medications data).
-204
View File
@@ -1,204 +0,0 @@
# Reference: Tasks 12-15 — Bottom Tiles
## Task 12: LastConsultation Tile
### File: `src/components/tiles/LastConsultationTile.tsx`
**Layout:** Full-width card.
**Content:**
- CardHeader: green dot + "LAST CONSULTATION" + right text "Most recent role"
**Header info row:**
- `display: flex`, `flex-wrap: wrap`, `gap: 16px`
- `margin-bottom: 14px`, `padding-bottom: 14px`, `border-bottom: 1px solid var(--border-light)`
- Each field:
- Label: 10px, uppercase, `letter-spacing: 0.06em`, text-tertiary
- Value: 11.5px, 600 weight, text-primary
| Label | Value |
|-------|-------|
| Date | May 2025 |
| Organisation | NHS Norfolk & Waveney ICB |
| Type | Permanent · Full-time |
| Band | 8a |
**Role title:**
- "Interim Head, Population Health & Data Analysis"
- 13.5px, 600 weight, `color: var(--accent)` (#0D6E6E)
- `margin-bottom: 12px`
**Bullet list:**
- `list-style: none`, flex column, `gap: 7px`
- Each bullet: 12.5px, text-primary, `padding-left: 16px`, `line-height: 1.5`
- Pseudo `::before`: 5px circle, accent color (#0D6E6E), `opacity: 0.5`, positioned left at top 7px
**Bullets** (from first consultation's examination array):
- Led a cross-functional team of 12 across data, analytics, and population health workstreams
- Oversaw £220M prescribing budget with full analytical accountability and reporting to ICB board
- Identified £14.6M in efficiency savings through data-driven prescribing interventions
- Designed and deployed Power BI dashboards used by 200+ clinicians and commissioners
- Spearheaded SQL analytics transformation, migrating legacy Access databases to modern data stack
- Established team data literacy programme, upskilling 30+ non-technical staff in data interpretation
**Data:** `import { consultations } from '@/data/consultations'` — use `consultations[0]` (the most recent).
Map consultation fields:
- date → Date field
- organization → Organisation field
- role → Role title
- examination array → Bullet points
---
## Task 13: CareerActivity Tile
### File: `src/components/tiles/CareerActivityTile.tsx`
**Layout:** Full-width card.
**Content:**
- CardHeader: teal dot + "CAREER ACTIVITY" + right text "Full timeline"
**Activity grid:**
- `display: grid`, `grid-template-columns: 1fr 1fr`, `gap: 10px`
- Below 900px: `grid-template-columns: 1fr` (single column)
**Each activity item:**
- `display: flex`, `gap: 10px`
- `padding: 10px 12px`
- `background: var(--bg)` (#F0F5F4)
- `border-radius: var(--radius-sm)` (6px)
- `border: 1px solid var(--border-light)`
- 12px font
- `transition: border-color 0.15s`
- Hover: `border-color: var(--accent-border)`
**Dot (left):**
- 8px circle, flex-shrink-0, `margin-top: 2px` (aligns with text)
- Color by type:
- Role: teal (#0D6E6E)
- Project: amber (#D97706)
- Certification: green (#059669)
- Education: purple (#7C3AED)
**Content (right):**
- Title: 600 weight, text-primary, `line-height: 1.3`
- Meta: 11px, text-secondary, `margin-top: 2px`
- Date: 10px, mono font, text-tertiary, `margin-top: 3px`
**Building the timeline data:**
Merge entries from multiple data sources, sorted newest-first:
```typescript
type ActivityType = 'role' | 'project' | 'cert' | 'edu'
interface ActivityEntry {
id: string
type: ActivityType
title: string
meta: string
date: string
sortYear: number // for sorting
}
```
Sources:
1. `consultations` → type "role": title=role, meta=organization, date=duration
2. `investigations` (selected key ones) → type "project": title=name, meta=short description, date=year
3. `documents` where type='Certificate' → type "cert": title=title, meta=source, date=date
4. `documents` where type='Results' (MPharm) → type "edu": title=title, meta=source, date=date
Match the concept HTML entries:
| Type | Title | Meta | Date |
|------|-------|------|------|
| role | Interim Head, Population Health & Data Analysis | NHS Norfolk & Waveney ICB | 2024 2025 |
| project | £220M Prescribing Budget Oversight | Lead analyst & budget owner | 2024 |
| role | Senior Data Analyst — Medicines Optimisation | NHS Norfolk & Waveney ICB | 2021 2024 |
| project | SQL Analytics Transformation | Legacy migration project lead | 2025 |
| cert | Power BI Data Analyst Associate | Microsoft Certified | 2023 |
| role | Prescribing Data Pharmacist | NHS Norwich CCG | 2018 2021 |
| cert | Clinical Pharmacy Diploma | Professional development | 2019 |
| role | Community Pharmacist | Boots UK | 2016 2018 |
| edu | MPharm (Hons) — 2:1 | University of East Anglia | 2011 2015 |
| cert | GPhC Registration | General Pharmaceutical Council | August 2016 |
**Expansion prep:** Activity items should accept onClick for Task 16 (expand to show full role/project detail).
---
## Task 14: Education Tile
### File: `src/components/tiles/EducationTile.tsx`
**Layout:** Full-width card, below Career Activity.
**Content:**
- CardHeader: purple dot (#7C3AED) + "EDUCATION"
**Education entries:**
Vertical stack of education items.
Each item:
- `padding: 7px 10px`
- `background: var(--surface)` (#FFFFFF)
- `border: 1px solid var(--border-light)`
- `border-radius: var(--radius-sm)` (6px)
- 11.5px, text-primary
Structure:
- Degree name: 600 weight, `display: block`
- Detail: text-secondary, 11px, `margin-top: 2px`
**Entries** (from CV):
| Degree | Detail |
|--------|--------|
| MPharm (Hons) — 2:1 | University of East Anglia · 2015 |
| NHS Leadership Academy — Mary Seacole Programme | 2018 · 78% |
| A-Levels: Mathematics (A*), Chemistry (B), Politics (C) | Highworth Grammar School · 20092011 |
**Data:** Filter `src/data/documents.ts` for education entries, or hardcode from CV since the documents data may not have all education entries.
Note: The concept HTML only shows the MPharm entry. But the CV has more education. Include all CV education entries.
---
## Task 15: Projects Tile
### File: `src/components/tiles/ProjectsTile.tsx`
**Layout:** Full-width card, prominent position.
**Content:**
- CardHeader: amber dot + "ACTIVE PROJECTS"
**Project entries:**
Vertical list, styled as interactive items.
Each project:
- `display: flex`, `align-items: flex-start`, `gap: 8px`
- `padding: 7px 10px`
- `background: var(--surface)`, `border: 1px solid var(--border-light)`
- `border-radius: var(--radius-sm)` (6px)
- 11.5px, text-primary
- Hover: `border-color: var(--accent-border)`
- `transition: border-color 0.15s`
Structure:
- **Status dot** (7px circle, flex-shrink-0, `margin-top: 4px`):
- Complete: success (#059669)
- Ongoing: accent (#0D6E6E)
- Live: success with pulse animation
- **Project name**: text-primary, flex 1
- **Year badge**: 10px, mono font, text-tertiary, `margin-left: auto`, flex-shrink-0
**Data:** `import { investigations } from '@/data/investigations'`
Map investigations to projects:
- name → Project name
- status → dot color
- requestedYear → Year badge
- resultSummary → Available for expansion (Task 16)
**Expansion prep:** Each item should accept onClick for Task 16 (expand to show methodology, tech stack, results).
-259
View File
@@ -1,259 +0,0 @@
# Reference: Tasks 16-18 — Interactions
## Task 16: Tile Expansion System
### Overview
Three tiles have expandable items: CareerActivity (roles), Projects, and CoreSkills. Clicking an item expands it in-place to reveal detail, like expanding a clinical record entry.
### Expansion Pattern (consistent across all tiles)
**Animation:**
- Framer Motion `AnimatePresence` + `motion.div`
- Height-only animation: 200ms, ease-out
- **No opacity fade on content** (guardrail)
- `overflow: hidden` on the animated container
```typescript
<AnimatePresence initial={false}>
{isExpanded && (
<motion.div
initial={{ height: 0 }}
animate={{ height: 'auto' }}
exit={{ height: 0 }}
transition={prefersReducedMotion ? { duration: 0 } : { duration: 0.2, ease: 'easeOut' }}
style={{ overflow: 'hidden' }}
>
{/* expanded content */}
</motion.div>
)}
</AnimatePresence>
```
**Behavior:**
- Single-expand accordion: only one item expanded at a time within each tile
- Click expanded item again to collapse
- Click different item: collapses current, expands new
- State: `expandedItemId: string | null` in each tile component
**Keyboard:**
- Enter/Space: toggle expand/collapse
- Escape: collapse current item
- `aria-expanded` on each clickable item
**Visual:**
- Expanded content has slightly different background (`var(--bg)` or subtle border-left)
- Colored left border on expanded panel (accent color for roles, amber for projects, teal for skills)
- Content padding: 12-16px
### CareerActivity Expansion (roles)
When a role-type activity item is expanded:
- Show full role details from corresponding consultation entry
- Structure: role title, organization, date range
- Achievement bullets (examination array from consultation)
- Coded entries if available
- Match expanded content to `consultations` data by mapping activity item to consultation
### Projects Expansion
When a project item is expanded:
- Show from investigation data:
- Methodology
- Tech stack (as tags or inline list)
- Results (bulleted)
- External URL link if available ("View Results" button)
### CoreSkills Expansion
When a skill item is expanded:
- Show "prescribing history" — a timeline of skill development
- **Data source:** `import { medications } from '@/data/medications'` (NOT `skills.ts`). The `medications.ts` file has 18 entries, each with a `prescribingHistory` array of `{ year, description }` entries. Map from `skills.ts` to `medications.ts` by matching skill name to medication name (e.g., "Data Analysis" in skills.ts → find the medication with `name: "Data Analysis"` in medications.ts to get its `prescribingHistory`).
- Format: vertical timeline with year markers and descriptions
- Timeline dots: accent color, 6px, with connecting line
- Year: mono font, 12px, semibold
- Description: 12px, regular
---
## Task 17: KPI Flip Cards
### Overview
In the LatestResults tile, each metric card can be clicked to "flip" and reveal an explanation of that KPI.
### Flip Animation
**CSS Perspective approach:**
```css
.metric-card {
perspective: 1000px;
cursor: pointer;
}
.metric-card-inner {
transition: transform 0.4s ease-in-out;
transform-style: preserve-3d;
position: relative;
}
.metric-card-inner.flipped {
transform: rotateY(180deg);
}
.metric-card-front,
.metric-card-back {
backface-visibility: hidden;
position: absolute;
inset: 0;
}
.metric-card-back {
transform: rotateY(180deg);
}
```
Or use Framer Motion `animate={{ rotateY: isFlipped ? 180 : 0 }}` with `perspective` on parent.
**Behavior:**
- Click to flip front → back
- Click again to flip back → front
- Only one card flipped at a time (clicking another card flips the current one back)
- State: `flippedCardId: string | null` in LatestResultsTile
**Front face:** Current metric display (value + label + sub) — same as Task 10.
**Back face:**
- `background: var(--accent-light)` (subtle teal tint)
- `padding: 14px`
- Text: 12px, text-secondary, `line-height: 1.5`
- The explanation text from KPI data's `explanation` field
**Reduced motion:**
- No 3D flip animation
- Instant content swap (front → back)
- Could use a simple crossfade or just replace content immediately
**Keyboard:**
- Enter/Space to flip
- Each metric card should be `tabIndex={0}` with appropriate `aria-label`
**KPI Explanations** (from `src/data/kpis.ts`):
- £220M: Budget management with forecasting models
- £14.6M: Efficiency programme through data analysis
- 9+ Years: NHS service progression since 2016
- 12: Cross-functional team leadership
---
## Task 18: Command Palette
### File: `src/components/CommandPalette.tsx`
### Trigger
- **Ctrl+K** (global `keydown` listener on `document`)
- **Click** on TopBar search bar (or focus on search input)
- The TopBar search input does NOT do inline search — it opens the palette
### Overlay
- `position: fixed`, `inset: 0`
- `background: rgba(26,43,42,0.45)`
- `backdrop-filter: blur(4px)`
- `z-index: 1000`
- Fade in: `opacity: 0 → 1`, `visibility: hidden → visible`, 200ms transition
- Click overlay (outside modal) to close
### Palette Modal
- `width: 580px`, `max-height: 520px`
- `background: var(--surface)` (#FFFFFF)
- `border-radius: 12px`
- `box-shadow: 0 20px 60px rgba(26,43,42,0.2), 0 0 0 1px rgba(26,43,42,0.08)`
- `overflow: hidden`
- Entrance: `transform: scale(0.97) translateY(-8px)``scale(1) translateY(0)`, 200ms cubic-bezier
### Search Input
- Flex row: search icon (18px, accent) + input + "ESC" hint badge
- `padding: 14px 18px`, `border-bottom: 1px solid var(--border-light)`
- Input: 15px, font-body, placeholder "Search records, experience, skills..."
- ESC badge: mono 10px, tertiary, bg var(--bg), border, padding 2px 7px, radius 4px
### Results Area
- `overflow-y: auto`, `padding: 8px`, `flex: 1`
- Custom scrollbar (4px)
### Result Sections
Section label: 10px, 600 weight, uppercase, `letter-spacing: 0.08em`, text-tertiary, `padding: 8px 10px 5px`
### Result Items
- `display: flex`, `align-items: center`, `gap: 10px`
- `padding: 9px 10px`, `border-radius: var(--radius-sm)` (6px)
- `cursor: pointer`, `transition: background 0.1s`
- 13px, text-primary
- Hover/selected: `background: var(--accent-light)`
- Selected also gets: `outline: 1.5px solid var(--accent-border)`
**Item structure:**
- Icon container: 28px square, 6px radius, colored bg per section
- Experience: teal
- Core Skills: green
- Active Projects: amber
- Achievements: amber
- Education: purple
- Quick Actions: teal
- Text: title (500 weight) + subtitle (11px, tertiary, truncated)
- Optional badge: 10px, mono, tertiary
### Fuzzy Search
Adapt existing `src/lib/search.ts` (fuse.js v7.0.0, already installed):
**Existing code:** `src/lib/search.ts` has `buildSearchIndex()` which creates a Fuse index from consultations, medications, problems, investigations, and documents. It groups results by `sectionLabel` via `groupResultsBySection()`. The `SearchResult` interface has `{ id, title, section: ViewId, sectionLabel, highlight }`.
**What needs changing:**
- The `section: ViewId` field is designed for view-switching navigation (navigating to `#consultations`, `#medications`, etc.). The new dashboard has no views — it's a single scrollable page. Results should either scroll to the relevant tile or expand an item within a tile.
- Add `skills.ts` data to the index (currently only `medications.ts` is indexed, not the new 5-skill entries)
- Add `kpis.ts` data to the index
- Add Quick Actions (Download CV, Send Email, View LinkedIn, View Projects)
- Update section labels to match palette grouping: "Experience", "Core Skills", "Active Projects", "Achievements", "Education", "Quick Actions"
- Add an `action` field to `SearchResult` so each result knows what to do when selected (scroll to tile, expand item, open link, etc.)
**Config (keep existing):**
- `threshold: 0.3`, weighted keys (title: 2, content: 1)
- `minMatchCharLength: 2`
- Group results by section
- Highlight matching text in titles using `<mark>` with accent-light background
### Keyboard Navigation
- **Arrow Down/Up**: move selection through results
- **Enter**: select highlighted result (navigate to section or trigger action)
- **Escape**: close palette
- `selectedIndex` state tracks which result is highlighted
- Auto-scroll highlighted result into view
### Quick Actions Section
| Title | Subtitle | Action |
|-------|----------|--------|
| Download CV | Export as PDF | Trigger download |
| Send Email | andy@charlwood.xyz | `mailto:` link |
| View LinkedIn | Professional profile | External link |
| View Projects | GitHub & portfolio | External link |
### Footer
- `display: flex`, `gap: 12px`
- `padding: 10px 18px`, `border-top: 1px solid var(--border-light)`
- 11px, text-tertiary
- Keyboard hints: `↑ ↓ Navigate`, `Enter Select`, `Esc Close`
- Each key in `<kbd>` styled element
### Reduced Motion
- No scale/translate entrance animation
- Instant show/hide (opacity only, or immediate)
### State Management
```typescript
const [isOpen, setIsOpen] = useState(false)
const [query, setQuery] = useState('')
const [selectedIndex, setSelectedIndex] = useState(-1)
```
Render the palette at the DashboardLayout level so it overlays everything.
-164
View File
@@ -1,164 +0,0 @@
# Reference: Tasks 19-21 — Polish
## Task 19: Responsive Design
### Desktop (>1024px)
- Full sidebar (272px) + TopBar + 2-column card grid
- All tiles at full spec (as designed in Tasks 8-15)
- Command palette at 580px width
### Tablet (7681024px)
- Sidebar: collapse to icon-only (56px) or hide entirely with toggle
- TopBar: full, but search bar may shrink (reduce min-width)
- Card grid: can stay 2-column if space permits, or switch to 1-column
- Activity grid inside CareerActivity tile: switch to 1-column
### Mobile (<768px)
- Sidebar: hidden entirely (off-canvas or removed)
- TopBar: simplified — brand text may truncate, hide search bar center section
- Navigation: consider a hamburger menu or bottom nav for key actions
- Card grid: single column
- All tiles stack vertically (full-width)
- Metric grid in LatestResults: stays 2x2 (compact enough)
- Activity grid in CareerActivity: single column
- Touch targets: all clickable elements 48px+ minimum
- Command palette: full-width with reduced padding
### Breakpoint Strategy
Use Tailwind responsive prefixes:
- `lg:` for desktop (>1024px)
- `md:` for tablet (>768px)
- Default styles for mobile-first
### Key responsive classes:
```
/* Card grid */
grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-[16px]
/* Sidebar visibility */
hidden lg:flex lg:flex-col
/* TopBar search */
hidden md:block
/* Activity grid */
grid grid-cols-1 md:grid-cols-2
/* Sidebar width */
lg:w-[272px] lg:min-w-[272px]
```
---
## Task 20: Accessibility Audit
### Semantic HTML
| Element | Tag | Notes |
|---------|-----|-------|
| TopBar | `<header>` | Fixed at top |
| Sidebar | `<aside>` or `<nav>` | Navigation/info panel |
| Main content | `<main>` | Card grid container |
| Individual tiles | `<article>` | Self-contained content sections |
| Tile sections | `<section>` | Within tiles (e.g., metric grid, bullet list) |
| Command palette | `<dialog>` or `div role="dialog"` | Modal overlay |
### Keyboard Navigation
| Key | Action |
|-----|--------|
| Tab | Move between interactive elements (tiles, buttons, links) |
| Enter/Space | Expand tile items, flip KPI cards, select palette results |
| Escape | Close expanded items, close command palette |
| Ctrl+K | Open command palette |
| Arrow Up/Down | Navigate command palette results |
### ARIA Attributes
- **Command palette search**: `role="combobox"`, `aria-expanded`, `aria-controls="palette-results"`, `aria-autocomplete="list"`
- **Palette results**: `role="listbox"`, each result `role="option"`
- **Palette overlay**: `role="dialog"`, `aria-modal="true"`, `aria-label="Search records"`
- **Expandable items**: `aria-expanded="true|false"` on trigger element
- **KPI flip cards**: `aria-label` describing front/back content, `role="button"`, `tabIndex={0}`
- **Status dots with text**: text labels present → dot can be `aria-hidden="true"`
- **Alert flags**: `role="status"` or decorative (visible text is sufficient)
- **Live region**: When palette opens/closes, announce via `aria-live="polite"` region
- **TopBar session info**: `aria-label="Active session information"`
### Focus Management
- **Command palette**: focus trap when open. Focus moves to search input on open. Returns to trigger element on close.
- **Focus visible**: `focus-visible:ring-2 focus-visible:ring-[var(--accent)]/40` on all interactive elements (buttons, links, expandable items, KPI cards)
- **Skip to content**: Optional "Skip to main content" link (only visible on focus)
- **After tile expansion**: focus should remain on the trigger or move into expanded content
### `prefers-reduced-motion`
Every animation must check:
```typescript
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
```
| Animation | Reduced Motion Behavior |
|-----------|------------------------|
| Dashboard entrance (topbar/sidebar/content) | Instant, no slide/fade |
| Tile expansion | Instant height change (duration: 0) |
| KPI flip | Instant content swap (no rotateY) |
| Palette entrance | Instant show (no scale/translate) |
| Status badge pulse | No animation |
| Hover transitions | Can keep (very brief) or disable |
### Color Contrast Verification
| Foreground | Background | Expected Ratio | Meets AA? |
|------------|-----------|-----------------|-----------|
| #0D6E6E (accent) | #FFFFFF (white) | ~5.5:1 | Yes |
| #1A2B2A (primary) | #FFFFFF | ~15:1 | Yes |
| #5B7A78 (secondary) | #FFFFFF | ~4.6:1 | Borderline — verify |
| #8DA8A5 (tertiary) | #FFFFFF | ~3.0:1 | Fails for body text — use only for decorative/supplementary |
| #0D6E6E (accent) | #F0F5F4 (bg) | ~4.8:1 | Yes for large text |
**Important:** Tertiary text (#8DA8A5) does NOT meet AA for body text. Use only for supplementary labels, dates, and decorative text where the information is also conveyed elsewhere (e.g., a date that's also in the title). For standalone readable text, use secondary (#5B7A78) or primary (#1A2B2A).
---
## Task 21: Clean Up and Final Polish
### Components to Remove (only after confirming unused)
- `src/components/PatientBanner.tsx` — replaced by TopBar
- `src/components/ClinicalSidebar.tsx` — replaced by Sidebar
- `src/components/Breadcrumb.tsx` — no longer needed (no view switching)
- `src/components/MobileBottomNav.tsx` — may be replaced or redesigned
- `src/components/PMRInterface.tsx` — replaced by DashboardLayout
### Views to Assess
The `src/components/views/` directory contains the old view components. Some may be reusable:
- **ConsultationsView.tsx**: Expanded entry rendering could be reused in CareerActivity expansion (Task 16). Check before removing.
- **MedicationsView.tsx**: Prescribing history rendering could be reused in CoreSkills expansion. Check before removing.
- **Other views**: If expansion (Task 16) doesn't reuse them, they can be removed.
**Rule: Only remove files that are confirmed unused.** Run a grep for imports before deleting.
### Hooks to Assess
- `src/hooks/useScrollCondensation.ts` — only used by PatientBanner. If PatientBanner is removed, this can go too.
- `src/hooks/useBreakpoint.ts` — may still be useful for responsive tile layouts. Check if any new dashboard component uses it. If not, remove.
### Context to Simplify
- `src/contexts/AccessibilityContext.tsx` — the existing context has `activeView`, `setActiveView`, `expandedItemId`, `setExpandedItem` designed for the old view-switching navigation. With the new single-page dashboard:
- `activeView` / `setActiveView` are no longer relevant (no view switching)
- `expandedItemId` / `setExpandedItem` may still be useful if tiles report their expanded item for accessibility announcements
- Assess whether to simplify the context or remove it entirely and manage expansion state locally in each tile
- **Note:** This context has 1 pre-existing ESLint warning — that's expected.
### Verification Checklist
- [ ] No dead imports (run `npm run lint` — ESLint catches unused imports)
- [ ] No TypeScript errors (`npm run typecheck`)
- [ ] Clean build (`npm run build`)
- [ ] Bundle size reasonable (should be similar to or smaller than current ~417KB)
- [ ] No console errors in dev mode
### Final Visual Review
Open `http://localhost:5173` and compare against `References/GPSystemconcept.html`:
- [ ] TopBar layout matches (brand, search, session)
- [ ] Sidebar matches (person header, tags, alerts)
- [ ] Card grid layout (2-column, full-width tiles span both)
- [ ] Each tile's visual treatment matches concept
- [ ] Shadows, borders, radius consistent
- [ ] Typography: Elvaro Grotesque (not DM Sans)
- [ ] Colors: teal accent (not NHS Blue)
- [ ] Hover states work (card shadow lift, border color change)
- [ ] Responsive: test at 1280px, 800px, 375px widths
+71
View File
@@ -0,0 +1,71 @@
cli:
backend: "codex"
event_loop:
prompt_file: "PROMPT.md"
starting_event: "task.start"
completion_promise: "LOOP_COMPLETE"
max_iterations: 50
backpressure:
gates:
- name: "lint"
command: "npm run lint"
on_fail: "retry"
- name: "typecheck"
command: "npm run typecheck"
on_fail: "retry"
- name: "build"
command: "npm run build"
on_fail: "retry"
hats:
planner:
name: "Sidebar Workflow Planner"
triggers: ["task.start", "review.changes_requested"]
publishes: ["plan.ready"]
instructions: |
Read PROMPT.md first.
Your role is planning only:
- Analyse current layout/nav implementation in the existing codebase.
- Create or update .ralph/plan.md with a concrete implementation plan.
- Include file-level changes, risks, and accessibility/responsive checks.
- If triggered by review.changes_requested, read .ralph/review.md and adapt the plan.
Do not write implementation code.
Emit plan.ready when the plan is ready.
builder:
name: "Sidebar Workflow Builder"
triggers: ["plan.ready"]
publishes: ["build.done"]
instructions: |
Read PROMPT.md and .ralph/plan.md first.
Implement the planned sidebar-focused layout changes:
- Move top navigation responsibilities into the sidebar.
- Remove obsolete top navbar/subnav behavior from the rendered layout.
- Implement desktop and mobile sidebar behavior requested in PROMPT.md.
- Keep section labels aligned to actual recruiter-facing content.
- Ensure scroll behavior and anchor navigation are correct.
Update .ralph/plan.md as work is completed.
Emit build.done when implementation is complete.
reviewer:
name: "Sidebar Workflow Reviewer"
triggers: ["build.done"]
publishes: ["review.approved", "review.changes_requested"]
instructions: |
Read PROMPT.md (and .ralph/plan.md if needed), then verify final behavior.
Validate against all success criteria and project conventions:
- UX behavior (desktop + mobile)
- Navigation semantics and labels
- Accessibility and interaction quality
- Lint/typecheck/build status
Write findings to .ralph/review.md.
If any criteria fail, emit review.changes_requested with specific actionable feedback.
If all criteria pass, print LOOP_COMPLETE.
+39
View File
@@ -12,6 +12,8 @@
"@xenova/transformers": "^2.17.2", "@xenova/transformers": "^2.17.2",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
"d3": "^7.9.0", "d3": "^7.9.0",
"embla-carousel-autoplay": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"framer-motion": "^11.15.0", "framer-motion": "^11.15.0",
"fuse.js": "^7.1.0", "fuse.js": "^7.1.0",
"lucide-react": "^0.468.0", "lucide-react": "^0.468.0",
@@ -3286,6 +3288,43 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/embla-carousel": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
"license": "MIT"
},
"node_modules/embla-carousel-autoplay": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/embla-carousel-autoplay/-/embla-carousel-autoplay-8.6.0.tgz",
"integrity": "sha512-OBu5G3nwaSXkZCo1A6LTaFMZ8EpkYbwIaH+bPqdBnDGQ2fh4+NbzjXjs2SktoPNKCtflfVMc75njaDHOYXcrsA==",
"license": "MIT",
"peerDependencies": {
"embla-carousel": "8.6.0"
}
},
"node_modules/embla-carousel-react": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.6.0.tgz",
"integrity": "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==",
"license": "MIT",
"dependencies": {
"embla-carousel": "8.6.0",
"embla-carousel-reactive-utils": "8.6.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/embla-carousel-reactive-utils": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.6.0.tgz",
"integrity": "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==",
"license": "MIT",
"peerDependencies": {
"embla-carousel": "8.6.0"
}
},
"node_modules/emoji-regex": { "node_modules/emoji-regex": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+2
View File
@@ -17,6 +17,8 @@
"@xenova/transformers": "^2.17.2", "@xenova/transformers": "^2.17.2",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
"d3": "^7.9.0", "d3": "^7.9.0",
"embla-carousel-autoplay": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"framer-motion": "^11.15.0", "framer-motion": "^11.15.0",
"fuse.js": "^7.1.0", "fuse.js": "^7.1.0",
"lucide-react": "^0.468.0", "lucide-react": "^0.468.0",
+1 -28
View File
@@ -1,34 +1,7 @@
# Ralph Orchestrator Configuration
# Generated by: ralph init --backend codex
# Docs: https://github.com/mikeyobrien/ralph-orchestrator
cli: cli:
backend: "codex" backend: "codex"
event_loop: event_loop:
prompt_file: "PROMPT.md" prompt_file: "PROMPT.md"
completion_promise: "LOOP_COMPLETE" completion_promise: "LOOP_COMPLETE"
max_iterations: 100 max_iterations: 50
# max_runtime_seconds: 14400 # 4 hours max
# ─────────────────────────────────────────────────────────────────────────────
# Additional Configuration (uncomment to customize)
# ─────────────────────────────────────────────────────────────────────────────
# core:
# scratchpad: ".ralph/agent/scratchpad.md"
# specs_dir: ".ralph/specs/"
# Custom hats for multi-agent workflows:
# hats:
# builder:
# name: "Builder"
# triggers: ["build.task"]
# publishes: ["build.done", "build.blocked"]
#
# reviewer:
# name: "Reviewer"
# triggers: ["review.request"]
# publishes: ["review.approved", "review.changes_requested"]
# Create PROMPT.md with your task, then run: ralph run
+19
View File
@@ -1,3 +1,22 @@
# Ralph Progress Log # Ralph Progress Log
Started: Sat Feb 14 02:26:59 GMT 2026 Started: Sat Feb 14 02:26:59 GMT 2026
--- ---
## Manual Intervention -- 2026-02-16
### Reason: Sidebar navigation architecture needs a structural reset to remove navbar artifacts and align labels with recruiter-facing content.
### Changes made:
- Added `Ralph/prompts.md` with a comprehensive sidebar-first implementation prompt.
- Captured mobile collapsed sidebar requirements (hamburger + five quick-access icons).
- Captured IA/naming migration from legacy labels to content-true labels.
### Tasks reset: none
### Tasks added:
- Remove top navbar and eliminate hidden top scroll space artifact.
- Move primary nav into sidebar with canonical labels.
- Add `Navigation` subgroup and contextual links.
- Implement collapsed-by-default mobile sidebar with quick icons and expandable full menu.
- Apply GP-style iconography to recruiter-friendly labels.
### Context for next iteration:
- Treat this as a structural layout update, not a cosmetic tweak.
- Prioritize fixing offset/height logic that currently reveals hidden space above sidebar content.
- Keep text labels aligned to portfolio content while allowing metaphor-based icons.
### New guardrails added: none
+1 -1
View File
@@ -421,7 +421,7 @@ export function DashboardLayout() {
{/* PatientSummaryTile — full width (includes Latest Results subsection) */} {/* PatientSummaryTile — full width (includes Latest Results subsection) */}
<PatientSummaryTile /> <PatientSummaryTile />
{/* ProjectsTile — half width */} {/* ProjectsTile — full width */}
<ProjectsTile /> <ProjectsTile />
{/* Patient Pathway — parent section with constellation graph + subsections */} {/* Patient Pathway — parent section with constellation graph + subsections */}
+1 -1
View File
@@ -13,7 +13,7 @@ const sections: NavSection[] = [
{ id: 'overview', label: 'Overview', tileId: 'patient-summary' }, { id: 'overview', label: 'Overview', tileId: 'patient-summary' },
{ id: 'skills', label: 'Skills', tileId: 'section-skills' }, { id: 'skills', label: 'Skills', tileId: 'section-skills' },
{ id: 'experience', label: 'Experience', tileId: 'section-experience' }, { id: 'experience', label: 'Experience', tileId: 'section-experience' },
{ id: 'projects', label: 'Projects', tileId: 'projects' }, { id: 'projects', label: 'Significant Interventions', tileId: 'projects' },
{ id: 'education', label: 'Education', tileId: 'section-education' }, { id: 'education', label: 'Education', tileId: 'section-education' },
] ]
+282 -78
View File
@@ -1,4 +1,4 @@
import React, { useCallback } from 'react' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { investigations } from '@/data/investigations' import { investigations } from '@/data/investigations'
import { Card, CardHeader } from '../Card' import { Card, CardHeader } from '../Card'
import { useDetailPanel } from '@/contexts/DetailPanelContext' import { useDetailPanel } from '@/contexts/DetailPanelContext'
@@ -12,10 +12,19 @@ const statusColorMap: Record<string, string> = {
interface ProjectItemProps { interface ProjectItemProps {
project: Investigation project: Investigation
slideWidth: string
cardMinHeight: number
thumbnailHeight: number
onClick: () => void onClick: () => void
} }
function ProjectItem({ project, onClick }: ProjectItemProps) { function ProjectItem({
project,
slideWidth,
cardMinHeight,
thumbnailHeight,
onClick,
}: ProjectItemProps) {
const dotColor = statusColorMap[project.status] || '#0D6E6E' const dotColor = statusColorMap[project.status] || '#0D6E6E'
const isLive = project.status === 'Live' const isLive = project.status === 'Live'
@@ -31,112 +40,307 @@ function ProjectItem({ project, onClick }: ProjectItemProps) {
return ( return (
<div <div
role="button"
tabIndex={0}
onClick={onClick}
onKeyDown={handleKeyDown}
style={{ style={{
display: 'flex', flex: `0 0 ${slideWidth}`,
flexDirection: 'column', minWidth: 0,
background: 'var(--surface)',
border: '1px solid var(--border-light)',
borderRadius: 'var(--radius-sm)',
padding: '12px 16px',
minHeight: '44px',
fontSize: '13px',
color: 'var(--text-primary)',
transition: 'border-color 0.15s, box-shadow 0.15s',
cursor: 'pointer',
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = 'var(--accent-border)'
e.currentTarget.style.boxShadow = '0 2px 8px rgba(26,43,42,0.08)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = 'var(--border-light)'
e.currentTarget.style.boxShadow = 'none'
}} }}
> >
{/* Row: status dot + name + year */}
<div <div
role="button"
tabIndex={0}
onClick={onClick}
onKeyDown={handleKeyDown}
style={{ style={{
display: 'flex', display: 'flex',
alignItems: 'flex-start', flexDirection: 'column',
gap: '8px', gap: '10px',
marginBottom: '8px', background: 'var(--surface)',
border: '1px solid var(--border-light)',
borderRadius: 'var(--radius-sm)',
padding: '12px',
minHeight: `${cardMinHeight}px`,
fontSize: '13px',
color: 'var(--text-primary)',
transition: 'border-color 0.15s, box-shadow 0.15s',
cursor: 'pointer',
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = 'var(--accent-border)'
e.currentTarget.style.boxShadow = '0 2px 8px rgba(26,43,42,0.08)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = 'var(--border-light)'
e.currentTarget.style.boxShadow = 'none'
}}
onFocus={(e) => {
e.currentTarget.style.borderColor = 'var(--accent-border)'
e.currentTarget.style.boxShadow = '0 2px 8px rgba(26,43,42,0.08)'
}}
onBlur={(e) => {
e.currentTarget.style.borderColor = 'var(--border-light)'
e.currentTarget.style.boxShadow = 'none'
}} }}
> >
<div <div
style={{ style={{
width: '8px', minHeight: `${thumbnailHeight}px`,
height: '8px', flex: 1,
borderRadius: '50%', borderRadius: '6px',
backgroundColor: dotColor, border: '1px solid var(--border-light)',
flexShrink: 0, background:
marginTop: '4px', 'linear-gradient(135deg, rgba(19, 94, 94, 0.12), rgba(212, 171, 46, 0.18))',
animation: isLive ? 'pulse 2s infinite' : undefined, display: 'flex',
}} alignItems: 'center',
aria-hidden="true" justifyContent: 'center',
/>
<span style={{ flex: 1, fontWeight: 500 }}>{project.name}</span>
<span
style={{
fontSize: '11px',
fontFamily: 'var(--font-geist-mono)', fontFamily: 'var(--font-geist-mono)',
fontSize: '10px',
letterSpacing: '0.08em',
color: 'var(--text-tertiary)', color: 'var(--text-tertiary)',
flexShrink: 0, textTransform: 'uppercase',
}} }}
> >
{project.requestedYear} Thumbnail Pending
</span> </div>
</div>
{/* Tech stack tags */}
{project.techStack && project.techStack.length > 0 && (
<div <div
style={{ style={{
display: 'flex', display: 'flex',
flexWrap: 'wrap', alignItems: 'flex-start',
gap: '4px', gap: '8px',
}} }}
> >
{project.techStack.map((tech) => ( <div
<span style={{
key={tech} width: '8px',
style={{ height: '8px',
fontSize: '10px', borderRadius: '50%',
fontFamily: 'var(--font-geist-mono)', backgroundColor: dotColor,
padding: '3px 8px', flexShrink: 0,
borderRadius: '3px', marginTop: '4px',
background: 'var(--amber-light)', animation: isLive ? 'pulse 2s infinite' : undefined,
color: '#92400E', }}
border: '1px solid var(--amber-border)', aria-hidden="true"
}} />
> <span style={{ flex: 1, fontWeight: 500 }}>{project.name}</span>
{tech} <span
</span> style={{
))} fontSize: '11px',
fontFamily: 'var(--font-geist-mono)',
color: 'var(--text-tertiary)',
flexShrink: 0,
}}
>
{project.requestedYear}
</span>
</div> </div>
)}
{project.techStack && project.techStack.length > 0 && (
<div
style={{
display: 'flex',
flexWrap: 'wrap',
gap: '4px',
}}
>
{project.techStack.map((tech) => (
<span
key={tech}
style={{
fontSize: '10px',
fontFamily: 'var(--font-geist-mono)',
padding: '3px 8px',
borderRadius: '3px',
background: 'var(--amber-light)',
color: '#92400E',
border: '1px solid var(--amber-border)',
}}
>
{tech}
</span>
))}
</div>
)}
</div>
</div> </div>
) )
} }
export function ProjectsTile() { export function ProjectsTile() {
const { openPanel } = useDetailPanel() const { openPanel } = useDetailPanel()
const viewportRef = useRef<HTMLDivElement | null>(null)
const trackRef = useRef<HTMLDivElement | null>(null)
const firstSetRef = useRef<HTMLDivElement | null>(null)
const offsetRef = useRef(0)
const isPausedRef = useRef(false)
const [viewportWidth, setViewportWidth] = useState(1200)
const [prefersReducedMotion, setPrefersReducedMotion] = useState(() =>
typeof window !== 'undefined'
? window.matchMedia('(prefers-reduced-motion: reduce)').matches
: false,
)
useEffect(() => {
const viewportEl = viewportRef.current
if (!viewportEl || typeof window === 'undefined') {
return
}
const updateWidth = () => {
const nextWidth = viewportEl.clientWidth
if (nextWidth > 0) {
setViewportWidth(nextWidth)
}
}
updateWidth()
if (typeof ResizeObserver !== 'undefined') {
const observer = new ResizeObserver(() => updateWidth())
observer.observe(viewportEl)
return () => observer.disconnect()
}
window.addEventListener('resize', updateWidth)
return () => window.removeEventListener('resize', updateWidth)
}, [])
useEffect(() => {
if (typeof window === 'undefined') {
return
}
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)')
const syncMotionPreference = () => setPrefersReducedMotion(mediaQuery.matches)
syncMotionPreference()
mediaQuery.addEventListener('change', syncMotionPreference)
return () => mediaQuery.removeEventListener('change', syncMotionPreference)
}, [])
useEffect(() => {
const trackEl = trackRef.current
const firstSetEl = firstSetRef.current
if (!trackEl || !firstSetEl || prefersReducedMotion) {
return
}
let animationFrameId = 0
let lastTime = 0
const speedPxPerSecond = viewportWidth < 768 ? 18 : 24
const tick = (timestamp: number) => {
if (!lastTime) {
lastTime = timestamp
}
const deltaSeconds = (timestamp - lastTime) / 1000
lastTime = timestamp
if (!isPausedRef.current) {
const setWidth = firstSetEl.offsetWidth
if (setWidth > 0) {
offsetRef.current += speedPxPerSecond * deltaSeconds
if (offsetRef.current >= setWidth) {
offsetRef.current -= setWidth
}
trackEl.style.transform = `translate3d(-${offsetRef.current}px, 0, 0)`
}
}
animationFrameId = window.requestAnimationFrame(tick)
}
animationFrameId = window.requestAnimationFrame(tick)
return () => window.cancelAnimationFrame(animationFrameId)
}, [prefersReducedMotion, viewportWidth])
const cardsPerView = useMemo(() => {
if (viewportWidth < 768) {
return 1
}
return 4
}, [viewportWidth])
const slideWidth = useMemo(() => {
const gap = 12
const totalGap = (cardsPerView - 1) * gap
const computedWidth = (viewportWidth - totalGap) / cardsPerView
return `${Math.max(computedWidth, 0)}px`
}, [cardsPerView, viewportWidth])
const cardMinHeight = useMemo(() => {
if (viewportWidth < 640) {
return 168
}
if (viewportWidth < 1024) {
return 182
}
if (viewportWidth < 1440) {
return 196
}
return 214
}, [viewportWidth])
const thumbnailHeight = useMemo(() => {
if (viewportWidth < 640) {
return 62
}
if (viewportWidth < 1024) {
return 68
}
if (viewportWidth < 1440) {
return 76
}
return 84
}, [viewportWidth])
const setPaused = (value: boolean) => {
isPausedRef.current = value
}
return ( return (
<Card tileId="projects"> <Card full tileId="projects">
<CardHeader dotColor="amber" title="ACTIVE PROJECTS" /> <CardHeader dotColor="amber" title="SIGNIFICANT INTERVENTIONS" />
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}> <div
{investigations.map((project) => ( ref={viewportRef}
<ProjectItem style={{ overflow: 'hidden' }}
key={project.id} onMouseEnter={() => setPaused(true)}
project={project} onMouseLeave={() => setPaused(false)}
onClick={() => openPanel({ type: 'project', investigation: project })} onFocusCapture={() => setPaused(true)}
/> onBlurCapture={(event) => {
))} if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {
setPaused(false)
}
}}
>
<div
ref={trackRef}
style={{
display: 'flex',
width: 'max-content',
willChange: 'transform',
transform: 'translate3d(0, 0, 0)',
}}
>
{[0, 1].map((setIndex) => (
<div
key={setIndex}
ref={setIndex === 0 ? firstSetRef : undefined}
style={{ display: 'flex', gap: '12px', paddingRight: '12px', flexShrink: 0 }}
>
{investigations.map((project) => (
<ProjectItem
key={`${setIndex}-${project.id}`}
project={project}
slideWidth={slideWidth}
cardMinHeight={cardMinHeight}
thumbnailHeight={thumbnailHeight}
onClick={() => openPanel({ type: 'project', investigation: project })}
/>
))}
</div>
))}
</div>
</div> </div>
</Card> </Card>
) )
+1 -1
View File
@@ -390,7 +390,7 @@ html {
/* Desktop: 2 columns */ /* Desktop: 2 columns */
@media (min-width: 1024px) { @media (min-width: 1024px) {
.pathway-columns { .pathway-columns {
grid-template-columns: minmax(0, 1.85fr) minmax(0, 1fr); grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr);
align-items: start; align-items: start;
gap: 22px; gap: 22px;
} }
+5 -6
View File
@@ -7,7 +7,7 @@ import { skills } from '@/data/skills'
import { kpis } from '@/data/kpis' import { kpis } from '@/data/kpis'
import type { DetailPanelContent } from '@/types/pmr' import type { DetailPanelContent } from '@/types/pmr'
export type PaletteSection = 'Experience' | 'Core Skills' | 'Active Projects' | 'Achievements' | 'Education' | 'Quick Actions' export type PaletteSection = 'Experience' | 'Core Skills' | 'Significant Interventions' | 'Achievements' | 'Education' | 'Quick Actions'
export type PaletteAction = export type PaletteAction =
| { type: 'scroll'; tileId: string } | { type: 'scroll'; tileId: string }
@@ -61,13 +61,13 @@ export function buildPaletteData(): PaletteItem[] {
}) })
}) })
// Active Projects — all 5 investigations from investigations.ts // Significant Interventions — all 5 investigations from investigations.ts
investigations.forEach((inv) => { investigations.forEach((inv) => {
items.push({ items.push({
id: `proj-${inv.id}`, id: `proj-${inv.id}`,
title: inv.name, title: inv.name,
subtitle: `${inv.methodology.split('.')[0]} \u00b7 ${inv.requestedYear}`, subtitle: `${inv.methodology.split('.')[0]} \u00b7 ${inv.requestedYear}`,
section: 'Active Projects', section: 'Significant Interventions',
iconVariant: 'amber', iconVariant: 'amber',
iconType: 'project', iconType: 'project',
keywords: `${inv.name.toLowerCase()} ${inv.methodology.toLowerCase()} ${inv.techStack.join(' ').toLowerCase()} ${inv.requestedYear}`, keywords: `${inv.name.toLowerCase()} ${inv.methodology.toLowerCase()} ${inv.techStack.join(' ').toLowerCase()} ${inv.requestedYear}`,
@@ -218,7 +218,7 @@ export function buildSearchIndex(items: PaletteItem[]): Fuse<PaletteItem> {
const SECTION_ORDER: PaletteSection[] = [ const SECTION_ORDER: PaletteSection[] = [
'Experience', 'Experience',
'Core Skills', 'Core Skills',
'Active Projects', 'Significant Interventions',
'Achievements', 'Achievements',
'Education', 'Education',
'Quick Actions', 'Quick Actions',
@@ -332,7 +332,7 @@ export function buildEmbeddingTexts(): Array<{ id: string; text: string }> {
}) })
}) })
// Investigations (Active Projects) — enriched with role context and cross-references // Investigations (Significant Interventions) — enriched with role context and cross-references
const projectContextMap: Record<string, string> = { const projectContextMap: Record<string, string> = {
'inv-pharmetrics': 'Built during Deputy Head role at NHS Norfolk & Waveney ICB. Provides self-serve analytics for budget holders across the integrated care system. Live at medicines.charlwood.xyz.', 'inv-pharmetrics': 'Built during Deputy Head role at NHS Norfolk & Waveney ICB. Provides self-serve analytics for budget holders across the integrated care system. Live at medicines.charlwood.xyz.',
'inv-switching-algorithm': 'Built during Interim Head role at NHS Norfolk & Waveney ICB. Uses real-world GP prescribing data to auto-identify patients on expensive drugs suitable for cost-effective alternatives. Compressed months of manual analysis into 3 days. Includes novel GP payment system linking incentive rewards to prescribing savings.', 'inv-switching-algorithm': 'Built during Interim Head role at NHS Norfolk & Waveney ICB. Uses real-world GP prescribing data to auto-identify patients on expensive drugs suitable for cost-effective alternatives. Compressed months of manual analysis into 3 days. Includes novel GP payment system linking incentive rewards to prescribing savings.',
@@ -394,4 +394,3 @@ export function buildEmbeddingTexts(): Array<{ id: string; text: string }> {
return texts return texts
} }