Spaces:
Runtime error
feat: Initial Clinical-Mind project setup with full-stack architecture
Browse filesComplete project scaffold for AI-powered clinical reasoning simulator:
Frontend (React 18 + TypeScript + Tailwind CSS v4):
- Landing page with hero, features, specialties, and CTA sections
- Case browser with specialty/difficulty filters and sample cases
- Interactive case interface with progressive info reveal and AI tutor sidebar
- Performance dashboard with Recharts analytics and bias radar
- D3.js knowledge graph visualization with interactive force-directed layout
- Design system: Button, Card, Badge, Input, StatCard components
- Warm, organic color palette (Honest Greens inspired)
Backend (FastAPI + Python):
- REST API with cases, student, and analytics endpoints
- Case generator with sample cardiology, infectious, neurology cases
- Socratic tutor with keyword-based Socratic responses
- Cognitive bias detector (anchoring, premature closure, availability, confirmation)
- Knowledge graph builder with demo data
- Case recommendation engine
https://claude.ai/code/session_01EtTBqEZVEmdWihzhSden2o
- .gitignore +38 -0
- DEVELOPMENT_STATUS.md +59 -0
- MEMORY.md +48 -0
- README.md +136 -0
- backend/.env.example +3 -0
- backend/app/__init__.py +1 -0
- backend/app/api/__init__.py +1 -0
- backend/app/api/analytics.py +47 -0
- backend/app/api/cases.py +72 -0
- backend/app/api/student.py +58 -0
- backend/app/core/__init__.py +1 -0
- backend/app/core/agents/__init__.py +1 -0
- backend/app/core/agents/tutor.py +55 -0
- backend/app/core/analytics/__init__.py +1 -0
- backend/app/core/analytics/bias_detector.py +118 -0
- backend/app/core/analytics/knowledge_graph.py +79 -0
- backend/app/core/analytics/recommender.py +62 -0
- backend/app/core/rag/__init__.py +1 -0
- backend/app/core/rag/generator.py +130 -0
- backend/app/main.py +31 -0
- backend/app/models/__init__.py +1 -0
- backend/app/utils/__init__.py +1 -0
- backend/requirements.txt +9 -0
- frontend/.gitignore +23 -0
- frontend/README.md +46 -0
- frontend/package-lock.json +0 -0
- frontend/package.json +56 -0
- frontend/postcss.config.js +5 -0
- frontend/public/favicon.ico +0 -0
- frontend/public/index.html +43 -0
- frontend/public/logo192.png +0 -0
- frontend/public/logo512.png +0 -0
- frontend/public/manifest.json +25 -0
- frontend/public/robots.txt +3 -0
- frontend/src/App.tsx +26 -0
- frontend/src/components/layout/Footer.tsx +27 -0
- frontend/src/components/layout/Header.tsx +54 -0
- frontend/src/components/layout/Layout.tsx +25 -0
- frontend/src/components/layout/index.ts +3 -0
- frontend/src/components/ui/Badge.tsx +32 -0
- frontend/src/components/ui/Button.tsx +44 -0
- frontend/src/components/ui/Card.tsx +32 -0
- frontend/src/components/ui/Input.tsx +54 -0
- frontend/src/components/ui/StatCard.tsx +36 -0
- frontend/src/components/ui/index.ts +5 -0
- frontend/src/index.css +115 -0
- frontend/src/index.tsx +13 -0
- frontend/src/pages/CaseBrowser.tsx +193 -0
- frontend/src/pages/CaseInterface.tsx +342 -0
- frontend/src/pages/Dashboard.tsx +248 -0
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dependencies
|
| 2 |
+
node_modules/
|
| 3 |
+
frontend/node_modules/
|
| 4 |
+
backend/venv/
|
| 5 |
+
backend/__pycache__/
|
| 6 |
+
backend/app/__pycache__/
|
| 7 |
+
**/__pycache__/
|
| 8 |
+
*.pyc
|
| 9 |
+
|
| 10 |
+
# Build
|
| 11 |
+
frontend/build/
|
| 12 |
+
dist/
|
| 13 |
+
|
| 14 |
+
# Environment
|
| 15 |
+
.env
|
| 16 |
+
.env.local
|
| 17 |
+
backend/.env
|
| 18 |
+
|
| 19 |
+
# IDE
|
| 20 |
+
.vscode/
|
| 21 |
+
.idea/
|
| 22 |
+
*.swp
|
| 23 |
+
*.swo
|
| 24 |
+
|
| 25 |
+
# OS
|
| 26 |
+
.DS_Store
|
| 27 |
+
Thumbs.db
|
| 28 |
+
|
| 29 |
+
# Data
|
| 30 |
+
backend/data/vector_db/
|
| 31 |
+
*.db
|
| 32 |
+
|
| 33 |
+
# Logs
|
| 34 |
+
*.log
|
| 35 |
+
npm-debug.log*
|
| 36 |
+
|
| 37 |
+
# Testing
|
| 38 |
+
coverage/
|
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Development Status
|
| 2 |
+
|
| 3 |
+
Last Updated: 2026-02-11
|
| 4 |
+
|
| 5 |
+
## Phase Completion
|
| 6 |
+
|
| 7 |
+
- [x] Phase 0: Setup & Foundation
|
| 8 |
+
- [x] Phase 1: Core UI Foundation
|
| 9 |
+
- [x] Phase 2: Backend - RAG System
|
| 10 |
+
- [x] Phase 3: Interactive Case Interface
|
| 11 |
+
- [x] Phase 4: Cognitive Bias Detection
|
| 12 |
+
- [x] Phase 5: Knowledge Graph Visualization
|
| 13 |
+
- [x] Phase 6: Dashboard & Progress Tracking
|
| 14 |
+
- [ ] Phase 7: Polish & Demo Prep
|
| 15 |
+
|
| 16 |
+
## Features Complete
|
| 17 |
+
|
| 18 |
+
### Core Features
|
| 19 |
+
- [x] Landing page with hero, features, specialties, CTA
|
| 20 |
+
- [x] Case browser with filters (specialty, difficulty)
|
| 21 |
+
- [x] Interactive case interface with progressive reveal
|
| 22 |
+
- [x] AI tutor sidebar with Socratic dialogue
|
| 23 |
+
- [x] Cognitive bias detection and reporting
|
| 24 |
+
- [x] D3.js knowledge graph visualization
|
| 25 |
+
- [x] Performance dashboard with charts
|
| 26 |
+
- [x] Specialty performance breakdown
|
| 27 |
+
- [x] Case recommendations engine
|
| 28 |
+
|
| 29 |
+
### UI Components
|
| 30 |
+
- [x] Button (primary, secondary, tertiary variants)
|
| 31 |
+
- [x] Card (hover effects, padding options)
|
| 32 |
+
- [x] Badge (default, success, warning, error, info)
|
| 33 |
+
- [x] Input (text, multiline)
|
| 34 |
+
- [x] StatCard (with colored border, trend)
|
| 35 |
+
|
| 36 |
+
### Backend
|
| 37 |
+
- [x] FastAPI application with CORS
|
| 38 |
+
- [x] Case generation API
|
| 39 |
+
- [x] Student profile API
|
| 40 |
+
- [x] Analytics API
|
| 41 |
+
- [x] Bias detection engine
|
| 42 |
+
- [x] Knowledge graph builder
|
| 43 |
+
- [x] Case recommendation engine
|
| 44 |
+
- [x] Socratic tutor (keyword-based)
|
| 45 |
+
|
| 46 |
+
### Design System
|
| 47 |
+
- [x] Warm color palette (Honest Greens inspired)
|
| 48 |
+
- [x] Custom typography scale (18px body)
|
| 49 |
+
- [x] Tailwind CSS v4 with @theme
|
| 50 |
+
- [x] Animations (fadeInUp, gentleFloat, scalePulse)
|
| 51 |
+
- [x] Responsive layout
|
| 52 |
+
|
| 53 |
+
## Known Issues
|
| 54 |
+
- Recharts v3 requires type casting for React 18 compatibility
|
| 55 |
+
- Backend uses in-memory data (no persistence)
|
| 56 |
+
|
| 57 |
+
## Build Status
|
| 58 |
+
- Frontend: Compiles successfully
|
| 59 |
+
- Bundle size: ~203 KB gzipped
|
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Clinical-Mind - Development Memory
|
| 2 |
+
|
| 3 |
+
## Key Decisions
|
| 4 |
+
|
| 5 |
+
### Design
|
| 6 |
+
- **Color Palette:** Warm, organic (Honest Greens inspired)
|
| 7 |
+
- Primary: Forest green (#2D5C3F), not generic blue
|
| 8 |
+
- Backgrounds: Cream (#FFFCF7), warm grays
|
| 9 |
+
- Accents: Terracotta (#C85835), sage green (#6B8E6F)
|
| 10 |
+
|
| 11 |
+
- **Typography:** Larger than typical (18px body)
|
| 12 |
+
- Font: Inter (clean, professional)
|
| 13 |
+
- Generous line heights (1.6-1.8)
|
| 14 |
+
|
| 15 |
+
- **Visualizations:** Code-based (D3.js, Recharts)
|
| 16 |
+
- No image generation APIs needed
|
| 17 |
+
- Interactive, data-driven
|
| 18 |
+
|
| 19 |
+
### Architecture
|
| 20 |
+
- **Frontend:** React 18 + TypeScript + Tailwind CSS v4
|
| 21 |
+
- **Backend:** FastAPI + Python
|
| 22 |
+
- **AI:** Claude Opus 4.6 (Socratic dialogue)
|
| 23 |
+
- **Database:** ChromaDB (vector store for RAG)
|
| 24 |
+
- **Charts:** Recharts v3 (with type workarounds for React 18)
|
| 25 |
+
|
| 26 |
+
### Technical Notes
|
| 27 |
+
- Recharts v3 has type incompatibilities with React 18 TypeScript
|
| 28 |
+
- Solution: Cast components to `any` for JSX compatibility
|
| 29 |
+
- Tailwind CSS v4 uses `@theme` directive instead of `tailwind.config.js`
|
| 30 |
+
- D3.js v7 works well with React useRef/useEffect pattern
|
| 31 |
+
|
| 32 |
+
## What's Working
|
| 33 |
+
- Frontend builds successfully (React + TypeScript + Tailwind v4)
|
| 34 |
+
- All 5 pages implemented (Landing, CaseBrowser, CaseInterface, Dashboard, KnowledgeGraph)
|
| 35 |
+
- UI component library (Button, Card, Badge, Input, StatCard)
|
| 36 |
+
- Layout components (Header, Footer, Layout with routing)
|
| 37 |
+
- Backend API structure (FastAPI with cases, student, analytics endpoints)
|
| 38 |
+
- Sample medical cases (Cardiology, Infectious, Neurology)
|
| 39 |
+
- Bias detection engine (anchoring, premature closure, availability, confirmation)
|
| 40 |
+
- Knowledge graph builder with demo data
|
| 41 |
+
- D3.js force-directed graph visualization
|
| 42 |
+
- Recharts analytics (area chart, radar chart)
|
| 43 |
+
|
| 44 |
+
## Architecture Decisions
|
| 45 |
+
- Demo data is hardcoded for hackathon speed
|
| 46 |
+
- Socratic tutor uses keyword matching (can be upgraded to Claude API)
|
| 47 |
+
- Case generator returns pre-built cases (can be upgraded to RAG + Claude)
|
| 48 |
+
- In-memory storage (no persistent database for demo)
|
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Clinical-Mind
|
| 2 |
+
|
| 3 |
+
**Master clinical reasoning, one case at a time.**
|
| 4 |
+
|
| 5 |
+
An AI-powered clinical reasoning simulator that helps Indian medical students develop expert-level diagnostic thinking by exposing cognitive biases, providing Socratic feedback, and creating realistic case scenarios.
|
| 6 |
+
|
| 7 |
+
## Problem
|
| 8 |
+
|
| 9 |
+
Medical students can memorize and answer MCQs, but struggle with real clinical reasoning:
|
| 10 |
+
- **Invisible cognitive biases** (anchoring, premature closure, availability)
|
| 11 |
+
- **Can't explain reasoning** when attendings ask "Why?"
|
| 12 |
+
- **No practice under pressure** - freeze in real emergencies
|
| 13 |
+
- **Can't see knowledge connections** between concepts
|
| 14 |
+
- **Textbook cases ≠ Indian reality** (dengue, TB, resource constraints)
|
| 15 |
+
|
| 16 |
+
## Solution
|
| 17 |
+
|
| 18 |
+
Clinical-Mind is a multi-layered reasoning development platform:
|
| 19 |
+
|
| 20 |
+
1. **RAG-Powered Case Generation** - Dynamic cases from Indian medical literature
|
| 21 |
+
2. **Socratic AI Tutor** - Multi-turn dialogue that asks "why" until deep understanding
|
| 22 |
+
3. **Cognitive Bias Detection** - Tracks patterns across 20+ cases, identifies biases
|
| 23 |
+
4. **Knowledge Graph Visualization** - Interactive D3.js map of concept mastery
|
| 24 |
+
5. **Performance Analytics** - Personalized dashboard with peer benchmarking
|
| 25 |
+
6. **India-Centric Content** - Cases set in Indian hospitals with regional disease patterns
|
| 26 |
+
|
| 27 |
+
## Tech Stack
|
| 28 |
+
|
| 29 |
+
| Layer | Technology |
|
| 30 |
+
|-------|-----------|
|
| 31 |
+
| Frontend | React 18, TypeScript, Tailwind CSS |
|
| 32 |
+
| Visualization | D3.js (knowledge graph), Recharts (analytics) |
|
| 33 |
+
| Backend | Python 3.11+, FastAPI |
|
| 34 |
+
| AI Engine | Claude Opus 4.6 (Anthropic API) |
|
| 35 |
+
| Vector DB | ChromaDB + LangChain |
|
| 36 |
+
| Embeddings | Sentence-Transformers |
|
| 37 |
+
|
| 38 |
+
## Quick Start
|
| 39 |
+
|
| 40 |
+
### Frontend
|
| 41 |
+
```bash
|
| 42 |
+
cd frontend
|
| 43 |
+
npm install
|
| 44 |
+
npm start
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
### Backend
|
| 48 |
+
```bash
|
| 49 |
+
cd backend
|
| 50 |
+
python -m venv venv
|
| 51 |
+
source venv/bin/activate # or `venv\Scripts\activate` on Windows
|
| 52 |
+
pip install -r requirements.txt
|
| 53 |
+
uvicorn app.main:app --reload
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
### Environment Variables
|
| 57 |
+
```bash
|
| 58 |
+
cp backend/.env.example backend/.env
|
| 59 |
+
# Add your ANTHROPIC_API_KEY
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
## Project Structure
|
| 63 |
+
|
| 64 |
+
```
|
| 65 |
+
clinical-mind/
|
| 66 |
+
├── frontend/ # React + TypeScript UI
|
| 67 |
+
│ ├── src/
|
| 68 |
+
│ │ ├── components/
|
| 69 |
+
│ │ │ ├── ui/ # Design system (Button, Card, Badge, etc.)
|
| 70 |
+
│ │ │ ├── layout/ # Header, Footer, Layout
|
| 71 |
+
│ │ │ ├── case/ # Case-specific components
|
| 72 |
+
│ │ │ └── visualizations/ # D3.js, Recharts components
|
| 73 |
+
│ │ ├── pages/
|
| 74 |
+
│ │ │ ├── Landing.tsx # Home page
|
| 75 |
+
│ │ │ ├── CaseBrowser.tsx # Browse/filter cases
|
| 76 |
+
│ │ │ ├── CaseInterface.tsx # Main case-solving experience
|
| 77 |
+
│ │ │ ├── Dashboard.tsx # Performance analytics
|
| 78 |
+
│ │ │ └── KnowledgeGraph.tsx # D3.js knowledge map
|
| 79 |
+
│ │ ├── types/ # TypeScript interfaces
|
| 80 |
+
│ │ └── hooks/ # Custom React hooks
|
| 81 |
+
│ └── public/
|
| 82 |
+
├── backend/ # FastAPI + Python
|
| 83 |
+
│ ├── app/
|
| 84 |
+
│ │ ├── api/ # REST endpoints
|
| 85 |
+
│ │ ├── core/
|
| 86 |
+
│ │ │ ├── rag/ # RAG case generation
|
| 87 |
+
│ │ │ ├── agents/ # Socratic tutor AI
|
| 88 |
+
│ │ │ └── analytics/ # Bias detection, knowledge graph
|
| 89 |
+
│ │ └── models/ # Data models
|
| 90 |
+
│ └── data/
|
| 91 |
+
│ └── medical_corpus/ # Medical literature for RAG
|
| 92 |
+
└── docs/ # Documentation
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
## Key Features
|
| 96 |
+
|
| 97 |
+
### Interactive Case Interface
|
| 98 |
+
- Progressive information reveal (history → exam → labs)
|
| 99 |
+
- Real-time AI tutor sidebar with Socratic questioning
|
| 100 |
+
- Diagnosis submission with detailed feedback
|
| 101 |
+
|
| 102 |
+
### Cognitive Bias Detection
|
| 103 |
+
- Tracks anchoring, premature closure, availability, and confirmation biases
|
| 104 |
+
- Statistical analysis across case history
|
| 105 |
+
- Personalized recommendations to counter biases
|
| 106 |
+
|
| 107 |
+
### Knowledge Graph
|
| 108 |
+
- Interactive D3.js force-directed graph
|
| 109 |
+
- Color-coded by category (specialty, diagnosis, symptom, investigation)
|
| 110 |
+
- Shows strong vs weak concept connections
|
| 111 |
+
- Click nodes to see mastery details
|
| 112 |
+
|
| 113 |
+
### Performance Dashboard
|
| 114 |
+
- Accuracy trends over time (area charts)
|
| 115 |
+
- Specialty-wise performance breakdown
|
| 116 |
+
- Bias radar chart
|
| 117 |
+
- Personalized case recommendations
|
| 118 |
+
|
| 119 |
+
## Design Philosophy
|
| 120 |
+
|
| 121 |
+
Inspired by Honest Greens + Linear:
|
| 122 |
+
- **Warm, organic palette** (cream backgrounds, forest greens, terracotta accents)
|
| 123 |
+
- **Larger typography** (18px body minimum for long study sessions)
|
| 124 |
+
- **Generous spacing** and smooth transitions (400ms ease-out)
|
| 125 |
+
- **Premium but approachable** - professional without being intimidating
|
| 126 |
+
|
| 127 |
+
## Hackathon
|
| 128 |
+
|
| 129 |
+
Built for **Problem Statement #3: "Amplify Human Judgment"**
|
| 130 |
+
- AI sharpens medical expertise without replacing it
|
| 131 |
+
- Makes students dramatically more capable
|
| 132 |
+
- Keeps humans in the loop
|
| 133 |
+
|
| 134 |
+
## License
|
| 135 |
+
|
| 136 |
+
MIT
|
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
ANTHROPIC_API_KEY=sk-ant-your-key-here
|
| 2 |
+
CHROMA_DB_PATH=./data/vector_db
|
| 3 |
+
FRONTEND_URL=http://localhost:3000
|
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Clinical-Mind Backend
|
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# API routes
|
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter
|
| 2 |
+
|
| 3 |
+
router = APIRouter()
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
@router.get("/performance")
|
| 7 |
+
async def get_performance():
|
| 8 |
+
return {
|
| 9 |
+
"overall_accuracy": 75,
|
| 10 |
+
"cases_completed": 48,
|
| 11 |
+
"avg_time_minutes": 10,
|
| 12 |
+
"peer_percentile": 15,
|
| 13 |
+
"history": [
|
| 14 |
+
{"week": "W1", "accuracy": 55, "avg_time": 18},
|
| 15 |
+
{"week": "W2", "accuracy": 60, "avg_time": 16},
|
| 16 |
+
{"week": "W3", "accuracy": 58, "avg_time": 15},
|
| 17 |
+
{"week": "W4", "accuracy": 68, "avg_time": 14},
|
| 18 |
+
{"week": "W5", "accuracy": 72, "avg_time": 12},
|
| 19 |
+
{"week": "W6", "accuracy": 75, "avg_time": 11},
|
| 20 |
+
{"week": "W7", "accuracy": 78, "avg_time": 10},
|
| 21 |
+
],
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@router.get("/peer-comparison")
|
| 26 |
+
async def get_peer_comparison():
|
| 27 |
+
return {
|
| 28 |
+
"student_accuracy": 75,
|
| 29 |
+
"peer_average": 62,
|
| 30 |
+
"top_10_average": 88,
|
| 31 |
+
"ranking": "Top 15%",
|
| 32 |
+
"specialty_comparison": {
|
| 33 |
+
"cardiology": {"student": 82, "average": 65},
|
| 34 |
+
"respiratory": {"student": 65, "average": 60},
|
| 35 |
+
"infectious": {"student": 78, "average": 70},
|
| 36 |
+
"neurology": {"student": 45, "average": 55},
|
| 37 |
+
"gastro": {"student": 70, "average": 58},
|
| 38 |
+
"emergency": {"student": 55, "average": 52},
|
| 39 |
+
},
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
@router.get("/recommendations")
|
| 44 |
+
async def get_recommendations():
|
| 45 |
+
from app.core.analytics.recommender import CaseRecommender
|
| 46 |
+
recommender = CaseRecommender()
|
| 47 |
+
return recommender.get_demo_recommendations()
|
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, HTTPException
|
| 2 |
+
from pydantic import BaseModel
|
| 3 |
+
from typing import Optional
|
| 4 |
+
from app.core.rag.generator import CaseGenerator
|
| 5 |
+
|
| 6 |
+
router = APIRouter()
|
| 7 |
+
case_generator = CaseGenerator()
|
| 8 |
+
|
| 9 |
+
SPECIALTIES = [
|
| 10 |
+
{"id": "cardiology", "name": "Cardiology", "icon": "heart", "cases_available": 30, "description": "Heart failure, ACS, arrhythmias"},
|
| 11 |
+
{"id": "respiratory", "name": "Respiratory", "icon": "lungs", "cases_available": 25, "description": "Pneumonia, COPD, TB"},
|
| 12 |
+
{"id": "infectious", "name": "Infectious Disease", "icon": "virus", "cases_available": 28, "description": "Dengue, malaria, typhoid"},
|
| 13 |
+
{"id": "neurology", "name": "Neurology", "icon": "brain", "cases_available": 20, "description": "Stroke, meningitis, seizures"},
|
| 14 |
+
{"id": "gastro", "name": "Gastroenterology", "icon": "microscope", "cases_available": 22, "description": "Liver disease, pancreatitis, GI bleeds"},
|
| 15 |
+
{"id": "emergency", "name": "Emergency Medicine", "icon": "alert", "cases_available": 35, "description": "Acute MI, sepsis, trauma"},
|
| 16 |
+
]
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class CaseRequest(BaseModel):
|
| 20 |
+
specialty: str
|
| 21 |
+
difficulty: str = "intermediate"
|
| 22 |
+
year_level: str = "final_year"
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class CaseActionRequest(BaseModel):
|
| 26 |
+
case_id: str
|
| 27 |
+
action_type: str
|
| 28 |
+
student_input: Optional[str] = None
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class DiagnosisRequest(BaseModel):
|
| 32 |
+
case_id: str
|
| 33 |
+
diagnosis: str
|
| 34 |
+
reasoning: str = ""
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
@router.get("/specialties")
|
| 38 |
+
async def get_specialties():
|
| 39 |
+
return {"specialties": SPECIALTIES}
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@router.post("/generate")
|
| 43 |
+
async def generate_case(request: CaseRequest):
|
| 44 |
+
try:
|
| 45 |
+
case = case_generator.generate_case(
|
| 46 |
+
specialty=request.specialty,
|
| 47 |
+
difficulty=request.difficulty,
|
| 48 |
+
year_level=request.year_level,
|
| 49 |
+
)
|
| 50 |
+
return case
|
| 51 |
+
except Exception as e:
|
| 52 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
@router.get("/{case_id}")
|
| 56 |
+
async def get_case(case_id: str):
|
| 57 |
+
case = case_generator.get_case(case_id)
|
| 58 |
+
if not case:
|
| 59 |
+
raise HTTPException(status_code=404, detail="Case not found")
|
| 60 |
+
return case
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
@router.post("/{case_id}/action")
|
| 64 |
+
async def case_action(case_id: str, request: CaseActionRequest):
|
| 65 |
+
result = case_generator.process_action(case_id, request.action_type, request.student_input)
|
| 66 |
+
return result
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
@router.post("/{case_id}/diagnose")
|
| 70 |
+
async def submit_diagnosis(case_id: str, request: DiagnosisRequest):
|
| 71 |
+
result = case_generator.evaluate_diagnosis(case_id, request.diagnosis, request.reasoning)
|
| 72 |
+
return result
|
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter
|
| 2 |
+
from pydantic import BaseModel
|
| 3 |
+
from typing import Optional
|
| 4 |
+
|
| 5 |
+
router = APIRouter()
|
| 6 |
+
|
| 7 |
+
# In-memory student data (demo purposes)
|
| 8 |
+
DEMO_STUDENT = {
|
| 9 |
+
"id": "student-001",
|
| 10 |
+
"name": "Medical Student",
|
| 11 |
+
"year_level": "final_year",
|
| 12 |
+
"cases_completed": 48,
|
| 13 |
+
"accuracy": 75,
|
| 14 |
+
"avg_time": 10,
|
| 15 |
+
"percentile": 15,
|
| 16 |
+
"specialty_scores": {
|
| 17 |
+
"cardiology": 82,
|
| 18 |
+
"respiratory": 65,
|
| 19 |
+
"infectious": 78,
|
| 20 |
+
"neurology": 45,
|
| 21 |
+
"gastro": 70,
|
| 22 |
+
"emergency": 55,
|
| 23 |
+
},
|
| 24 |
+
"weak_areas": ["neurology", "emergency"],
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class ProfileUpdate(BaseModel):
|
| 29 |
+
name: Optional[str] = None
|
| 30 |
+
year_level: Optional[str] = None
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@router.get("/profile")
|
| 34 |
+
async def get_profile():
|
| 35 |
+
return DEMO_STUDENT
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@router.put("/profile")
|
| 39 |
+
async def update_profile(update: ProfileUpdate):
|
| 40 |
+
if update.name:
|
| 41 |
+
DEMO_STUDENT["name"] = update.name
|
| 42 |
+
if update.year_level:
|
| 43 |
+
DEMO_STUDENT["year_level"] = update.year_level
|
| 44 |
+
return DEMO_STUDENT
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
@router.get("/biases")
|
| 48 |
+
async def get_biases():
|
| 49 |
+
from app.core.analytics.bias_detector import BiasDetector
|
| 50 |
+
detector = BiasDetector()
|
| 51 |
+
return detector.generate_demo_report()
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
@router.get("/knowledge-graph")
|
| 55 |
+
async def get_knowledge_graph():
|
| 56 |
+
from app.core.analytics.knowledge_graph import KnowledgeGraphBuilder
|
| 57 |
+
builder = KnowledgeGraphBuilder()
|
| 58 |
+
return builder.build_demo_graph()
|
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Core modules
|
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# AI Agents
|
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from typing import Optional
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class SocraticTutor:
|
| 6 |
+
"""AI tutor that uses Socratic method to guide clinical reasoning."""
|
| 7 |
+
|
| 8 |
+
def __init__(self):
|
| 9 |
+
self.conversation_history: list = []
|
| 10 |
+
self.api_key = os.environ.get("ANTHROPIC_API_KEY")
|
| 11 |
+
|
| 12 |
+
def respond(self, student_message: str, case_context: dict) -> str:
|
| 13 |
+
"""Generate Socratic response to student's reasoning."""
|
| 14 |
+
|
| 15 |
+
# For demo, use pre-built responses
|
| 16 |
+
# In production, this would call Claude API
|
| 17 |
+
self.conversation_history.append({
|
| 18 |
+
"role": "user",
|
| 19 |
+
"content": student_message,
|
| 20 |
+
})
|
| 21 |
+
|
| 22 |
+
response = self._generate_socratic_response(student_message, case_context)
|
| 23 |
+
|
| 24 |
+
self.conversation_history.append({
|
| 25 |
+
"role": "assistant",
|
| 26 |
+
"content": response,
|
| 27 |
+
})
|
| 28 |
+
|
| 29 |
+
return response
|
| 30 |
+
|
| 31 |
+
def _generate_socratic_response(self, message: str, context: dict) -> str:
|
| 32 |
+
"""Generate a teaching response using Socratic method."""
|
| 33 |
+
|
| 34 |
+
message_lower = message.lower()
|
| 35 |
+
|
| 36 |
+
if any(word in message_lower for word in ["heart attack", "mi", "stemi", "acs"]):
|
| 37 |
+
return "You're considering an acute coronary event. That's a reasonable starting point given the presentation. But what features of this case are unusual for a typical MI? What risk factors stand out?"
|
| 38 |
+
|
| 39 |
+
if any(word in message_lower for word in ["cocaine", "drug", "substance"]):
|
| 40 |
+
return "Excellent observation about the substance use. How does cocaine specifically affect the coronary vasculature? And critically - how does this change your management compared to a standard ACS protocol?"
|
| 41 |
+
|
| 42 |
+
if any(word in message_lower for word in ["pe", "embolism", "dvt"]):
|
| 43 |
+
return "Pulmonary embolism is an important differential for chest pain. What clinical features would help you distinguish PE from ACS in this patient? What investigation would be most helpful?"
|
| 44 |
+
|
| 45 |
+
if any(word in message_lower for word in ["beta blocker", "metoprolol", "atenolol"]):
|
| 46 |
+
return "Think carefully about beta-blockers in this context. What happens physiologically when you block beta-receptors in a patient with cocaine on board? This is a critical management distinction."
|
| 47 |
+
|
| 48 |
+
if len(self.conversation_history) <= 2:
|
| 49 |
+
return "Let's think through this systematically. What are the most dangerous causes of chest pain you need to rule out first? Start with your differential diagnosis."
|
| 50 |
+
|
| 51 |
+
return "Good thinking. Can you explain your reasoning further? What evidence supports your hypothesis, and what evidence might contradict it?"
|
| 52 |
+
|
| 53 |
+
def reset(self):
|
| 54 |
+
"""Reset conversation for a new case."""
|
| 55 |
+
self.conversation_history = []
|
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Analytics modules
|
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime
|
| 2 |
+
from typing import Optional
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class BiasDetector:
|
| 6 |
+
"""Detects cognitive biases from student decision patterns."""
|
| 7 |
+
|
| 8 |
+
def __init__(self):
|
| 9 |
+
self.student_history: list = []
|
| 10 |
+
|
| 11 |
+
def add_case_result(self, case_id: str, student_actions: list, diagnosis: str, correct: bool):
|
| 12 |
+
self.student_history.append({
|
| 13 |
+
"case_id": case_id,
|
| 14 |
+
"actions": student_actions,
|
| 15 |
+
"diagnosis": diagnosis,
|
| 16 |
+
"correct": correct,
|
| 17 |
+
"timestamp": datetime.now().isoformat(),
|
| 18 |
+
})
|
| 19 |
+
|
| 20 |
+
def detect_anchoring_bias(self) -> Optional[dict]:
|
| 21 |
+
recent = self.student_history[-10:] if len(self.student_history) >= 10 else self.student_history
|
| 22 |
+
if not recent:
|
| 23 |
+
return None
|
| 24 |
+
|
| 25 |
+
anchoring_count = sum(
|
| 26 |
+
1 for case in recent
|
| 27 |
+
if len(case.get("actions", [])) > 0
|
| 28 |
+
and case["actions"][0].get("diagnosis") == case["diagnosis"]
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
if anchoring_count >= 7:
|
| 32 |
+
return {
|
| 33 |
+
"bias": "anchoring",
|
| 34 |
+
"severity": "moderate",
|
| 35 |
+
"score": anchoring_count * 10,
|
| 36 |
+
"evidence": f"Stuck with initial diagnosis in {anchoring_count}/{len(recent)} cases",
|
| 37 |
+
"recommendation": "Practice cases with atypical presentations. Force yourself to reconsider after each new piece of information.",
|
| 38 |
+
}
|
| 39 |
+
return None
|
| 40 |
+
|
| 41 |
+
def detect_premature_closure(self) -> Optional[dict]:
|
| 42 |
+
recent = self.student_history[-10:] if len(self.student_history) >= 10 else self.student_history
|
| 43 |
+
if not recent:
|
| 44 |
+
return None
|
| 45 |
+
|
| 46 |
+
premature_count = sum(
|
| 47 |
+
1 for case in recent
|
| 48 |
+
if len(case.get("actions", {}).get("differential_list", [])) < 3
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
if premature_count >= 6:
|
| 52 |
+
return {
|
| 53 |
+
"bias": "premature_closure",
|
| 54 |
+
"severity": "high",
|
| 55 |
+
"score": premature_count * 10,
|
| 56 |
+
"evidence": f"Only considered 1-2 diagnoses in {premature_count}/{len(recent)} cases",
|
| 57 |
+
"recommendation": "Force yourself to list 3+ differential diagnoses before deciding.",
|
| 58 |
+
}
|
| 59 |
+
return None
|
| 60 |
+
|
| 61 |
+
def generate_bias_report(self) -> dict:
|
| 62 |
+
biases = []
|
| 63 |
+
anchoring = self.detect_anchoring_bias()
|
| 64 |
+
if anchoring:
|
| 65 |
+
biases.append(anchoring)
|
| 66 |
+
premature = self.detect_premature_closure()
|
| 67 |
+
if premature:
|
| 68 |
+
biases.append(premature)
|
| 69 |
+
|
| 70 |
+
return {
|
| 71 |
+
"biases_detected": biases,
|
| 72 |
+
"cases_analyzed": len(self.student_history),
|
| 73 |
+
"overall_accuracy": self._calculate_accuracy(),
|
| 74 |
+
"generated_at": datetime.now().isoformat(),
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
def generate_demo_report(self) -> dict:
|
| 78 |
+
return {
|
| 79 |
+
"biases_detected": [
|
| 80 |
+
{
|
| 81 |
+
"type": "anchoring",
|
| 82 |
+
"severity": "moderate",
|
| 83 |
+
"score": 65,
|
| 84 |
+
"evidence": "You stuck with your initial diagnosis in 7 out of 10 recent cases, even when new information contradicted it.",
|
| 85 |
+
"recommendation": "Practice cases with atypical presentations. Force yourself to reconsider after each new piece of information.",
|
| 86 |
+
},
|
| 87 |
+
{
|
| 88 |
+
"type": "premature_closure",
|
| 89 |
+
"severity": "low",
|
| 90 |
+
"score": 40,
|
| 91 |
+
"evidence": "In 4 out of 10 cases, you considered fewer than 3 differential diagnoses before settling on your answer.",
|
| 92 |
+
"recommendation": "Before finalizing, always list at least 3 differential diagnoses and explain why you're ruling each one out.",
|
| 93 |
+
},
|
| 94 |
+
{
|
| 95 |
+
"type": "availability",
|
| 96 |
+
"severity": "moderate",
|
| 97 |
+
"score": 55,
|
| 98 |
+
"evidence": "After studying cardiology, you diagnosed 3 consecutive non-cardiac cases as cardiac. Your recent study focus influenced your diagnoses.",
|
| 99 |
+
"recommendation": "Before diagnosing, list 3 differential diagnoses from different organ systems.",
|
| 100 |
+
},
|
| 101 |
+
{
|
| 102 |
+
"type": "confirmation",
|
| 103 |
+
"severity": "low",
|
| 104 |
+
"score": 30,
|
| 105 |
+
"evidence": "Minimal confirmation bias detected. You generally consider contradicting evidence.",
|
| 106 |
+
"recommendation": "Continue actively seeking evidence that contradicts your working diagnosis.",
|
| 107 |
+
},
|
| 108 |
+
],
|
| 109 |
+
"cases_analyzed": 48,
|
| 110 |
+
"overall_accuracy": 75,
|
| 111 |
+
"generated_at": datetime.now().isoformat(),
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
def _calculate_accuracy(self) -> float:
|
| 115 |
+
if not self.student_history:
|
| 116 |
+
return 0.0
|
| 117 |
+
correct = sum(1 for case in self.student_history if case.get("correct"))
|
| 118 |
+
return round(correct / len(self.student_history) * 100, 1)
|
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class KnowledgeGraphBuilder:
|
| 2 |
+
"""Builds knowledge graph from student case history."""
|
| 3 |
+
|
| 4 |
+
def __init__(self):
|
| 5 |
+
self.concepts: dict = {}
|
| 6 |
+
self.connections: list = []
|
| 7 |
+
|
| 8 |
+
def update_concept(self, concept: str, correct: bool):
|
| 9 |
+
if concept not in self.concepts:
|
| 10 |
+
self.concepts[concept] = {"correct": 0, "total": 0}
|
| 11 |
+
self.concepts[concept]["total"] += 1
|
| 12 |
+
if correct:
|
| 13 |
+
self.concepts[concept]["correct"] += 1
|
| 14 |
+
|
| 15 |
+
def add_connection(self, source: str, target: str, correct: bool):
|
| 16 |
+
connection_id = f"{source}-{target}"
|
| 17 |
+
existing = next((c for c in self.connections if c["id"] == connection_id), None)
|
| 18 |
+
if existing:
|
| 19 |
+
existing["total"] += 1
|
| 20 |
+
if correct:
|
| 21 |
+
existing["correct"] += 1
|
| 22 |
+
else:
|
| 23 |
+
self.connections.append({
|
| 24 |
+
"id": connection_id,
|
| 25 |
+
"source": source,
|
| 26 |
+
"target": target,
|
| 27 |
+
"correct": 1 if correct else 0,
|
| 28 |
+
"total": 1,
|
| 29 |
+
})
|
| 30 |
+
|
| 31 |
+
def to_graph_data(self) -> dict:
|
| 32 |
+
nodes = [
|
| 33 |
+
{"id": concept, "strength": data["correct"] / max(data["total"], 1), "size": data["total"]}
|
| 34 |
+
for concept, data in self.concepts.items()
|
| 35 |
+
]
|
| 36 |
+
links = [
|
| 37 |
+
{"source": conn["source"], "target": conn["target"], "strength": conn["correct"] / max(conn["total"], 1)}
|
| 38 |
+
for conn in self.connections
|
| 39 |
+
]
|
| 40 |
+
return {"nodes": nodes, "links": links}
|
| 41 |
+
|
| 42 |
+
def build_demo_graph(self) -> dict:
|
| 43 |
+
return {
|
| 44 |
+
"nodes": [
|
| 45 |
+
{"id": "Cardiology", "strength": 0.82, "size": 12, "category": "specialty"},
|
| 46 |
+
{"id": "Respiratory", "strength": 0.65, "size": 8, "category": "specialty"},
|
| 47 |
+
{"id": "Infectious", "strength": 0.78, "size": 10, "category": "specialty"},
|
| 48 |
+
{"id": "Neurology", "strength": 0.45, "size": 5, "category": "specialty"},
|
| 49 |
+
{"id": "Gastro", "strength": 0.70, "size": 7, "category": "specialty"},
|
| 50 |
+
{"id": "Emergency", "strength": 0.55, "size": 6, "category": "specialty"},
|
| 51 |
+
{"id": "STEMI", "strength": 0.85, "size": 8, "category": "diagnosis"},
|
| 52 |
+
{"id": "Pulmonary Embolism", "strength": 0.40, "size": 4, "category": "diagnosis"},
|
| 53 |
+
{"id": "Dengue", "strength": 0.80, "size": 9, "category": "diagnosis"},
|
| 54 |
+
{"id": "Pneumonia", "strength": 0.72, "size": 7, "category": "diagnosis"},
|
| 55 |
+
{"id": "Meningitis", "strength": 0.35, "size": 3, "category": "diagnosis"},
|
| 56 |
+
{"id": "Chest Pain", "strength": 0.90, "size": 10, "category": "symptom"},
|
| 57 |
+
{"id": "Dyspnea", "strength": 0.75, "size": 8, "category": "symptom"},
|
| 58 |
+
{"id": "Fever", "strength": 0.85, "size": 11, "category": "symptom"},
|
| 59 |
+
{"id": "Headache", "strength": 0.60, "size": 6, "category": "symptom"},
|
| 60 |
+
{"id": "ECG", "strength": 0.88, "size": 9, "category": "investigation"},
|
| 61 |
+
{"id": "Troponin", "strength": 0.80, "size": 7, "category": "investigation"},
|
| 62 |
+
],
|
| 63 |
+
"links": [
|
| 64 |
+
{"source": "Chest Pain", "target": "STEMI", "strength": 0.9},
|
| 65 |
+
{"source": "Chest Pain", "target": "Pulmonary Embolism", "strength": 0.3},
|
| 66 |
+
{"source": "Cardiology", "target": "STEMI", "strength": 0.9},
|
| 67 |
+
{"source": "STEMI", "target": "ECG", "strength": 0.9},
|
| 68 |
+
{"source": "STEMI", "target": "Troponin", "strength": 0.85},
|
| 69 |
+
{"source": "Dyspnea", "target": "Pneumonia", "strength": 0.75},
|
| 70 |
+
{"source": "Dyspnea", "target": "Pulmonary Embolism", "strength": 0.35},
|
| 71 |
+
{"source": "Respiratory", "target": "Pneumonia", "strength": 0.72},
|
| 72 |
+
{"source": "Fever", "target": "Dengue", "strength": 0.8},
|
| 73 |
+
{"source": "Fever", "target": "Infectious", "strength": 0.78},
|
| 74 |
+
{"source": "Fever", "target": "Meningitis", "strength": 0.4},
|
| 75 |
+
{"source": "Infectious", "target": "Dengue", "strength": 0.82},
|
| 76 |
+
{"source": "Headache", "target": "Meningitis", "strength": 0.35},
|
| 77 |
+
{"source": "Headache", "target": "Neurology", "strength": 0.48},
|
| 78 |
+
],
|
| 79 |
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class CaseRecommender:
|
| 2 |
+
"""Recommends next cases based on student needs."""
|
| 3 |
+
|
| 4 |
+
def recommend(self, student_profile: dict) -> list:
|
| 5 |
+
recommendations = []
|
| 6 |
+
specialty_scores = student_profile.get("specialty_scores", {})
|
| 7 |
+
|
| 8 |
+
weak_specialties = [s for s, score in specialty_scores.items() if score < 60]
|
| 9 |
+
if weak_specialties:
|
| 10 |
+
recommendations.append({
|
| 11 |
+
"type": "weak_area",
|
| 12 |
+
"specialty": weak_specialties[0],
|
| 13 |
+
"difficulty": "beginner",
|
| 14 |
+
"reason": f"Your {weak_specialties[0]} accuracy is only {specialty_scores[weak_specialties[0]]}%",
|
| 15 |
+
"priority": "high",
|
| 16 |
+
})
|
| 17 |
+
|
| 18 |
+
if student_profile.get("biases", {}).get("anchoring"):
|
| 19 |
+
recommendations.append({
|
| 20 |
+
"type": "bias_counter",
|
| 21 |
+
"specialty": "mixed",
|
| 22 |
+
"difficulty": "intermediate",
|
| 23 |
+
"reason": "Atypical presentation cases to reduce anchoring bias",
|
| 24 |
+
"priority": "medium",
|
| 25 |
+
})
|
| 26 |
+
|
| 27 |
+
strong_specialties = [s for s, score in specialty_scores.items() if score > 80]
|
| 28 |
+
if strong_specialties:
|
| 29 |
+
recommendations.append({
|
| 30 |
+
"type": "challenge",
|
| 31 |
+
"specialty": strong_specialties[0],
|
| 32 |
+
"difficulty": "advanced",
|
| 33 |
+
"reason": f"Your {strong_specialties[0]} accuracy is {specialty_scores[strong_specialties[0]]}%. Ready for advanced cases!",
|
| 34 |
+
"priority": "low",
|
| 35 |
+
})
|
| 36 |
+
|
| 37 |
+
return recommendations
|
| 38 |
+
|
| 39 |
+
def get_demo_recommendations(self) -> list:
|
| 40 |
+
return [
|
| 41 |
+
{
|
| 42 |
+
"type": "weak_area",
|
| 43 |
+
"specialty": "Neurology",
|
| 44 |
+
"difficulty": "beginner",
|
| 45 |
+
"reason": "Your neurology accuracy is only 45%. Let's strengthen this foundation.",
|
| 46 |
+
"priority": "high",
|
| 47 |
+
},
|
| 48 |
+
{
|
| 49 |
+
"type": "bias_counter",
|
| 50 |
+
"specialty": "Mixed",
|
| 51 |
+
"difficulty": "intermediate",
|
| 52 |
+
"reason": "Atypical presentation cases to reduce your anchoring bias pattern.",
|
| 53 |
+
"priority": "medium",
|
| 54 |
+
},
|
| 55 |
+
{
|
| 56 |
+
"type": "challenge",
|
| 57 |
+
"specialty": "Cardiology",
|
| 58 |
+
"difficulty": "advanced",
|
| 59 |
+
"reason": "Your cardiology accuracy is 82%. Ready for advanced cases!",
|
| 60 |
+
"priority": "low",
|
| 61 |
+
},
|
| 62 |
+
]
|
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# RAG system
|
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import uuid
|
| 3 |
+
from typing import Optional
|
| 4 |
+
|
| 5 |
+
# Sample cases for demo (in production, these would be generated via Claude API + RAG)
|
| 6 |
+
SAMPLE_CASES = {
|
| 7 |
+
"cardiology": [
|
| 8 |
+
{
|
| 9 |
+
"patient": {"age": 28, "gender": "Male", "location": "Mumbai"},
|
| 10 |
+
"chief_complaint": "Acute onset chest pain radiating to left arm",
|
| 11 |
+
"initial_presentation": "A 28-year-old male software engineer presents to the ED at 2 AM with crushing chest pain that started 45 minutes ago while sleeping. He appears anxious, diaphoretic, and is clutching his chest. He denies previous cardiac history but mentions significant work stress and recent cocaine use at a party.",
|
| 12 |
+
"vital_signs": {"bp": "160/95", "hr": 110, "rr": 22, "temp": 37.2, "spo2": 96},
|
| 13 |
+
"stages": [
|
| 14 |
+
{"stage": "history", "info": "Pain started suddenly, woke him from sleep. Severity: 8/10, crushing quality. Radiates to left arm and jaw. Associated with nausea and sweating. He admits to cocaine use ~3 hours ago. No prior cardiac history. Family history: father had MI at age 52. Smokes 5 cigarettes/day."},
|
| 15 |
+
{"stage": "physical_exam", "info": "Alert, anxious, diaphoretic. Cardiovascular: Tachycardic, regular rhythm, no murmurs. S1/S2 normal. JVP not elevated. Chest: Clear bilateral air entry. Abdomen: Soft, non-tender. Extremities: No edema. Pupils dilated bilaterally."},
|
| 16 |
+
{"stage": "labs", "info": "ECG: Sinus tachycardia, ST elevation in leads II, III, aVF (inferior leads). Troponin I: 0.8 ng/mL (elevated, normal <0.04). CBC: Normal. BMP: Normal. Urine drug screen: Positive for cocaine. CXR: Normal cardiac silhouette."},
|
| 17 |
+
],
|
| 18 |
+
"diagnosis": "Cocaine-induced acute coronary syndrome (inferior STEMI)",
|
| 19 |
+
"differentials": ["Acute coronary syndrome (STEMI)", "Cocaine-induced coronary vasospasm", "Aortic dissection", "Pulmonary embolism", "Pericarditis"],
|
| 20 |
+
"learning_points": ["Always screen for substance use in young patients with ACS", "Cocaine causes coronary vasospasm AND increases myocardial oxygen demand", "Beta-blockers are contraindicated in cocaine-induced ACS", "First-line: Benzodiazepines + Nitroglycerin"],
|
| 21 |
+
"atypical_features": "Young age, cocaine use as precipitant, inferior STEMI pattern",
|
| 22 |
+
"specialty": "cardiology",
|
| 23 |
+
"difficulty": "intermediate",
|
| 24 |
+
}
|
| 25 |
+
],
|
| 26 |
+
"infectious": [
|
| 27 |
+
{
|
| 28 |
+
"patient": {"age": 35, "gender": "Female", "location": "Kochi, Kerala"},
|
| 29 |
+
"chief_complaint": "High-grade fever for 5 days with body aches",
|
| 30 |
+
"initial_presentation": "A 35-year-old housewife presents during monsoon season with 5 days of high-grade fever (103-104\u00b0F), severe myalgia, retro-orbital pain, and a macular rash on her trunk. She lives near a construction site with stagnant water. Her neighbor was recently hospitalized for dengue.",
|
| 31 |
+
"vital_signs": {"bp": "100/70", "hr": 96, "rr": 18, "temp": 39.4, "spo2": 98},
|
| 32 |
+
"stages": [
|
| 33 |
+
{"stage": "history", "info": "Fever started 5 days ago, initially intermittent, now continuous. Severe headache, especially behind the eyes. Diffuse body aches. Noticed rash on trunk 2 days ago. Mild nausea, reduced appetite. No bleeding from any site. LMP: 2 weeks ago, regular. Lives near construction site with stagnant water pools."},
|
| 34 |
+
{"stage": "physical_exam", "info": "Febrile, flushed. Macular blanching rash on trunk. Positive tourniquet test. No petechiae. Mild hepatomegaly (2 cm below costal margin). No splenomegaly. No lymphadenopathy. No signs of plasma leakage. Alert and oriented."},
|
| 35 |
+
{"stage": "labs", "info": "CBC: WBC 3,200 (low), Platelets 45,000 (critically low), Hct 42%. NS1 Antigen: Positive. Dengue IgM: Positive. LFT: AST 180, ALT 120 (elevated). PT/INR: Normal. CRP: Elevated. Blood smear: No malarial parasites."},
|
| 36 |
+
],
|
| 37 |
+
"diagnosis": "Dengue fever with warning signs (approaching critical phase)",
|
| 38 |
+
"differentials": ["Dengue fever", "Chikungunya", "Malaria", "Typhoid fever", "Leptospirosis"],
|
| 39 |
+
"learning_points": ["Warning signs: Abdominal pain, persistent vomiting, fluid accumulation, mucosal bleeding, lethargy, hepatomegaly >2cm, increasing hematocrit with decreasing platelets", "Critical phase occurs around day 3-7 of illness (defervescence)", "Fluid management is the cornerstone - avoid excessive fluids", "No role for prophylactic platelet transfusion if no active bleeding", "Monitor hematocrit every 6-12 hours during critical phase"],
|
| 40 |
+
"atypical_features": "Classic presentation but requires careful monitoring for transition to severe dengue",
|
| 41 |
+
"specialty": "infectious",
|
| 42 |
+
"difficulty": "beginner",
|
| 43 |
+
}
|
| 44 |
+
],
|
| 45 |
+
"neurology": [
|
| 46 |
+
{
|
| 47 |
+
"patient": {"age": 42, "gender": "Male", "location": "Delhi"},
|
| 48 |
+
"chief_complaint": "Progressive weakness in legs for 3 days",
|
| 49 |
+
"initial_presentation": "A 42-year-old schoolteacher presents with progressive ascending weakness starting in both feet 3 days ago, now reaching his thighs. He had a viral upper respiratory infection 2 weeks ago. He reports tingling in his fingers since this morning and is having difficulty climbing stairs.",
|
| 50 |
+
"vital_signs": {"bp": "130/80", "hr": 88, "rr": 20, "temp": 37.0, "spo2": 97},
|
| 51 |
+
"stages": [
|
| 52 |
+
{"stage": "history", "info": "Weakness started in feet, ascending to thighs over 3 days. Symmetric involvement. Tingling/numbness in fingertips since morning. Had a 'cold' 2 weeks ago with sore throat and runny nose. Back pain between shoulder blades. No bowel/bladder dysfunction yet. No recent vaccinations. No travel history."},
|
| 53 |
+
{"stage": "physical_exam", "info": "Power: Lower limbs 3/5 proximally, 2/5 distally. Upper limbs 4/5. Deep tendon reflexes: Absent in lower limbs, reduced in upper limbs. Plantars: Mute bilaterally. Sensation: Glove-and-stocking pattern of reduced light touch. Cranial nerves: Intact. Respiratory: Using accessory muscles slightly. FVC: 1.8L (predicted 3.5L) - CRITICAL."},
|
| 54 |
+
{"stage": "labs", "info": "NCS/EMG: Demyelinating pattern with prolonged distal latencies, reduced conduction velocities, conduction blocks. CSF: Protein 1.2 g/L (elevated), WBC 3 cells (albuminocytological dissociation). MRI spine: Enhancement of cauda equina nerve roots. ABG: pH 7.38, pCO2 44 (borderline). Spirometry: FVC declining - was 2.2L yesterday."},
|
| 55 |
+
],
|
| 56 |
+
"diagnosis": "Guillain-Barr\u00e9 Syndrome (acute inflammatory demyelinating polyneuropathy) with impending respiratory failure",
|
| 57 |
+
"differentials": ["Guillain-Barr\u00e9 Syndrome", "Transverse myelitis", "Myasthenia gravis", "Hypokalemic periodic paralysis", "Acute poliomyelitis"],
|
| 58 |
+
"learning_points": ["FVC < 20 mL/kg or declining FVC is an indication for ICU admission and potential intubation", "Albuminocytological dissociation (high protein, normal cells) in CSF is characteristic", "Treatment: IVIg or plasmapheresis - both equally effective", "20-30% of GBS patients require mechanical ventilation", "Autonomic dysfunction (BP fluctuations, arrhythmias) is a major cause of mortality"],
|
| 59 |
+
"atypical_features": "Rapidly progressive with early respiratory compromise - requires urgent intervention",
|
| 60 |
+
"specialty": "neurology",
|
| 61 |
+
"difficulty": "advanced",
|
| 62 |
+
}
|
| 63 |
+
],
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
class CaseGenerator:
|
| 68 |
+
def __init__(self):
|
| 69 |
+
self.active_cases: dict = {}
|
| 70 |
+
|
| 71 |
+
def generate_case(self, specialty: str, difficulty: str = "intermediate", year_level: str = "final_year") -> dict:
|
| 72 |
+
case_id = str(uuid.uuid4())[:8]
|
| 73 |
+
|
| 74 |
+
# Get sample case for the specialty
|
| 75 |
+
specialty_cases = SAMPLE_CASES.get(specialty, SAMPLE_CASES.get("cardiology", []))
|
| 76 |
+
if not specialty_cases:
|
| 77 |
+
specialty_cases = list(SAMPLE_CASES.values())[0]
|
| 78 |
+
|
| 79 |
+
case_data = specialty_cases[0].copy()
|
| 80 |
+
case_data["id"] = case_id
|
| 81 |
+
|
| 82 |
+
self.active_cases[case_id] = case_data
|
| 83 |
+
return case_data
|
| 84 |
+
|
| 85 |
+
def get_case(self, case_id: str) -> Optional[dict]:
|
| 86 |
+
return self.active_cases.get(case_id)
|
| 87 |
+
|
| 88 |
+
def process_action(self, case_id: str, action_type: str, student_input: Optional[str] = None) -> dict:
|
| 89 |
+
case = self.active_cases.get(case_id)
|
| 90 |
+
if not case:
|
| 91 |
+
return {"error": "Case not found"}
|
| 92 |
+
|
| 93 |
+
stage_map = {
|
| 94 |
+
"take_history": 0,
|
| 95 |
+
"physical_exam": 1,
|
| 96 |
+
"order_labs": 2,
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
stage_index = stage_map.get(action_type)
|
| 100 |
+
if stage_index is not None and stage_index < len(case.get("stages", [])):
|
| 101 |
+
return {
|
| 102 |
+
"action": action_type,
|
| 103 |
+
"result": case["stages"][stage_index],
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
return {"action": action_type, "result": "Action processed"}
|
| 107 |
+
|
| 108 |
+
def evaluate_diagnosis(self, case_id: str, diagnosis: str, reasoning: str = "") -> dict:
|
| 109 |
+
case = self.active_cases.get(case_id)
|
| 110 |
+
if not case:
|
| 111 |
+
return {"error": "Case not found"}
|
| 112 |
+
|
| 113 |
+
correct_diagnosis = case.get("diagnosis", "").lower()
|
| 114 |
+
student_diagnosis = diagnosis.lower()
|
| 115 |
+
|
| 116 |
+
# Simple matching
|
| 117 |
+
is_correct = any(
|
| 118 |
+
keyword in student_diagnosis
|
| 119 |
+
for keyword in correct_diagnosis.split()
|
| 120 |
+
if len(keyword) > 3
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
return {
|
| 124 |
+
"student_diagnosis": diagnosis,
|
| 125 |
+
"correct_diagnosis": case["diagnosis"],
|
| 126 |
+
"is_correct": is_correct,
|
| 127 |
+
"differentials": case.get("differentials", []),
|
| 128 |
+
"learning_points": case.get("learning_points", []),
|
| 129 |
+
"feedback": "Excellent clinical reasoning!" if is_correct else f"The correct diagnosis is {case['diagnosis']}. Review the key learning points.",
|
| 130 |
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI
|
| 2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
+
from app.api import cases, student, analytics
|
| 4 |
+
|
| 5 |
+
app = FastAPI(
|
| 6 |
+
title="Clinical-Mind API",
|
| 7 |
+
description="AI-powered clinical reasoning simulator for medical students",
|
| 8 |
+
version="1.0.0",
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
app.add_middleware(
|
| 12 |
+
CORSMiddleware,
|
| 13 |
+
allow_origins=["http://localhost:3000", "http://localhost:5173"],
|
| 14 |
+
allow_credentials=True,
|
| 15 |
+
allow_methods=["*"],
|
| 16 |
+
allow_headers=["*"],
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
app.include_router(cases.router, prefix="/api/cases", tags=["cases"])
|
| 20 |
+
app.include_router(student.router, prefix="/api/student", tags=["student"])
|
| 21 |
+
app.include_router(analytics.router, prefix="/api/analytics", tags=["analytics"])
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@app.get("/")
|
| 25 |
+
async def root():
|
| 26 |
+
return {"message": "Clinical-Mind API", "version": "1.0.0"}
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@app.get("/health")
|
| 30 |
+
async def health():
|
| 31 |
+
return {"status": "healthy"}
|
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Data models
|
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Utilities
|
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.115.0
|
| 2 |
+
uvicorn==0.30.0
|
| 3 |
+
anthropic==0.39.0
|
| 4 |
+
chromadb==0.5.0
|
| 5 |
+
langchain==0.3.0
|
| 6 |
+
langchain-community==0.3.0
|
| 7 |
+
sentence-transformers==3.0.0
|
| 8 |
+
pydantic==2.9.0
|
| 9 |
+
python-dotenv==1.0.0
|
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
| 2 |
+
|
| 3 |
+
# dependencies
|
| 4 |
+
/node_modules
|
| 5 |
+
/.pnp
|
| 6 |
+
.pnp.js
|
| 7 |
+
|
| 8 |
+
# testing
|
| 9 |
+
/coverage
|
| 10 |
+
|
| 11 |
+
# production
|
| 12 |
+
/build
|
| 13 |
+
|
| 14 |
+
# misc
|
| 15 |
+
.DS_Store
|
| 16 |
+
.env.local
|
| 17 |
+
.env.development.local
|
| 18 |
+
.env.test.local
|
| 19 |
+
.env.production.local
|
| 20 |
+
|
| 21 |
+
npm-debug.log*
|
| 22 |
+
yarn-debug.log*
|
| 23 |
+
yarn-error.log*
|
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Getting Started with Create React App
|
| 2 |
+
|
| 3 |
+
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
| 4 |
+
|
| 5 |
+
## Available Scripts
|
| 6 |
+
|
| 7 |
+
In the project directory, you can run:
|
| 8 |
+
|
| 9 |
+
### `npm start`
|
| 10 |
+
|
| 11 |
+
Runs the app in the development mode.\
|
| 12 |
+
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
| 13 |
+
|
| 14 |
+
The page will reload if you make edits.\
|
| 15 |
+
You will also see any lint errors in the console.
|
| 16 |
+
|
| 17 |
+
### `npm test`
|
| 18 |
+
|
| 19 |
+
Launches the test runner in the interactive watch mode.\
|
| 20 |
+
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
| 21 |
+
|
| 22 |
+
### `npm run build`
|
| 23 |
+
|
| 24 |
+
Builds the app for production to the `build` folder.\
|
| 25 |
+
It correctly bundles React in production mode and optimizes the build for the best performance.
|
| 26 |
+
|
| 27 |
+
The build is minified and the filenames include the hashes.\
|
| 28 |
+
Your app is ready to be deployed!
|
| 29 |
+
|
| 30 |
+
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
| 31 |
+
|
| 32 |
+
### `npm run eject`
|
| 33 |
+
|
| 34 |
+
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
| 35 |
+
|
| 36 |
+
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
| 37 |
+
|
| 38 |
+
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
| 39 |
+
|
| 40 |
+
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
| 41 |
+
|
| 42 |
+
## Learn More
|
| 43 |
+
|
| 44 |
+
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
| 45 |
+
|
| 46 |
+
To learn React, check out the [React documentation](https://reactjs.org/).
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "frontend",
|
| 3 |
+
"version": "0.1.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"dependencies": {
|
| 6 |
+
"@testing-library/dom": "^10.4.1",
|
| 7 |
+
"@testing-library/jest-dom": "^6.9.1",
|
| 8 |
+
"@testing-library/react": "^16.3.2",
|
| 9 |
+
"@testing-library/user-event": "^13.5.0",
|
| 10 |
+
"@types/d3": "^7.4.3",
|
| 11 |
+
"@types/jest": "^27.5.2",
|
| 12 |
+
"@types/node": "^16.18.126",
|
| 13 |
+
"@types/react": "^19.2.13",
|
| 14 |
+
"@types/react-dom": "^19.2.3",
|
| 15 |
+
"@types/recharts": "^1.8.29",
|
| 16 |
+
"axios": "^1.13.5",
|
| 17 |
+
"d3": "^7.9.0",
|
| 18 |
+
"react": "^19.2.4",
|
| 19 |
+
"react-dom": "^19.2.4",
|
| 20 |
+
"react-router-dom": "^7.13.0",
|
| 21 |
+
"react-scripts": "5.0.1",
|
| 22 |
+
"recharts": "^3.7.0",
|
| 23 |
+
"typescript": "^4.9.5",
|
| 24 |
+
"web-vitals": "^2.1.4"
|
| 25 |
+
},
|
| 26 |
+
"scripts": {
|
| 27 |
+
"start": "react-scripts start",
|
| 28 |
+
"build": "react-scripts build",
|
| 29 |
+
"test": "react-scripts test",
|
| 30 |
+
"eject": "react-scripts eject"
|
| 31 |
+
},
|
| 32 |
+
"eslintConfig": {
|
| 33 |
+
"extends": [
|
| 34 |
+
"react-app",
|
| 35 |
+
"react-app/jest"
|
| 36 |
+
]
|
| 37 |
+
},
|
| 38 |
+
"browserslist": {
|
| 39 |
+
"production": [
|
| 40 |
+
">0.2%",
|
| 41 |
+
"not dead",
|
| 42 |
+
"not op_mini all"
|
| 43 |
+
],
|
| 44 |
+
"development": [
|
| 45 |
+
"last 1 chrome version",
|
| 46 |
+
"last 1 firefox version",
|
| 47 |
+
"last 1 safari version"
|
| 48 |
+
]
|
| 49 |
+
},
|
| 50 |
+
"devDependencies": {
|
| 51 |
+
"@tailwindcss/postcss": "^4.1.18",
|
| 52 |
+
"autoprefixer": "^10.4.24",
|
| 53 |
+
"postcss": "^8.5.6",
|
| 54 |
+
"tailwindcss": "^4.1.18"
|
| 55 |
+
}
|
| 56 |
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module.exports = {
|
| 2 |
+
plugins: {
|
| 3 |
+
"@tailwindcss/postcss": {},
|
| 4 |
+
},
|
| 5 |
+
};
|
|
|
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8" />
|
| 5 |
+
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 7 |
+
<meta name="theme-color" content="#000000" />
|
| 8 |
+
<meta
|
| 9 |
+
name="description"
|
| 10 |
+
content="Web site created using create-react-app"
|
| 11 |
+
/>
|
| 12 |
+
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
| 13 |
+
<!--
|
| 14 |
+
manifest.json provides metadata used when your web app is installed on a
|
| 15 |
+
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
| 16 |
+
-->
|
| 17 |
+
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
| 18 |
+
<!--
|
| 19 |
+
Notice the use of %PUBLIC_URL% in the tags above.
|
| 20 |
+
It will be replaced with the URL of the `public` folder during the build.
|
| 21 |
+
Only files inside the `public` folder can be referenced from the HTML.
|
| 22 |
+
|
| 23 |
+
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
| 24 |
+
work correctly both with client-side routing and a non-root public URL.
|
| 25 |
+
Learn how to configure a non-root public URL by running `npm run build`.
|
| 26 |
+
-->
|
| 27 |
+
<title>React App</title>
|
| 28 |
+
</head>
|
| 29 |
+
<body>
|
| 30 |
+
<noscript>You need to enable JavaScript to run this app.</noscript>
|
| 31 |
+
<div id="root"></div>
|
| 32 |
+
<!--
|
| 33 |
+
This HTML file is a template.
|
| 34 |
+
If you open it directly in the browser, you will see an empty page.
|
| 35 |
+
|
| 36 |
+
You can add webfonts, meta tags, or analytics to this file.
|
| 37 |
+
The build step will place the bundled scripts into the <body> tag.
|
| 38 |
+
|
| 39 |
+
To begin the development, run `npm start` or `yarn start`.
|
| 40 |
+
To create a production bundle, use `npm run build` or `yarn build`.
|
| 41 |
+
-->
|
| 42 |
+
</body>
|
| 43 |
+
</html>
|
|
|
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"short_name": "React App",
|
| 3 |
+
"name": "Create React App Sample",
|
| 4 |
+
"icons": [
|
| 5 |
+
{
|
| 6 |
+
"src": "favicon.ico",
|
| 7 |
+
"sizes": "64x64 32x32 24x24 16x16",
|
| 8 |
+
"type": "image/x-icon"
|
| 9 |
+
},
|
| 10 |
+
{
|
| 11 |
+
"src": "logo192.png",
|
| 12 |
+
"type": "image/png",
|
| 13 |
+
"sizes": "192x192"
|
| 14 |
+
},
|
| 15 |
+
{
|
| 16 |
+
"src": "logo512.png",
|
| 17 |
+
"type": "image/png",
|
| 18 |
+
"sizes": "512x512"
|
| 19 |
+
}
|
| 20 |
+
],
|
| 21 |
+
"start_url": ".",
|
| 22 |
+
"display": "standalone",
|
| 23 |
+
"theme_color": "#000000",
|
| 24 |
+
"background_color": "#ffffff"
|
| 25 |
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# https://www.robotstxt.org/robotstxt.html
|
| 2 |
+
User-agent: *
|
| 3 |
+
Disallow:
|
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
| 3 |
+
import { Layout } from './components/layout';
|
| 4 |
+
import { Landing } from './pages/Landing';
|
| 5 |
+
import { CaseBrowser } from './pages/CaseBrowser';
|
| 6 |
+
import { CaseInterface } from './pages/CaseInterface';
|
| 7 |
+
import { Dashboard } from './pages/Dashboard';
|
| 8 |
+
import { KnowledgeGraphPage } from './pages/KnowledgeGraph';
|
| 9 |
+
|
| 10 |
+
function App() {
|
| 11 |
+
return (
|
| 12 |
+
<Router>
|
| 13 |
+
<Layout>
|
| 14 |
+
<Routes>
|
| 15 |
+
<Route path="/" element={<Landing />} />
|
| 16 |
+
<Route path="/cases" element={<CaseBrowser />} />
|
| 17 |
+
<Route path="/case/:id" element={<CaseInterface />} />
|
| 18 |
+
<Route path="/dashboard" element={<Dashboard />} />
|
| 19 |
+
<Route path="/knowledge-graph" element={<KnowledgeGraphPage />} />
|
| 20 |
+
</Routes>
|
| 21 |
+
</Layout>
|
| 22 |
+
</Router>
|
| 23 |
+
);
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
export default App;
|
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
|
| 3 |
+
export const Footer: React.FC = () => {
|
| 4 |
+
return (
|
| 5 |
+
<footer className="border-t border-warm-gray-100 bg-warm-gray-50 mt-auto">
|
| 6 |
+
<div className="max-w-7xl mx-auto px-6 py-8">
|
| 7 |
+
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
|
| 8 |
+
<div className="flex items-center gap-3">
|
| 9 |
+
<div className="w-8 h-8 bg-forest-green rounded-lg flex items-center justify-center">
|
| 10 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 11 |
+
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="#FFFCF7" strokeWidth="2" strokeLinejoin="round"/>
|
| 12 |
+
<path d="M2 17L12 22L22 17" stroke="#FFFCF7" strokeWidth="2" strokeLinejoin="round"/>
|
| 13 |
+
<path d="M2 12L12 17L22 12" stroke="#FFFCF7" strokeWidth="2" strokeLinejoin="round"/>
|
| 14 |
+
</svg>
|
| 15 |
+
</div>
|
| 16 |
+
<span className="text-sm text-text-secondary">
|
| 17 |
+
Clinical<span className="text-forest-green font-semibold">Mind</span> - Master clinical reasoning, one case at a time
|
| 18 |
+
</span>
|
| 19 |
+
</div>
|
| 20 |
+
<div className="text-sm text-text-tertiary">
|
| 21 |
+
Built for Indian medical students
|
| 22 |
+
</div>
|
| 23 |
+
</div>
|
| 24 |
+
</div>
|
| 25 |
+
</footer>
|
| 26 |
+
);
|
| 27 |
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { Link, useLocation } from 'react-router-dom';
|
| 3 |
+
|
| 4 |
+
export const Header: React.FC = () => {
|
| 5 |
+
const location = useLocation();
|
| 6 |
+
|
| 7 |
+
const isActive = (path: string) => location.pathname === path;
|
| 8 |
+
|
| 9 |
+
return (
|
| 10 |
+
<header className="sticky top-0 z-50 bg-cream-white/80 backdrop-blur-md border-b border-warm-gray-100">
|
| 11 |
+
<div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between">
|
| 12 |
+
<Link to="/" className="flex items-center gap-3 no-underline">
|
| 13 |
+
<div className="w-10 h-10 bg-forest-green rounded-xl flex items-center justify-center">
|
| 14 |
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 15 |
+
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="#FFFCF7" strokeWidth="2" strokeLinejoin="round"/>
|
| 16 |
+
<path d="M2 17L12 22L22 17" stroke="#FFFCF7" strokeWidth="2" strokeLinejoin="round"/>
|
| 17 |
+
<path d="M2 12L12 17L22 12" stroke="#FFFCF7" strokeWidth="2" strokeLinejoin="round"/>
|
| 18 |
+
</svg>
|
| 19 |
+
</div>
|
| 20 |
+
<span className="text-xl font-bold text-text-primary">
|
| 21 |
+
Clinical<span className="text-forest-green">Mind</span>
|
| 22 |
+
</span>
|
| 23 |
+
</Link>
|
| 24 |
+
|
| 25 |
+
<nav className="hidden md:flex items-center gap-8">
|
| 26 |
+
<Link
|
| 27 |
+
to="/cases"
|
| 28 |
+
className={`text-base font-medium no-underline transition-colors duration-300 ${isActive('/cases') ? 'text-forest-green' : 'text-text-secondary hover:text-forest-green'}`}
|
| 29 |
+
>
|
| 30 |
+
Cases
|
| 31 |
+
</Link>
|
| 32 |
+
<Link
|
| 33 |
+
to="/dashboard"
|
| 34 |
+
className={`text-base font-medium no-underline transition-colors duration-300 ${isActive('/dashboard') ? 'text-forest-green' : 'text-text-secondary hover:text-forest-green'}`}
|
| 35 |
+
>
|
| 36 |
+
Dashboard
|
| 37 |
+
</Link>
|
| 38 |
+
<Link
|
| 39 |
+
to="/knowledge-graph"
|
| 40 |
+
className={`text-base font-medium no-underline transition-colors duration-300 ${isActive('/knowledge-graph') ? 'text-forest-green' : 'text-text-secondary hover:text-forest-green'}`}
|
| 41 |
+
>
|
| 42 |
+
Knowledge Map
|
| 43 |
+
</Link>
|
| 44 |
+
</nav>
|
| 45 |
+
|
| 46 |
+
<div className="flex items-center gap-4">
|
| 47 |
+
<div className="w-9 h-9 bg-sage-green/20 rounded-full flex items-center justify-center">
|
| 48 |
+
<span className="text-sm font-semibold text-forest-green">SM</span>
|
| 49 |
+
</div>
|
| 50 |
+
</div>
|
| 51 |
+
</div>
|
| 52 |
+
</header>
|
| 53 |
+
);
|
| 54 |
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { Header } from './Header';
|
| 3 |
+
import { Footer } from './Footer';
|
| 4 |
+
|
| 5 |
+
interface LayoutProps {
|
| 6 |
+
children: React.ReactNode;
|
| 7 |
+
showHeader?: boolean;
|
| 8 |
+
showFooter?: boolean;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
export const Layout: React.FC<LayoutProps> = ({
|
| 12 |
+
children,
|
| 13 |
+
showHeader = true,
|
| 14 |
+
showFooter = true,
|
| 15 |
+
}) => {
|
| 16 |
+
return (
|
| 17 |
+
<div className="min-h-screen flex flex-col bg-cream-white">
|
| 18 |
+
{showHeader && <Header />}
|
| 19 |
+
<main className="flex-1">
|
| 20 |
+
{children}
|
| 21 |
+
</main>
|
| 22 |
+
{showFooter && <Footer />}
|
| 23 |
+
</div>
|
| 24 |
+
);
|
| 25 |
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export { Header } from './Header';
|
| 2 |
+
export { Footer } from './Footer';
|
| 3 |
+
export { Layout } from './Layout';
|
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
|
| 3 |
+
interface BadgeProps {
|
| 4 |
+
children: React.ReactNode;
|
| 5 |
+
variant?: 'default' | 'success' | 'warning' | 'error' | 'info';
|
| 6 |
+
size?: 'sm' | 'md';
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
const variantStyles = {
|
| 10 |
+
default: 'bg-warm-gray-50 text-text-secondary border-warm-gray-100',
|
| 11 |
+
success: 'bg-forest-green/10 text-forest-green border-forest-green/20',
|
| 12 |
+
warning: 'bg-warning/10 text-warning border-warning/20',
|
| 13 |
+
error: 'bg-terracotta/10 text-terracotta border-terracotta/20',
|
| 14 |
+
info: 'bg-soft-blue/10 text-soft-blue border-soft-blue/20',
|
| 15 |
+
};
|
| 16 |
+
|
| 17 |
+
const sizeStyles = {
|
| 18 |
+
sm: 'px-2 py-0.5 text-xs',
|
| 19 |
+
md: 'px-3 py-1 text-sm',
|
| 20 |
+
};
|
| 21 |
+
|
| 22 |
+
export const Badge: React.FC<BadgeProps> = ({
|
| 23 |
+
children,
|
| 24 |
+
variant = 'default',
|
| 25 |
+
size = 'md',
|
| 26 |
+
}) => {
|
| 27 |
+
return (
|
| 28 |
+
<span className={`inline-flex items-center rounded-full border font-medium ${variantStyles[variant]} ${sizeStyles[size]}`}>
|
| 29 |
+
{children}
|
| 30 |
+
</span>
|
| 31 |
+
);
|
| 32 |
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
|
| 3 |
+
interface ButtonProps {
|
| 4 |
+
variant?: 'primary' | 'secondary' | 'tertiary';
|
| 5 |
+
size?: 'sm' | 'md' | 'lg';
|
| 6 |
+
children: React.ReactNode;
|
| 7 |
+
onClick?: () => void;
|
| 8 |
+
disabled?: boolean;
|
| 9 |
+
className?: string;
|
| 10 |
+
type?: 'button' | 'submit';
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
const variantStyles = {
|
| 14 |
+
primary: 'bg-forest-green text-cream-white hover:bg-forest-green-dark shadow-[0_2px_8px_rgba(45,92,63,0.15)] hover:shadow-[0_4px_12px_rgba(45,92,63,0.25)] hover:-translate-y-0.5 active:translate-y-0',
|
| 15 |
+
secondary: 'bg-transparent text-forest-green border-2 border-warm-gray-100 hover:border-forest-green hover:bg-forest-green/[0.04]',
|
| 16 |
+
tertiary: 'bg-transparent text-text-secondary hover:text-forest-green hover:bg-warm-gray-50',
|
| 17 |
+
};
|
| 18 |
+
|
| 19 |
+
const sizeStyles = {
|
| 20 |
+
sm: 'px-4 py-2 text-sm rounded-lg',
|
| 21 |
+
md: 'px-7 py-3.5 text-base rounded-xl',
|
| 22 |
+
lg: 'px-8 py-4 text-lg rounded-xl',
|
| 23 |
+
};
|
| 24 |
+
|
| 25 |
+
export const Button: React.FC<ButtonProps> = ({
|
| 26 |
+
variant = 'primary',
|
| 27 |
+
size = 'md',
|
| 28 |
+
children,
|
| 29 |
+
onClick,
|
| 30 |
+
disabled = false,
|
| 31 |
+
className = '',
|
| 32 |
+
type = 'button',
|
| 33 |
+
}) => {
|
| 34 |
+
return (
|
| 35 |
+
<button
|
| 36 |
+
type={type}
|
| 37 |
+
onClick={onClick}
|
| 38 |
+
disabled={disabled}
|
| 39 |
+
className={`font-semibold transition-all duration-300 ease-out cursor-pointer inline-flex items-center justify-center gap-2 ${variantStyles[variant]} ${sizeStyles[size]} ${disabled ? 'opacity-50 cursor-not-allowed' : ''} ${className}`}
|
| 40 |
+
>
|
| 41 |
+
{children}
|
| 42 |
+
</button>
|
| 43 |
+
);
|
| 44 |
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
|
| 3 |
+
interface CardProps {
|
| 4 |
+
children: React.ReactNode;
|
| 5 |
+
hover?: boolean;
|
| 6 |
+
padding?: 'sm' | 'md' | 'lg';
|
| 7 |
+
className?: string;
|
| 8 |
+
onClick?: () => void;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
const paddingStyles = {
|
| 12 |
+
sm: 'p-4',
|
| 13 |
+
md: 'p-6',
|
| 14 |
+
lg: 'p-8',
|
| 15 |
+
};
|
| 16 |
+
|
| 17 |
+
export const Card: React.FC<CardProps> = ({
|
| 18 |
+
children,
|
| 19 |
+
hover = false,
|
| 20 |
+
padding = 'md',
|
| 21 |
+
className = '',
|
| 22 |
+
onClick,
|
| 23 |
+
}) => {
|
| 24 |
+
return (
|
| 25 |
+
<div
|
| 26 |
+
onClick={onClick}
|
| 27 |
+
className={`bg-cream-white border-[1.5px] border-warm-gray-100 rounded-2xl shadow-[0_2px_8px_rgba(42,37,32,0.04)] transition-all duration-400 ${paddingStyles[padding]} ${hover ? 'cursor-pointer hover:border-forest-green hover:shadow-[0_4px_16px_rgba(45,92,63,0.08)] hover:-translate-y-0.5' : ''} ${className}`}
|
| 28 |
+
>
|
| 29 |
+
{children}
|
| 30 |
+
</div>
|
| 31 |
+
);
|
| 32 |
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
|
| 3 |
+
interface InputProps {
|
| 4 |
+
placeholder?: string;
|
| 5 |
+
value: string;
|
| 6 |
+
onChange: (value: string) => void;
|
| 7 |
+
onKeyPress?: (e: React.KeyboardEvent) => void;
|
| 8 |
+
label?: string;
|
| 9 |
+
className?: string;
|
| 10 |
+
multiline?: boolean;
|
| 11 |
+
rows?: number;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
export const Input: React.FC<InputProps> = ({
|
| 15 |
+
placeholder,
|
| 16 |
+
value,
|
| 17 |
+
onChange,
|
| 18 |
+
onKeyPress,
|
| 19 |
+
label,
|
| 20 |
+
className = '',
|
| 21 |
+
multiline = false,
|
| 22 |
+
rows = 3,
|
| 23 |
+
}) => {
|
| 24 |
+
const baseStyles = 'w-full p-3.5 rounded-xl border-[1.5px] border-warm-gray-100 bg-cream-white text-text-primary text-base placeholder:text-text-tertiary focus:outline-none focus:border-forest-green focus:ring-2 focus:ring-forest-green/10 transition-all duration-300';
|
| 25 |
+
|
| 26 |
+
return (
|
| 27 |
+
<div className={className}>
|
| 28 |
+
{label && (
|
| 29 |
+
<label className="block text-sm font-medium text-text-secondary mb-2">
|
| 30 |
+
{label}
|
| 31 |
+
</label>
|
| 32 |
+
)}
|
| 33 |
+
{multiline ? (
|
| 34 |
+
<textarea
|
| 35 |
+
placeholder={placeholder}
|
| 36 |
+
value={value}
|
| 37 |
+
onChange={(e) => onChange(e.target.value)}
|
| 38 |
+
onKeyPress={onKeyPress}
|
| 39 |
+
rows={rows}
|
| 40 |
+
className={`${baseStyles} resize-none`}
|
| 41 |
+
/>
|
| 42 |
+
) : (
|
| 43 |
+
<input
|
| 44 |
+
type="text"
|
| 45 |
+
placeholder={placeholder}
|
| 46 |
+
value={value}
|
| 47 |
+
onChange={(e) => onChange(e.target.value)}
|
| 48 |
+
onKeyPress={onKeyPress}
|
| 49 |
+
className={baseStyles}
|
| 50 |
+
/>
|
| 51 |
+
)}
|
| 52 |
+
</div>
|
| 53 |
+
);
|
| 54 |
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
|
| 3 |
+
interface StatCardProps {
|
| 4 |
+
title: string;
|
| 5 |
+
value: string;
|
| 6 |
+
trend?: string;
|
| 7 |
+
color?: 'green' | 'blue' | 'terracotta';
|
| 8 |
+
icon?: React.ReactNode;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
const colorStyles = {
|
| 12 |
+
green: 'border-l-forest-green',
|
| 13 |
+
blue: 'border-l-soft-blue',
|
| 14 |
+
terracotta: 'border-l-terracotta',
|
| 15 |
+
};
|
| 16 |
+
|
| 17 |
+
export const StatCard: React.FC<StatCardProps> = ({
|
| 18 |
+
title,
|
| 19 |
+
value,
|
| 20 |
+
trend,
|
| 21 |
+
color = 'green',
|
| 22 |
+
icon,
|
| 23 |
+
}) => {
|
| 24 |
+
return (
|
| 25 |
+
<div className={`bg-cream-white border-[1.5px] border-warm-gray-100 rounded-2xl p-6 border-l-4 ${colorStyles[color]} shadow-[0_2px_8px_rgba(42,37,32,0.04)]`}>
|
| 26 |
+
<div className="flex items-center justify-between mb-2">
|
| 27 |
+
<span className="text-sm text-text-tertiary font-medium">{title}</span>
|
| 28 |
+
{icon && <span className="text-text-tertiary">{icon}</span>}
|
| 29 |
+
</div>
|
| 30 |
+
<div className="text-2xl font-bold text-text-primary mb-1">{value}</div>
|
| 31 |
+
{trend && (
|
| 32 |
+
<div className="text-sm text-sage-green font-medium">{trend}</div>
|
| 33 |
+
)}
|
| 34 |
+
</div>
|
| 35 |
+
);
|
| 36 |
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export { Button } from './Button';
|
| 2 |
+
export { Card } from './Card';
|
| 3 |
+
export { Badge } from './Badge';
|
| 4 |
+
export { Input } from './Input';
|
| 5 |
+
export { StatCard } from './StatCard';
|
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import "tailwindcss";
|
| 2 |
+
|
| 3 |
+
@theme {
|
| 4 |
+
/* Primary Colors */
|
| 5 |
+
--color-cream-white: #FFFCF7;
|
| 6 |
+
--color-warm-gray-50: #F5F3F0;
|
| 7 |
+
--color-warm-gray-100: #E8E5E0;
|
| 8 |
+
--color-warm-gray-200: #D4D0CA;
|
| 9 |
+
--color-warm-gray-900: #2A2520;
|
| 10 |
+
|
| 11 |
+
/* Accent Colors */
|
| 12 |
+
--color-forest-green: #2D5C3F;
|
| 13 |
+
--color-forest-green-dark: #234730;
|
| 14 |
+
--color-sage-green: #6B8E6F;
|
| 15 |
+
--color-terracotta: #C85835;
|
| 16 |
+
--color-soft-blue: #4A7C8C;
|
| 17 |
+
|
| 18 |
+
/* Feedback Colors */
|
| 19 |
+
--color-success: #2D5C3F;
|
| 20 |
+
--color-warning: #D4803F;
|
| 21 |
+
--color-error: #C85835;
|
| 22 |
+
|
| 23 |
+
/* Text Colors */
|
| 24 |
+
--color-text-primary: #2A2520;
|
| 25 |
+
--color-text-secondary: #5A5147;
|
| 26 |
+
--color-text-tertiary: #8A8179;
|
| 27 |
+
|
| 28 |
+
/* Font sizes - Larger scale */
|
| 29 |
+
--font-size-xs: 0.875rem;
|
| 30 |
+
--font-size-sm: 1rem;
|
| 31 |
+
--font-size-base: 1.125rem;
|
| 32 |
+
--font-size-lg: 1.375rem;
|
| 33 |
+
--font-size-xl: 1.75rem;
|
| 34 |
+
--font-size-2xl: 2.25rem;
|
| 35 |
+
--font-size-3xl: 3rem;
|
| 36 |
+
|
| 37 |
+
/* Font family */
|
| 38 |
+
--font-sans: 'Inter', -apple-system, system-ui, sans-serif;
|
| 39 |
+
|
| 40 |
+
/* Border radius */
|
| 41 |
+
--radius-lg: 12px;
|
| 42 |
+
--radius-xl: 16px;
|
| 43 |
+
--radius-2xl: 24px;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
/* Base styles */
|
| 47 |
+
body {
|
| 48 |
+
font-family: var(--font-sans);
|
| 49 |
+
background-color: var(--color-cream-white);
|
| 50 |
+
color: var(--color-text-primary);
|
| 51 |
+
font-size: var(--font-size-base);
|
| 52 |
+
line-height: 1.6;
|
| 53 |
+
-webkit-font-smoothing: antialiased;
|
| 54 |
+
-moz-osx-font-smoothing: grayscale;
|
| 55 |
+
margin: 0;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
/* Animations */
|
| 59 |
+
@keyframes fadeInUp {
|
| 60 |
+
from {
|
| 61 |
+
opacity: 0;
|
| 62 |
+
transform: translateY(24px);
|
| 63 |
+
}
|
| 64 |
+
to {
|
| 65 |
+
opacity: 1;
|
| 66 |
+
transform: translateY(0);
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
@keyframes gentleFloat {
|
| 71 |
+
0%, 100% { transform: translateY(0px); }
|
| 72 |
+
50% { transform: translateY(-8px); }
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
@keyframes scalePulse {
|
| 76 |
+
0%, 100% { transform: scale(1); }
|
| 77 |
+
50% { transform: scale(1.05); }
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
.animate-fade-in-up {
|
| 81 |
+
animation: fadeInUp 0.6s ease-out forwards;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.animate-gentle-float {
|
| 85 |
+
animation: gentleFloat 4s ease-in-out infinite;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.animate-scale-pulse {
|
| 89 |
+
animation: scalePulse 2s ease-in-out infinite;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
/* Staggered animations */
|
| 93 |
+
.animate-delay-100 { animation-delay: 100ms; }
|
| 94 |
+
.animate-delay-200 { animation-delay: 200ms; }
|
| 95 |
+
.animate-delay-300 { animation-delay: 300ms; }
|
| 96 |
+
.animate-delay-400 { animation-delay: 400ms; }
|
| 97 |
+
.animate-delay-500 { animation-delay: 500ms; }
|
| 98 |
+
|
| 99 |
+
/* Scrollbar styling */
|
| 100 |
+
::-webkit-scrollbar {
|
| 101 |
+
width: 8px;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
::-webkit-scrollbar-track {
|
| 105 |
+
background: var(--color-warm-gray-50);
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
::-webkit-scrollbar-thumb {
|
| 109 |
+
background: var(--color-warm-gray-200);
|
| 110 |
+
border-radius: 4px;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
::-webkit-scrollbar-thumb:hover {
|
| 114 |
+
background: var(--color-text-tertiary);
|
| 115 |
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import ReactDOM from 'react-dom/client';
|
| 3 |
+
import './index.css';
|
| 4 |
+
import App from './App';
|
| 5 |
+
|
| 6 |
+
const root = ReactDOM.createRoot(
|
| 7 |
+
document.getElementById('root') as HTMLElement
|
| 8 |
+
);
|
| 9 |
+
root.render(
|
| 10 |
+
<React.StrictMode>
|
| 11 |
+
<App />
|
| 12 |
+
</React.StrictMode>
|
| 13 |
+
);
|
|
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from 'react';
|
| 2 |
+
import { useNavigate } from 'react-router-dom';
|
| 3 |
+
import { Button, Card, Badge } from '../components/ui';
|
| 4 |
+
|
| 5 |
+
const specialties = [
|
| 6 |
+
{ id: 'all', name: 'All Specialties' },
|
| 7 |
+
{ id: 'cardiology', name: 'Cardiology' },
|
| 8 |
+
{ id: 'respiratory', name: 'Respiratory' },
|
| 9 |
+
{ id: 'infectious', name: 'Infectious Disease' },
|
| 10 |
+
{ id: 'neurology', name: 'Neurology' },
|
| 11 |
+
{ id: 'gastro', name: 'Gastroenterology' },
|
| 12 |
+
{ id: 'emergency', name: 'Emergency Medicine' },
|
| 13 |
+
];
|
| 14 |
+
|
| 15 |
+
const difficulties = [
|
| 16 |
+
{ id: 'all', name: 'All Levels' },
|
| 17 |
+
{ id: 'beginner', name: 'Beginner' },
|
| 18 |
+
{ id: 'intermediate', name: 'Intermediate' },
|
| 19 |
+
{ id: 'advanced', name: 'Advanced' },
|
| 20 |
+
];
|
| 21 |
+
|
| 22 |
+
const sampleCases = [
|
| 23 |
+
{
|
| 24 |
+
id: 'case-1',
|
| 25 |
+
title: 'Chest Pain in a Young Male',
|
| 26 |
+
specialty: 'cardiology',
|
| 27 |
+
difficulty: 'intermediate' as const,
|
| 28 |
+
setting: 'Urban Emergency Department, Mumbai',
|
| 29 |
+
snippet: 'A 28-year-old male presents to the ED with acute onset chest pain radiating to the left arm. He appears anxious and diaphoretic...',
|
| 30 |
+
tags: ['ACS', 'Differential Diagnosis', 'ECG Interpretation'],
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
id: 'case-2',
|
| 34 |
+
title: 'Fever with Thrombocytopenia',
|
| 35 |
+
specialty: 'infectious',
|
| 36 |
+
difficulty: 'beginner' as const,
|
| 37 |
+
setting: 'District Hospital, Kerala (Monsoon Season)',
|
| 38 |
+
snippet: 'A 35-year-old female presents with 5 days of high-grade fever, myalgia, and a positive tourniquet test. Platelet count: 45,000...',
|
| 39 |
+
tags: ['Dengue', 'Tropical Medicine', 'Fluid Management'],
|
| 40 |
+
},
|
| 41 |
+
{
|
| 42 |
+
id: 'case-3',
|
| 43 |
+
title: 'Progressive Weakness with Respiratory Distress',
|
| 44 |
+
specialty: 'neurology',
|
| 45 |
+
difficulty: 'advanced' as const,
|
| 46 |
+
setting: 'Tertiary Care Hospital, Delhi',
|
| 47 |
+
snippet: 'A 42-year-old teacher presents with ascending weakness over 3 days, now with difficulty breathing. Deep tendon reflexes are absent...',
|
| 48 |
+
tags: ['GBS', 'Neuromuscular Emergency', 'Ventilation'],
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
id: 'case-4',
|
| 52 |
+
title: 'Chronic Cough in a Rural Setting',
|
| 53 |
+
specialty: 'respiratory',
|
| 54 |
+
difficulty: 'beginner' as const,
|
| 55 |
+
setting: 'Primary Health Centre, Rajasthan',
|
| 56 |
+
snippet: 'A 50-year-old farmer presents with productive cough for 3 weeks, evening rise of temperature, and significant weight loss...',
|
| 57 |
+
tags: ['TB', 'RNTCP', 'Sputum Analysis'],
|
| 58 |
+
},
|
| 59 |
+
{
|
| 60 |
+
id: 'case-5',
|
| 61 |
+
title: 'Acute Abdomen with Hematemesis',
|
| 62 |
+
specialty: 'gastro',
|
| 63 |
+
difficulty: 'intermediate' as const,
|
| 64 |
+
setting: 'Community Hospital, Tamil Nadu',
|
| 65 |
+
snippet: 'A 45-year-old male with known alcohol use presents with severe epigastric pain and coffee-ground vomitus. He is tachycardic...',
|
| 66 |
+
tags: ['Upper GI Bleed', 'Portal Hypertension', 'Resuscitation'],
|
| 67 |
+
},
|
| 68 |
+
{
|
| 69 |
+
id: 'case-6',
|
| 70 |
+
title: 'Pediatric Seizure with Altered Sensorium',
|
| 71 |
+
specialty: 'emergency',
|
| 72 |
+
difficulty: 'advanced' as const,
|
| 73 |
+
setting: 'Emergency Department, Kolkata',
|
| 74 |
+
snippet: 'A 4-year-old child is brought in with generalized tonic-clonic seizures lasting 10 minutes. Temperature is 39.8°C. Parents report recent travel...',
|
| 75 |
+
tags: ['Status Epilepticus', 'Cerebral Malaria', 'Pediatric Emergency'],
|
| 76 |
+
},
|
| 77 |
+
];
|
| 78 |
+
|
| 79 |
+
const difficultyColors: Record<string, 'success' | 'warning' | 'error'> = {
|
| 80 |
+
beginner: 'success',
|
| 81 |
+
intermediate: 'warning',
|
| 82 |
+
advanced: 'error',
|
| 83 |
+
};
|
| 84 |
+
|
| 85 |
+
export const CaseBrowser: React.FC = () => {
|
| 86 |
+
const navigate = useNavigate();
|
| 87 |
+
const [selectedSpecialty, setSelectedSpecialty] = useState('all');
|
| 88 |
+
const [selectedDifficulty, setSelectedDifficulty] = useState('all');
|
| 89 |
+
|
| 90 |
+
const filteredCases = sampleCases.filter((c) => {
|
| 91 |
+
const matchSpecialty = selectedSpecialty === 'all' || c.specialty === selectedSpecialty;
|
| 92 |
+
const matchDifficulty = selectedDifficulty === 'all' || c.difficulty === selectedDifficulty;
|
| 93 |
+
return matchSpecialty && matchDifficulty;
|
| 94 |
+
});
|
| 95 |
+
|
| 96 |
+
return (
|
| 97 |
+
<div className="max-w-7xl mx-auto px-6 py-10">
|
| 98 |
+
{/* Page Header */}
|
| 99 |
+
<div className="mb-10">
|
| 100 |
+
<h1 className="text-2xl md:text-[2.25rem] font-bold text-text-primary mb-3">
|
| 101 |
+
Clinical Cases
|
| 102 |
+
</h1>
|
| 103 |
+
<p className="text-lg text-text-secondary">
|
| 104 |
+
Choose a case to begin. Each case is dynamically generated from Indian medical literature.
|
| 105 |
+
</p>
|
| 106 |
+
</div>
|
| 107 |
+
|
| 108 |
+
{/* Filters */}
|
| 109 |
+
<div className="flex flex-col md:flex-row gap-4 mb-8">
|
| 110 |
+
<div className="flex flex-wrap gap-2">
|
| 111 |
+
{specialties.map((s) => (
|
| 112 |
+
<button
|
| 113 |
+
key={s.id}
|
| 114 |
+
onClick={() => setSelectedSpecialty(s.id)}
|
| 115 |
+
className={`px-4 py-2 rounded-xl text-sm font-medium transition-all duration-300 cursor-pointer border-none ${
|
| 116 |
+
selectedSpecialty === s.id
|
| 117 |
+
? 'bg-forest-green text-cream-white'
|
| 118 |
+
: 'bg-warm-gray-50 text-text-secondary hover:bg-warm-gray-100'
|
| 119 |
+
}`}
|
| 120 |
+
>
|
| 121 |
+
{s.name}
|
| 122 |
+
</button>
|
| 123 |
+
))}
|
| 124 |
+
</div>
|
| 125 |
+
<div className="flex flex-wrap gap-2">
|
| 126 |
+
{difficulties.map((d) => (
|
| 127 |
+
<button
|
| 128 |
+
key={d.id}
|
| 129 |
+
onClick={() => setSelectedDifficulty(d.id)}
|
| 130 |
+
className={`px-4 py-2 rounded-xl text-sm font-medium transition-all duration-300 cursor-pointer border-none ${
|
| 131 |
+
selectedDifficulty === d.id
|
| 132 |
+
? 'bg-forest-green text-cream-white'
|
| 133 |
+
: 'bg-warm-gray-50 text-text-secondary hover:bg-warm-gray-100'
|
| 134 |
+
}`}
|
| 135 |
+
>
|
| 136 |
+
{d.name}
|
| 137 |
+
</button>
|
| 138 |
+
))}
|
| 139 |
+
</div>
|
| 140 |
+
</div>
|
| 141 |
+
|
| 142 |
+
{/* Generate New Case */}
|
| 143 |
+
<Card padding="lg" className="mb-8 border-dashed border-2 border-forest-green/30 bg-forest-green/[0.02]">
|
| 144 |
+
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
|
| 145 |
+
<div>
|
| 146 |
+
<h3 className="text-lg font-semibold text-text-primary mb-1">Generate a New Case</h3>
|
| 147 |
+
<p className="text-base text-text-secondary">
|
| 148 |
+
Our RAG system creates unique cases from Indian medical journals. No two cases are alike.
|
| 149 |
+
</p>
|
| 150 |
+
</div>
|
| 151 |
+
<Button onClick={() => navigate('/case/new')}>
|
| 152 |
+
Generate Case
|
| 153 |
+
</Button>
|
| 154 |
+
</div>
|
| 155 |
+
</Card>
|
| 156 |
+
|
| 157 |
+
{/* Case Grid */}
|
| 158 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
| 159 |
+
{filteredCases.map((caseItem) => (
|
| 160 |
+
<Card
|
| 161 |
+
key={caseItem.id}
|
| 162 |
+
hover
|
| 163 |
+
padding="lg"
|
| 164 |
+
onClick={() => navigate(`/case/${caseItem.id}`)}
|
| 165 |
+
>
|
| 166 |
+
<div className="flex items-start justify-between mb-3">
|
| 167 |
+
<Badge variant={difficultyColors[caseItem.difficulty]}>
|
| 168 |
+
{caseItem.difficulty.charAt(0).toUpperCase() + caseItem.difficulty.slice(1)}
|
| 169 |
+
</Badge>
|
| 170 |
+
<span className="text-sm text-text-tertiary">{caseItem.setting}</span>
|
| 171 |
+
</div>
|
| 172 |
+
<h3 className="text-lg font-semibold text-text-primary mb-2">{caseItem.title}</h3>
|
| 173 |
+
<p className="text-base text-text-secondary mb-4 leading-relaxed">{caseItem.snippet}</p>
|
| 174 |
+
<div className="flex flex-wrap gap-2">
|
| 175 |
+
{caseItem.tags.map((tag) => (
|
| 176 |
+
<Badge key={tag} variant="default" size="sm">{tag}</Badge>
|
| 177 |
+
))}
|
| 178 |
+
</div>
|
| 179 |
+
</Card>
|
| 180 |
+
))}
|
| 181 |
+
</div>
|
| 182 |
+
|
| 183 |
+
{filteredCases.length === 0 && (
|
| 184 |
+
<div className="text-center py-16">
|
| 185 |
+
<p className="text-lg text-text-tertiary mb-4">No cases match your filters</p>
|
| 186 |
+
<Button variant="secondary" onClick={() => { setSelectedSpecialty('all'); setSelectedDifficulty('all'); }}>
|
| 187 |
+
Clear Filters
|
| 188 |
+
</Button>
|
| 189 |
+
</div>
|
| 190 |
+
)}
|
| 191 |
+
</div>
|
| 192 |
+
);
|
| 193 |
+
};
|
|
@@ -0,0 +1,342 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useRef, useEffect } from 'react';
|
| 2 |
+
import { useNavigate } from 'react-router-dom';
|
| 3 |
+
import { Button, Card, Badge, Input } from '../components/ui';
|
| 4 |
+
import type { Message, VitalSigns } from '../types';
|
| 5 |
+
|
| 6 |
+
interface CaseData {
|
| 7 |
+
patient: { age: number; gender: string; location: string };
|
| 8 |
+
chiefComplaint: string;
|
| 9 |
+
initialPresentation: string;
|
| 10 |
+
vitalSigns: VitalSigns;
|
| 11 |
+
stages: { stage: string; info: string; revealed: boolean }[];
|
| 12 |
+
diagnosis: string;
|
| 13 |
+
differentials: string[];
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
const sampleCase: CaseData = {
|
| 17 |
+
patient: { age: 28, gender: 'Male', location: 'Mumbai' },
|
| 18 |
+
chiefComplaint: 'Acute onset chest pain radiating to left arm',
|
| 19 |
+
initialPresentation:
|
| 20 |
+
'A 28-year-old male software engineer presents to the ED at 2 AM with crushing chest pain that started 45 minutes ago while he was sleeping. He appears anxious, diaphoretic, and is clutching his chest. He denies any previous cardiac history but mentions significant work stress and recent cocaine use at a party.',
|
| 21 |
+
vitalSigns: { bp: '160/95', hr: 110, rr: 22, temp: 37.2, spo2: 96 },
|
| 22 |
+
stages: [
|
| 23 |
+
{
|
| 24 |
+
stage: 'history',
|
| 25 |
+
info: 'Pain started suddenly, woke him from sleep. Severity: 8/10, crushing quality. Radiates to left arm and jaw. Associated with nausea and sweating. He admits to cocaine use ~3 hours ago. No prior cardiac history. Family history: father had MI at age 52. Smokes 5 cigarettes/day. No diabetes, hypertension on medications.',
|
| 26 |
+
revealed: false,
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
stage: 'physical_exam',
|
| 30 |
+
info: 'Alert, anxious, diaphoretic. Cardiovascular: Tachycardic, regular rhythm, no murmurs. S1/S2 normal. JVP not elevated. Chest: Clear bilateral air entry. Abdomen: Soft, non-tender. Extremities: No edema. Pupils dilated bilaterally (? cocaine effect).',
|
| 31 |
+
revealed: false,
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
stage: 'labs',
|
| 35 |
+
info: 'ECG: Sinus tachycardia, ST elevation in leads II, III, aVF (inferior leads). Troponin I: 0.8 ng/mL (elevated, normal <0.04). CBC: Normal. BMP: Normal. Urine drug screen: Positive for cocaine. CXR: Normal cardiac silhouette, clear lung fields.',
|
| 36 |
+
revealed: false,
|
| 37 |
+
},
|
| 38 |
+
],
|
| 39 |
+
diagnosis: 'Cocaine-induced acute coronary syndrome (inferior STEMI)',
|
| 40 |
+
differentials: [
|
| 41 |
+
'Acute coronary syndrome (STEMI)',
|
| 42 |
+
'Cocaine-induced coronary vasospasm',
|
| 43 |
+
'Aortic dissection',
|
| 44 |
+
'Pulmonary embolism',
|
| 45 |
+
'Pericarditis',
|
| 46 |
+
],
|
| 47 |
+
};
|
| 48 |
+
|
| 49 |
+
const stageLabels: Record<string, string> = {
|
| 50 |
+
history: 'History',
|
| 51 |
+
physical_exam: 'Physical Examination',
|
| 52 |
+
labs: 'Investigations',
|
| 53 |
+
};
|
| 54 |
+
|
| 55 |
+
const stageIcons: Record<string, string> = {
|
| 56 |
+
history: '📋',
|
| 57 |
+
physical_exam: '🩺',
|
| 58 |
+
labs: '🔬',
|
| 59 |
+
};
|
| 60 |
+
|
| 61 |
+
const tutorResponses = [
|
| 62 |
+
"Interesting choice. Before we jump to conclusions, what other diagnoses could present with chest pain and ST elevation in a young male?",
|
| 63 |
+
"You're thinking about ACS, which is reasonable given the ECG. But what's unusual about this presentation? What risk factor stands out for a 28-year-old?",
|
| 64 |
+
"Good - you identified the cocaine use. Now, how does cocaine cause myocardial ischemia? And importantly, how does this change your management compared to a typical STEMI?",
|
| 65 |
+
"Exactly. Cocaine causes coronary vasospasm AND increases myocardial oxygen demand. This is critical because beta-blockers - typically used in ACS - are contraindicated here. Why?",
|
| 66 |
+
"Correct. Unopposed alpha stimulation with beta-blockers can worsen coronary vasospasm. Benzodiazepines and nitroglycerin are first-line. You've demonstrated strong reasoning - the key learning point is: always screen for cocaine in young patients with ACS.",
|
| 67 |
+
];
|
| 68 |
+
|
| 69 |
+
export const CaseInterface: React.FC = () => {
|
| 70 |
+
const navigate = useNavigate();
|
| 71 |
+
const [caseData] = useState<CaseData>(sampleCase);
|
| 72 |
+
const [revealedStages, setRevealedStages] = useState<Set<number>>(new Set());
|
| 73 |
+
const [messages, setMessages] = useState<Message[]>([
|
| 74 |
+
{
|
| 75 |
+
id: '1',
|
| 76 |
+
role: 'ai',
|
| 77 |
+
content: "I see you've started a new case. Take a look at the patient presentation and vital signs. What's your initial assessment? What differential diagnoses come to mind?",
|
| 78 |
+
timestamp: new Date(),
|
| 79 |
+
},
|
| 80 |
+
]);
|
| 81 |
+
const [inputValue, setInputValue] = useState('');
|
| 82 |
+
const [tutorIndex, setTutorIndex] = useState(0);
|
| 83 |
+
const [showDiagnosis, setShowDiagnosis] = useState(false);
|
| 84 |
+
const [studentDiagnosis, setStudentDiagnosis] = useState('');
|
| 85 |
+
const [showDiagnosisInput, setShowDiagnosisInput] = useState(false);
|
| 86 |
+
const chatEndRef = useRef<HTMLDivElement>(null);
|
| 87 |
+
|
| 88 |
+
useEffect(() => {
|
| 89 |
+
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
| 90 |
+
}, [messages]);
|
| 91 |
+
|
| 92 |
+
const revealStage = (index: number) => {
|
| 93 |
+
setRevealedStages((prev) => new Set(prev).add(index));
|
| 94 |
+
};
|
| 95 |
+
|
| 96 |
+
const sendMessage = () => {
|
| 97 |
+
if (!inputValue.trim()) return;
|
| 98 |
+
|
| 99 |
+
const studentMsg: Message = {
|
| 100 |
+
id: Date.now().toString(),
|
| 101 |
+
role: 'student',
|
| 102 |
+
content: inputValue,
|
| 103 |
+
timestamp: new Date(),
|
| 104 |
+
};
|
| 105 |
+
|
| 106 |
+
setMessages((prev) => [...prev, studentMsg]);
|
| 107 |
+
setInputValue('');
|
| 108 |
+
|
| 109 |
+
// Simulate AI response
|
| 110 |
+
setTimeout(() => {
|
| 111 |
+
const aiResponse: Message = {
|
| 112 |
+
id: (Date.now() + 1).toString(),
|
| 113 |
+
role: 'ai',
|
| 114 |
+
content: tutorResponses[tutorIndex] || "Excellent reasoning. You've identified the key features of this case. Ready to make your diagnosis?",
|
| 115 |
+
timestamp: new Date(),
|
| 116 |
+
};
|
| 117 |
+
setMessages((prev) => [...prev, aiResponse]);
|
| 118 |
+
setTutorIndex((prev) => prev + 1);
|
| 119 |
+
}, 1200);
|
| 120 |
+
};
|
| 121 |
+
|
| 122 |
+
const submitDiagnosis = () => {
|
| 123 |
+
if (!studentDiagnosis.trim()) return;
|
| 124 |
+
setShowDiagnosis(true);
|
| 125 |
+
|
| 126 |
+
const aiMsg: Message = {
|
| 127 |
+
id: Date.now().toString(),
|
| 128 |
+
role: 'ai',
|
| 129 |
+
content: `You diagnosed: "${studentDiagnosis}". The correct diagnosis is: ${caseData.diagnosis}. ${
|
| 130 |
+
studentDiagnosis.toLowerCase().includes('cocaine')
|
| 131 |
+
? "Excellent work! You correctly identified the cocaine-induced etiology, which is critical for management."
|
| 132 |
+
: "Close, but the key differentiator here is the cocaine use. Always screen for substance use in young patients with ACS - it fundamentally changes your management approach."
|
| 133 |
+
}`,
|
| 134 |
+
timestamp: new Date(),
|
| 135 |
+
};
|
| 136 |
+
setMessages((prev) => [...prev, aiMsg]);
|
| 137 |
+
};
|
| 138 |
+
|
| 139 |
+
return (
|
| 140 |
+
<div className="max-w-7xl mx-auto px-6 py-8">
|
| 141 |
+
{/* Case Header */}
|
| 142 |
+
<div className="flex items-center justify-between mb-8">
|
| 143 |
+
<div>
|
| 144 |
+
<div className="flex items-center gap-3 mb-2">
|
| 145 |
+
<Badge variant="warning">Intermediate</Badge>
|
| 146 |
+
<Badge variant="info">Cardiology</Badge>
|
| 147 |
+
</div>
|
| 148 |
+
<h1 className="text-xl md:text-2xl font-bold text-text-primary">
|
| 149 |
+
Chest Pain in a Young Male
|
| 150 |
+
</h1>
|
| 151 |
+
</div>
|
| 152 |
+
<Button variant="tertiary" onClick={() => navigate('/cases')}>
|
| 153 |
+
Exit Case
|
| 154 |
+
</Button>
|
| 155 |
+
</div>
|
| 156 |
+
|
| 157 |
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
| 158 |
+
{/* Main Case Area (2/3) */}
|
| 159 |
+
<div className="lg:col-span-2 space-y-6">
|
| 160 |
+
{/* Patient Card */}
|
| 161 |
+
<Card padding="lg">
|
| 162 |
+
<div className="flex items-center gap-4 mb-4">
|
| 163 |
+
<div className="w-14 h-14 bg-soft-blue/10 rounded-2xl flex items-center justify-center text-2xl">
|
| 164 |
+
👤
|
| 165 |
+
</div>
|
| 166 |
+
<div>
|
| 167 |
+
<h2 className="text-lg font-semibold text-text-primary">
|
| 168 |
+
{caseData.patient.age}-year-old {caseData.patient.gender}
|
| 169 |
+
</h2>
|
| 170 |
+
<p className="text-sm text-text-tertiary">{caseData.patient.location}</p>
|
| 171 |
+
</div>
|
| 172 |
+
</div>
|
| 173 |
+
<div className="bg-warm-gray-50 rounded-xl p-4 mb-4">
|
| 174 |
+
<span className="text-sm font-medium text-text-tertiary block mb-1">Chief Complaint</span>
|
| 175 |
+
<p className="text-base text-text-primary font-medium">{caseData.chiefComplaint}</p>
|
| 176 |
+
</div>
|
| 177 |
+
<p className="text-base text-text-secondary leading-relaxed">
|
| 178 |
+
{caseData.initialPresentation}
|
| 179 |
+
</p>
|
| 180 |
+
</Card>
|
| 181 |
+
|
| 182 |
+
{/* Vital Signs */}
|
| 183 |
+
<Card padding="md">
|
| 184 |
+
<h3 className="text-base font-semibold text-text-primary mb-4">Vital Signs</h3>
|
| 185 |
+
<div className="grid grid-cols-5 gap-4">
|
| 186 |
+
{[
|
| 187 |
+
{ label: 'BP', value: caseData.vitalSigns.bp, unit: 'mmHg' },
|
| 188 |
+
{ label: 'HR', value: caseData.vitalSigns.hr, unit: 'bpm' },
|
| 189 |
+
{ label: 'RR', value: caseData.vitalSigns.rr, unit: '/min' },
|
| 190 |
+
{ label: 'Temp', value: caseData.vitalSigns.temp, unit: '°C' },
|
| 191 |
+
{ label: 'SpO2', value: caseData.vitalSigns.spo2, unit: '%' },
|
| 192 |
+
].map((vital) => (
|
| 193 |
+
<div key={vital.label} className="text-center bg-warm-gray-50 rounded-xl p-3">
|
| 194 |
+
<div className="text-sm text-text-tertiary mb-1">{vital.label}</div>
|
| 195 |
+
<div className="text-lg font-bold text-text-primary">{vital.value}</div>
|
| 196 |
+
<div className="text-xs text-text-tertiary">{vital.unit}</div>
|
| 197 |
+
</div>
|
| 198 |
+
))}
|
| 199 |
+
</div>
|
| 200 |
+
</Card>
|
| 201 |
+
|
| 202 |
+
{/* Action Buttons - Reveal Stages */}
|
| 203 |
+
<div className="space-y-4">
|
| 204 |
+
{caseData.stages.map((stage, index) => (
|
| 205 |
+
<div key={stage.stage}>
|
| 206 |
+
{!revealedStages.has(index) ? (
|
| 207 |
+
<Button
|
| 208 |
+
variant="secondary"
|
| 209 |
+
className="w-full justify-start"
|
| 210 |
+
onClick={() => revealStage(index)}
|
| 211 |
+
>
|
| 212 |
+
<span className="mr-2">{stageIcons[stage.stage]}</span>
|
| 213 |
+
{stageLabels[stage.stage] || stage.stage}
|
| 214 |
+
</Button>
|
| 215 |
+
) : (
|
| 216 |
+
<Card padding="md" className="animate-fade-in-up">
|
| 217 |
+
<h3 className="text-base font-semibold text-text-primary mb-3 flex items-center gap-2">
|
| 218 |
+
<span>{stageIcons[stage.stage]}</span>
|
| 219 |
+
{stageLabels[stage.stage]}
|
| 220 |
+
</h3>
|
| 221 |
+
<p className="text-base text-text-secondary leading-relaxed whitespace-pre-line">
|
| 222 |
+
{stage.info}
|
| 223 |
+
</p>
|
| 224 |
+
</Card>
|
| 225 |
+
)}
|
| 226 |
+
</div>
|
| 227 |
+
))}
|
| 228 |
+
</div>
|
| 229 |
+
|
| 230 |
+
{/* Diagnosis Section */}
|
| 231 |
+
{revealedStages.size >= 2 && !showDiagnosis && (
|
| 232 |
+
<Card padding="lg" className="border-forest-green/30">
|
| 233 |
+
{!showDiagnosisInput ? (
|
| 234 |
+
<div className="text-center">
|
| 235 |
+
<h3 className="text-lg font-semibold text-text-primary mb-2">Ready to diagnose?</h3>
|
| 236 |
+
<p className="text-base text-text-secondary mb-4">
|
| 237 |
+
You've gathered enough information. Make your diagnosis.
|
| 238 |
+
</p>
|
| 239 |
+
<Button onClick={() => setShowDiagnosisInput(true)}>Make Diagnosis</Button>
|
| 240 |
+
</div>
|
| 241 |
+
) : (
|
| 242 |
+
<div>
|
| 243 |
+
<h3 className="text-lg font-semibold text-text-primary mb-4">Your Diagnosis</h3>
|
| 244 |
+
<Input
|
| 245 |
+
placeholder="Enter your diagnosis..."
|
| 246 |
+
value={studentDiagnosis}
|
| 247 |
+
onChange={setStudentDiagnosis}
|
| 248 |
+
onKeyPress={(e) => e.key === 'Enter' && submitDiagnosis()}
|
| 249 |
+
/>
|
| 250 |
+
<div className="mt-4 flex gap-3">
|
| 251 |
+
<Button onClick={submitDiagnosis}>Submit Diagnosis</Button>
|
| 252 |
+
<Button variant="tertiary" onClick={() => setShowDiagnosisInput(false)}>
|
| 253 |
+
Gather More Info
|
| 254 |
+
</Button>
|
| 255 |
+
</div>
|
| 256 |
+
</div>
|
| 257 |
+
)}
|
| 258 |
+
</Card>
|
| 259 |
+
)}
|
| 260 |
+
|
| 261 |
+
{/* Diagnosis Result */}
|
| 262 |
+
{showDiagnosis && (
|
| 263 |
+
<Card padding="lg" className="border-forest-green animate-fade-in-up">
|
| 264 |
+
<h3 className="text-lg font-semibold text-forest-green mb-4">Case Complete</h3>
|
| 265 |
+
<div className="space-y-4">
|
| 266 |
+
<div className="bg-forest-green/5 rounded-xl p-4">
|
| 267 |
+
<span className="text-sm font-medium text-forest-green block mb-1">Correct Diagnosis</span>
|
| 268 |
+
<p className="text-base font-semibold text-text-primary">{caseData.diagnosis}</p>
|
| 269 |
+
</div>
|
| 270 |
+
<div>
|
| 271 |
+
<span className="text-sm font-medium text-text-tertiary block mb-2">Key Differentials</span>
|
| 272 |
+
<div className="flex flex-wrap gap-2">
|
| 273 |
+
{caseData.differentials.map((d) => (
|
| 274 |
+
<Badge key={d} variant="default">{d}</Badge>
|
| 275 |
+
))}
|
| 276 |
+
</div>
|
| 277 |
+
</div>
|
| 278 |
+
<div className="flex gap-3 pt-2">
|
| 279 |
+
<Button onClick={() => navigate('/cases')}>Next Case</Button>
|
| 280 |
+
<Button variant="secondary" onClick={() => navigate('/dashboard')}>
|
| 281 |
+
View Dashboard
|
| 282 |
+
</Button>
|
| 283 |
+
</div>
|
| 284 |
+
</div>
|
| 285 |
+
</Card>
|
| 286 |
+
)}
|
| 287 |
+
</div>
|
| 288 |
+
|
| 289 |
+
{/* AI Tutor Sidebar (1/3) */}
|
| 290 |
+
<div className="lg:col-span-1">
|
| 291 |
+
<div className="sticky top-24">
|
| 292 |
+
<Card padding="md" className="h-[calc(100vh-8rem)] flex flex-col">
|
| 293 |
+
<div className="flex items-center gap-3 mb-4 pb-4 border-b border-warm-gray-100">
|
| 294 |
+
<div className="w-10 h-10 bg-forest-green/10 rounded-xl flex items-center justify-center">
|
| 295 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#2D5C3F" strokeWidth="2">
|
| 296 |
+
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
| 297 |
+
</svg>
|
| 298 |
+
</div>
|
| 299 |
+
<div>
|
| 300 |
+
<h3 className="text-base font-semibold text-text-primary">AI Tutor</h3>
|
| 301 |
+
<p className="text-xs text-text-tertiary">Socratic reasoning coach</p>
|
| 302 |
+
</div>
|
| 303 |
+
</div>
|
| 304 |
+
|
| 305 |
+
<div className="flex-1 overflow-y-auto space-y-3 mb-4">
|
| 306 |
+
{messages.map((msg) => (
|
| 307 |
+
<div
|
| 308 |
+
key={msg.id}
|
| 309 |
+
className={`p-3 rounded-xl text-sm leading-relaxed ${
|
| 310 |
+
msg.role === 'ai'
|
| 311 |
+
? 'bg-forest-green/5 border border-forest-green/10 text-text-primary'
|
| 312 |
+
: 'bg-warm-gray-50 border border-warm-gray-100 text-text-primary ml-4'
|
| 313 |
+
}`}
|
| 314 |
+
>
|
| 315 |
+
{msg.content}
|
| 316 |
+
</div>
|
| 317 |
+
))}
|
| 318 |
+
<div ref={chatEndRef} />
|
| 319 |
+
</div>
|
| 320 |
+
|
| 321 |
+
<div className="mt-auto">
|
| 322 |
+
<div className="flex gap-2">
|
| 323 |
+
<input
|
| 324 |
+
type="text"
|
| 325 |
+
placeholder="Type your reasoning..."
|
| 326 |
+
value={inputValue}
|
| 327 |
+
onChange={(e) => setInputValue(e.target.value)}
|
| 328 |
+
onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
|
| 329 |
+
className="flex-1 p-3 rounded-xl border-[1.5px] border-warm-gray-100 bg-cream-white text-sm text-text-primary placeholder:text-text-tertiary focus:outline-none focus:border-forest-green transition-colors"
|
| 330 |
+
/>
|
| 331 |
+
<Button size="sm" onClick={sendMessage}>
|
| 332 |
+
Send
|
| 333 |
+
</Button>
|
| 334 |
+
</div>
|
| 335 |
+
</div>
|
| 336 |
+
</Card>
|
| 337 |
+
</div>
|
| 338 |
+
</div>
|
| 339 |
+
</div>
|
| 340 |
+
</div>
|
| 341 |
+
);
|
| 342 |
+
};
|
|
@@ -0,0 +1,248 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { useNavigate } from 'react-router-dom';
|
| 3 |
+
import { Button, Card, Badge, StatCard } from '../components/ui';
|
| 4 |
+
import {
|
| 5 |
+
RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, Radar,
|
| 6 |
+
ResponsiveContainer, XAxis, YAxis, CartesianGrid, Tooltip, AreaChart, Area
|
| 7 |
+
} from 'recharts';
|
| 8 |
+
|
| 9 |
+
// Recharts v3 type workaround
|
| 10 |
+
const RChart = RadarChart as any;
|
| 11 |
+
const RGrid = PolarGrid as any;
|
| 12 |
+
const RAngleAxis = PolarAngleAxis as any;
|
| 13 |
+
const RRadiusAxis = PolarRadiusAxis as any;
|
| 14 |
+
const RRadar = Radar as any;
|
| 15 |
+
const RContainer = ResponsiveContainer as any;
|
| 16 |
+
const AChart = AreaChart as any;
|
| 17 |
+
const RXAxis = XAxis as any;
|
| 18 |
+
const RYAxis = YAxis as any;
|
| 19 |
+
const RCartesianGrid = CartesianGrid as any;
|
| 20 |
+
const RTooltip = Tooltip as any;
|
| 21 |
+
const RArea = Area as any;
|
| 22 |
+
|
| 23 |
+
// Sample data
|
| 24 |
+
const biasData = [
|
| 25 |
+
{ bias: 'Anchoring', score: 65 },
|
| 26 |
+
{ bias: 'Premature Closure', score: 40 },
|
| 27 |
+
{ bias: 'Availability', score: 55 },
|
| 28 |
+
{ bias: 'Confirmation', score: 30 },
|
| 29 |
+
];
|
| 30 |
+
|
| 31 |
+
const performanceHistory = [
|
| 32 |
+
{ week: 'W1', accuracy: 55, avgTime: 18 },
|
| 33 |
+
{ week: 'W2', accuracy: 60, avgTime: 16 },
|
| 34 |
+
{ week: 'W3', accuracy: 58, avgTime: 15 },
|
| 35 |
+
{ week: 'W4', accuracy: 68, avgTime: 14 },
|
| 36 |
+
{ week: 'W5', accuracy: 72, avgTime: 12 },
|
| 37 |
+
{ week: 'W6', accuracy: 75, avgTime: 11 },
|
| 38 |
+
{ week: 'W7', accuracy: 78, avgTime: 10 },
|
| 39 |
+
];
|
| 40 |
+
|
| 41 |
+
const specialtyScores = [
|
| 42 |
+
{ name: 'Cardiology', score: 82, cases: 12 },
|
| 43 |
+
{ name: 'Respiratory', score: 65, cases: 8 },
|
| 44 |
+
{ name: 'Infectious', score: 78, cases: 10 },
|
| 45 |
+
{ name: 'Neurology', score: 45, cases: 5 },
|
| 46 |
+
{ name: 'Gastro', score: 70, cases: 7 },
|
| 47 |
+
{ name: 'Emergency', score: 55, cases: 6 },
|
| 48 |
+
];
|
| 49 |
+
|
| 50 |
+
const biasInsights = [
|
| 51 |
+
{
|
| 52 |
+
type: 'Anchoring Bias',
|
| 53 |
+
severity: 'moderate' as const,
|
| 54 |
+
evidence: 'You stuck with your initial diagnosis in 7 out of 10 recent cases, even when new information contradicted it.',
|
| 55 |
+
recommendation: 'Practice cases with atypical presentations. Force yourself to reconsider after each new piece of information.',
|
| 56 |
+
},
|
| 57 |
+
{
|
| 58 |
+
type: 'Availability Bias',
|
| 59 |
+
severity: 'low' as const,
|
| 60 |
+
evidence: 'After studying cardiology, you diagnosed 3 consecutive non-cardiac cases as cardiac. Your recent study focus influenced your diagnoses.',
|
| 61 |
+
recommendation: 'Before diagnosing, list 3 differential diagnoses from different organ systems.',
|
| 62 |
+
},
|
| 63 |
+
];
|
| 64 |
+
|
| 65 |
+
const recommendations = [
|
| 66 |
+
{
|
| 67 |
+
type: 'Weak Area',
|
| 68 |
+
specialty: 'Neurology',
|
| 69 |
+
difficulty: 'beginner',
|
| 70 |
+
reason: 'Your neurology accuracy is only 45%. Let\'s strengthen this foundation.',
|
| 71 |
+
priority: 'high' as const,
|
| 72 |
+
},
|
| 73 |
+
{
|
| 74 |
+
type: 'Bias Counter',
|
| 75 |
+
specialty: 'Mixed',
|
| 76 |
+
difficulty: 'intermediate',
|
| 77 |
+
reason: 'Atypical presentation cases to reduce your anchoring bias pattern.',
|
| 78 |
+
priority: 'medium' as const,
|
| 79 |
+
},
|
| 80 |
+
{
|
| 81 |
+
type: 'Challenge',
|
| 82 |
+
specialty: 'Cardiology',
|
| 83 |
+
difficulty: 'advanced',
|
| 84 |
+
reason: 'Your cardiology accuracy is 82%. Ready for advanced cases!',
|
| 85 |
+
priority: 'low' as const,
|
| 86 |
+
},
|
| 87 |
+
];
|
| 88 |
+
|
| 89 |
+
const priorityColors: Record<string, 'error' | 'warning' | 'info'> = {
|
| 90 |
+
high: 'error',
|
| 91 |
+
medium: 'warning',
|
| 92 |
+
low: 'info',
|
| 93 |
+
};
|
| 94 |
+
|
| 95 |
+
export const Dashboard: React.FC = () => {
|
| 96 |
+
const navigate = useNavigate();
|
| 97 |
+
|
| 98 |
+
return (
|
| 99 |
+
<div className="max-w-7xl mx-auto px-6 py-8 space-y-8">
|
| 100 |
+
{/* Welcome Banner */}
|
| 101 |
+
<div className="bg-gradient-to-r from-forest-green to-sage-green rounded-2xl p-8 text-cream-white">
|
| 102 |
+
<h1 className="text-2xl md:text-3xl font-bold mb-2">Welcome back, Student</h1>
|
| 103 |
+
<p className="text-lg opacity-90 mb-4">
|
| 104 |
+
You've completed 48 cases across 6 specialties. Your diagnostic accuracy has improved 23% this month.
|
| 105 |
+
</p>
|
| 106 |
+
<Button
|
| 107 |
+
variant="secondary"
|
| 108 |
+
className="!border-cream-white !text-cream-white hover:!bg-cream-white/10"
|
| 109 |
+
onClick={() => navigate('/cases')}
|
| 110 |
+
>
|
| 111 |
+
Continue Learning
|
| 112 |
+
</Button>
|
| 113 |
+
</div>
|
| 114 |
+
|
| 115 |
+
{/* Stats Grid */}
|
| 116 |
+
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
| 117 |
+
<StatCard title="Overall Accuracy" value="75%" trend="+5% from last week" color="green" />
|
| 118 |
+
<StatCard title="Cases Completed" value="48" trend="8 this week" color="blue" />
|
| 119 |
+
<StatCard title="Avg. Time per Case" value="10 min" trend="-2 min improvement" color="green" />
|
| 120 |
+
<StatCard title="Peer Ranking" value="Top 15%" trend="Moved up 3%" color="terracotta" />
|
| 121 |
+
</div>
|
| 122 |
+
|
| 123 |
+
{/* Performance Chart + Bias Radar */}
|
| 124 |
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
| 125 |
+
{/* Accuracy Over Time */}
|
| 126 |
+
<Card padding="lg">
|
| 127 |
+
<h2 className="text-xl font-semibold text-text-primary mb-6">Performance Trend</h2>
|
| 128 |
+
<RContainer width="100%" height={280}>
|
| 129 |
+
<AChart data={performanceHistory}>
|
| 130 |
+
<defs>
|
| 131 |
+
<linearGradient id="colorAccuracy" x1="0" y1="0" x2="0" y2="1">
|
| 132 |
+
<stop offset="5%" stopColor="#2D5C3F" stopOpacity={0.15} />
|
| 133 |
+
<stop offset="95%" stopColor="#2D5C3F" stopOpacity={0} />
|
| 134 |
+
</linearGradient>
|
| 135 |
+
</defs>
|
| 136 |
+
<RCartesianGrid strokeDasharray="3 3" stroke="#E8E5E0" />
|
| 137 |
+
<RXAxis dataKey="week" stroke="#8A8179" fontSize={13} />
|
| 138 |
+
<RYAxis stroke="#8A8179" fontSize={13} domain={[0, 100]} />
|
| 139 |
+
<RTooltip
|
| 140 |
+
contentStyle={{
|
| 141 |
+
background: '#FFFCF7',
|
| 142 |
+
border: '1.5px solid #E8E5E0',
|
| 143 |
+
borderRadius: '12px',
|
| 144 |
+
fontSize: '14px',
|
| 145 |
+
}}
|
| 146 |
+
/>
|
| 147 |
+
<RArea
|
| 148 |
+
type="monotone"
|
| 149 |
+
dataKey="accuracy"
|
| 150 |
+
stroke="#2D5C3F"
|
| 151 |
+
strokeWidth={2.5}
|
| 152 |
+
fill="url(#colorAccuracy)"
|
| 153 |
+
/>
|
| 154 |
+
</AChart>
|
| 155 |
+
</RContainer>
|
| 156 |
+
</Card>
|
| 157 |
+
|
| 158 |
+
{/* Cognitive Bias Radar */}
|
| 159 |
+
<Card padding="lg">
|
| 160 |
+
<h2 className="text-xl font-semibold text-text-primary mb-6">Cognitive Bias Profile</h2>
|
| 161 |
+
<RContainer width="100%" height={280}>
|
| 162 |
+
<RChart data={biasData}>
|
| 163 |
+
<RGrid stroke="#E8E5E0" />
|
| 164 |
+
<RAngleAxis dataKey="bias" stroke="#5A5147" fontSize={13} />
|
| 165 |
+
<RRadiusAxis domain={[0, 100]} tick={false} axisLine={false} />
|
| 166 |
+
<RRadar
|
| 167 |
+
dataKey="score"
|
| 168 |
+
stroke="#2D5C3F"
|
| 169 |
+
fill="#2D5C3F"
|
| 170 |
+
fillOpacity={0.2}
|
| 171 |
+
strokeWidth={2}
|
| 172 |
+
/>
|
| 173 |
+
</RChart>
|
| 174 |
+
</RContainer>
|
| 175 |
+
<p className="text-sm text-text-tertiary text-center mt-2">
|
| 176 |
+
Lower scores are better. Score reflects bias frequency in recent cases.
|
| 177 |
+
</p>
|
| 178 |
+
</Card>
|
| 179 |
+
</div>
|
| 180 |
+
|
| 181 |
+
{/* Bias Insights */}
|
| 182 |
+
<Card padding="lg">
|
| 183 |
+
<h2 className="text-xl font-semibold text-text-primary mb-6">Bias Insights</h2>
|
| 184 |
+
<div className="space-y-4">
|
| 185 |
+
{biasInsights.map((insight) => (
|
| 186 |
+
<div key={insight.type} className="bg-warm-gray-50 rounded-xl p-5">
|
| 187 |
+
<div className="flex items-center gap-3 mb-2">
|
| 188 |
+
<Badge variant={insight.severity === 'moderate' ? 'warning' : 'info'}>
|
| 189 |
+
{insight.severity.charAt(0).toUpperCase() + insight.severity.slice(1)}
|
| 190 |
+
</Badge>
|
| 191 |
+
<h3 className="font-semibold text-text-primary">{insight.type}</h3>
|
| 192 |
+
</div>
|
| 193 |
+
<p className="text-base text-text-secondary mb-2">{insight.evidence}</p>
|
| 194 |
+
<p className="text-sm text-forest-green font-medium">
|
| 195 |
+
Recommendation: {insight.recommendation}
|
| 196 |
+
</p>
|
| 197 |
+
</div>
|
| 198 |
+
))}
|
| 199 |
+
</div>
|
| 200 |
+
</Card>
|
| 201 |
+
|
| 202 |
+
{/* Specialty Breakdown */}
|
| 203 |
+
<Card padding="lg">
|
| 204 |
+
<h2 className="text-xl font-semibold text-text-primary mb-6">Specialty Performance</h2>
|
| 205 |
+
<div className="space-y-4">
|
| 206 |
+
{specialtyScores.map((s) => (
|
| 207 |
+
<div key={s.name} className="flex items-center gap-4">
|
| 208 |
+
<span className="text-sm font-medium text-text-secondary w-28 shrink-0">{s.name}</span>
|
| 209 |
+
<div className="flex-1 bg-warm-gray-100 rounded-full h-3 overflow-hidden">
|
| 210 |
+
<div
|
| 211 |
+
className="h-full rounded-full transition-all duration-500"
|
| 212 |
+
style={{
|
| 213 |
+
width: `${s.score}%`,
|
| 214 |
+
backgroundColor: s.score >= 70 ? '#2D5C3F' : s.score >= 50 ? '#D4803F' : '#C85835',
|
| 215 |
+
}}
|
| 216 |
+
/>
|
| 217 |
+
</div>
|
| 218 |
+
<span className="text-sm font-semibold text-text-primary w-12 text-right">{s.score}%</span>
|
| 219 |
+
<span className="text-xs text-text-tertiary w-16 text-right">{s.cases} cases</span>
|
| 220 |
+
</div>
|
| 221 |
+
))}
|
| 222 |
+
</div>
|
| 223 |
+
</Card>
|
| 224 |
+
|
| 225 |
+
{/* Recommended Cases */}
|
| 226 |
+
<div>
|
| 227 |
+
<h2 className="text-xl font-semibold text-text-primary mb-6">Recommended For You</h2>
|
| 228 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
| 229 |
+
{recommendations.map((rec, i) => (
|
| 230 |
+
<Card key={i} hover padding="lg">
|
| 231 |
+
<Badge variant={priorityColors[rec.priority]} size="sm">
|
| 232 |
+
{rec.priority.toUpperCase()} PRIORITY
|
| 233 |
+
</Badge>
|
| 234 |
+
<h3 className="text-lg font-semibold text-text-primary mt-3 mb-1">{rec.type}</h3>
|
| 235 |
+
<p className="text-sm text-text-secondary mb-2">
|
| 236 |
+
{rec.specialty} - {rec.difficulty}
|
| 237 |
+
</p>
|
| 238 |
+
<p className="text-base text-text-secondary mb-4">{rec.reason}</p>
|
| 239 |
+
<Button size="sm" onClick={() => navigate('/cases')}>
|
| 240 |
+
Start Case
|
| 241 |
+
</Button>
|
| 242 |
+
</Card>
|
| 243 |
+
))}
|
| 244 |
+
</div>
|
| 245 |
+
</div>
|
| 246 |
+
</div>
|
| 247 |
+
);
|
| 248 |
+
};
|