diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..d7b1fd77
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,165 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+### Added
+- **Conditional PDF Access Based on Authentication** (2025-01-XX)
+ - Logged-in users see "View PDF" button that opens PDF viewer in new tab
+ - Non-logged-in users see "Download PDF" button that directly downloads the file
+ - Backend: Added `upload_file_guid` field to risk/source API responses
+ - Frontend: Conditional rendering based on Redux authentication state
+ - Fallback GUID extraction from URL if backend field is missing
+
+ **Backend Changes:**
+
+ *File: `server/api/views/risk/views_riskWithSources.py`*
+ ```python
+ # Added to source_info dictionary in 3 locations (lines ~138, ~252, ~359):
+ source_info = {
+ 'filename': filename,
+ 'title': getattr(embedding, 'title', None),
+ 'publication': getattr(embedding, 'publication', ''),
+ 'text': getattr(embedding, 'text', ''),
+ 'rule_type': medrule.rule_type,
+ 'history_type': medrule.history_type,
+ 'upload_fileid': getattr(embedding, 'upload_file_id', None),
+ 'page': getattr(embedding, 'page_num', None),
+ 'link_url': self._build_pdf_link(embedding),
+ 'upload_file_guid': str(embedding.upload_file.guid) if embedding.upload_file else None # NEW
+ }
+ ```
+
+ **Frontend Changes:**
+
+ *File: `frontend/src/pages/PatientManager/PatientManager.tsx`*
+ ```typescript
+ // Added imports:
+ import { useSelector } from "react-redux";
+ import { RootState } from "../../services/actions/types";
+
+ // Added hook to get auth state:
+ const { isAuthenticated } = useSelector((state: RootState) => state.auth);
+
+ // Passed to PatientSummary:
+
+ ```
+
+ *File: `frontend/src/pages/PatientManager/PatientSummary.tsx`*
+ ```typescript
+ // Updated interface:
+ interface PatientSummaryProps {
+ // ... existing props
+ isAuthenticated?: boolean; // NEW
+ }
+
+ // Updated SourceItem type:
+ type SourceItem = {
+ // ... existing fields
+ upload_file_guid?: string | null; // NEW
+ };
+
+ // Added helper function:
+ const extractGuidFromUrl = (url: string): string | null => {
+ try {
+ const urlObj = new URL(url, window.location.origin);
+ return urlObj.searchParams.get('guid');
+ } catch {
+ return null;
+ }
+ };
+
+ // Updated component:
+ const PatientSummary = ({
+ // ... existing props
+ isAuthenticated = false, // NEW
+ }: PatientSummaryProps) => {
+ const baseURL = import.meta.env.VITE_API_BASE_URL || ''; // NEW
+
+ // Updated MedicationItem props:
+ const MedicationItem = ({
+ // ... existing props
+ isAuthenticated, // NEW
+ baseURL, // NEW
+ }: {
+ // ... existing types
+ isAuthenticated: boolean; // NEW
+ baseURL: string; // NEW
+ }) => {
+
+ // Updated MedicationTier props:
+ const MedicationTier = ({
+ // ... existing props
+ isAuthenticated, // NEW
+ baseURL, // NEW
+ }: {
+ // ... existing types
+ isAuthenticated: boolean; // NEW
+ baseURL: string; // NEW
+ }) => (
+ // ... passes to MedicationItem
+
+ );
+
+ // Conditional button rendering:
+ {s.link_url && (() => {
+ const guid = s.upload_file_guid || extractGuidFromUrl(s.link_url);
+ if (!guid) return null;
+
+ return isAuthenticated ? (
+
+ View PDF
+
+ ) : (
+
+ Download PDF
+
+ );
+ })()}
+
+ // Updated all MedicationTier calls to pass new props:
+
+ ```
+
+### Fixed
+- **URL Route Case Consistency** (2025-01-XX)
+ - Fixed case mismatch between backend URL generation (`/drugsummary`) and frontend route (`/drugSummary`)
+ - Updated all references to use consistent camelCase `/drugSummary` route
+ - Affected files: `views_riskWithSources.py`, `Layout_V2_Sidebar.tsx`, `Layout_V2_Header.tsx`, `FileRow.tsx`
+
+- **Protected Route Authentication Flow** (2025-01-XX)
+ - Fixed blank page issue when opening protected routes in new tab
+ - `ProtectedRoute` now waits for authentication check to complete before redirecting
+ - Added `useAuth()` hook to `Layout_V2_Main` to trigger auth verification
+
+### Changed
+- **PatientSummary Component** (2025-01-XX)
+ - Now receives `isAuthenticated` prop from Redux state
+ - Props passed through component hierarchy: `PatientManager` → `PatientSummary` → `MedicationTier` → `MedicationItem`
+ - Added `baseURL` constant for API endpoint construction
+
+## [Previous versions would go here]
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 00000000..8562eb0d
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,318 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Project Overview
+
+Balancer is a web application designed to help prescribers choose suitable medications for patients with bipolar disorder. It's a Code for Philly project built with a PostgreSQL + Django REST Framework + React stack, running on Docker.
+
+Live site: https://balancertestsite.com
+
+## Development Setup
+
+### Prerequisites
+- Docker Desktop
+- Node.js and npm
+- API keys for OpenAI and Anthropic (request from team)
+
+### Initial Setup
+```bash
+# Clone the repository
+git clone
+
+# Install frontend dependencies
+cd frontend
+npm install
+cd ..
+
+# Configure environment variables
+# Copy config/env/dev.env.example and fill in API keys:
+# - OPENAI_API_KEY
+# - ANTHROPIC_API_KEY
+# - PINECONE_API_KEY (if needed)
+
+# Start all services
+docker compose up --build
+```
+
+### Services
+- **Frontend**: React + Vite dev server at http://localhost:3000
+- **Backend**: Django REST Framework at http://localhost:8000
+- **Database**: PostgreSQL at localhost:5433
+- **pgAdmin**: Commented out by default (port 5050)
+
+## Common Development Commands
+
+### Docker Operations
+```bash
+# Start all services
+docker compose up --build
+
+# Start in detached mode
+docker compose up -d
+
+# View logs
+docker compose logs -f [service_name]
+
+# Stop all services
+docker compose down
+
+# Rebuild a specific service
+docker compose build [frontend|backend|db]
+
+# Access Django shell in backend container
+docker compose exec backend python manage.py shell
+
+# Run Django migrations
+docker compose exec backend python manage.py makemigrations
+docker compose exec backend python manage.py migrate
+```
+
+### Frontend Development
+```bash
+cd frontend
+
+# Start dev server (outside Docker)
+npm run dev
+
+# Build for production
+npm run build
+
+# Lint TypeScript/TSX files
+npm run lint
+
+# Preview production build
+npm run preview
+```
+
+### Backend Development
+```bash
+cd server
+
+# Create Django superuser (credentials in api/management/commands/createsu.py)
+docker compose exec backend python manage.py createsuperuser
+
+# Access Django admin
+# Navigate to http://localhost:8000/admin
+
+# Run database migrations
+docker compose exec backend python manage.py makemigrations
+docker compose exec backend python manage.py migrate
+
+# Django shell
+docker compose exec backend python manage.py shell
+```
+
+### Git Workflow
+- Main development branch: `develop`
+- Production branch: `listOfMed` (used for PRs)
+- Create feature branches from `develop`
+- PRs should target `listOfMed` branch
+
+## Architecture
+
+### Backend Architecture (Django REST Framework)
+
+#### URL Routing Pattern
+Django uses **dynamic URL importing** (see `server/balancer_backend/urls.py`). API endpoints are organized by feature modules in `server/api/views/`:
+- `conversations/` - Patient conversation management
+- `feedback/` - User feedback
+- `listMeds/` - Medication catalog
+- `risk/` - Risk assessment endpoints
+- `uploadFile/` - PDF document uploads
+- `ai_promptStorage/` - AI prompt templates
+- `ai_settings/` - AI configuration
+- `embeddings/` - Vector embeddings for RAG
+- `medRules/` - Medication rules management
+- `text_extraction/` - PDF text extraction
+- `assistant/` - AI assistant endpoints
+
+Each module contains:
+- `views.py` or `views_*.py` - API endpoints
+- `models.py` - Django ORM models
+- `urls.py` - URL patterns
+- `serializers.py` - DRF serializers (if present)
+
+#### Authentication
+- Uses **JWT authentication** with `rest_framework_simplejwt`
+- Default: All endpoints require authentication (`IsAuthenticated`)
+- To make an endpoint public, add to the view class:
+ ```python
+ from rest_framework.permissions import AllowAny
+
+ class MyView(APIView):
+ permission_classes = [AllowAny]
+ authentication_classes = [] # Optional: disable auth entirely
+ ```
+- Auth endpoints via Djoser: `/auth/`
+- JWT token lifetime: 60 minutes (access), 1 day (refresh)
+
+#### Key Data Models
+- **Medication** (`api.views.listMeds.models`) - Medication catalog with benefits/risks
+- **MedRule** (`api.models.model_medRule`) - Include/Exclude rules for medications based on patient history
+- **MedRuleSource** - Junction table linking MedRules → Embeddings → Medications
+- **Embeddings** (`api.models.model_embeddings`) - Vector embeddings from uploaded PDFs for RAG
+- **UploadFile** (`api.views.uploadFile.models`) - Uploaded PDF documents with GUID references
+
+#### RAG (Retrieval Augmented Generation) System
+The application uses embeddings from medical literature PDFs to provide evidence-based medication recommendations:
+1. PDFs uploaded via `uploadFile` → text extracted → chunked → embedded (OpenAI/Pinecone)
+2. MedRules created linking medications to specific evidence (embeddings)
+3. API endpoints return recommendations with source citations (filename, page number, text excerpt)
+
+### Frontend Architecture (React + TypeScript)
+
+#### Project Structure
+- **`src/components/`** - Reusable React components (Header, forms, etc.)
+- **`src/pages/`** - Page-level components
+- **`src/routes/routes.tsx`** - React Router configuration
+- **`src/services/`** - Redux store, actions, reducers, API clients
+- **`src/contexts/`** - React Context providers (GlobalContext for app state)
+- **`src/api/`** - API client functions using Axios
+- **`src/utils/`** - Utility functions
+
+#### State Management
+- **Redux** for auth state and global application data
+ - Store: `src/services/store.tsx`
+ - Actions: `src/services/actions/`
+ - Reducers: `src/services/reducers/`
+- **React Context** (`GlobalContext`) for UI state:
+ - `showSummary` - Display medication summary
+ - `enterNewPatient` - New patient form state
+ - `isEditing` - Form edit mode
+ - `showMetaPanel` - Metadata panel visibility
+
+#### Routing
+Routes defined in `src/routes/routes.tsx`:
+- `/` - Medication Suggester (main tool)
+- `/medications` - Medication List
+- `/about` - About page
+- `/help` - Help documentation
+- `/feedback` - Feedback form
+- `/logout` - Logout handler
+- Admin routes (superuser only):
+ - `/rulesmanager` - Manage medication rules
+ - `/ManageMeds` - Manage medication database
+
+#### Styling
+- **Tailwind CSS** for utility-first styling
+- **PostCSS** with nesting support
+- Custom CSS in component directories (e.g., `Header/header.css`)
+- Fonts: Quicksand (branding), Satoshi (body text)
+
+### Database Schema Notes
+- **pgvector extension** enabled for vector similarity search
+- Custom Dockerfile for PostgreSQL (`db/Dockerfile`) - workaround for ARM64 compatibility
+- Database connection:
+ - Host: `db` (Docker internal) or `localhost:5433` (external)
+ - Credentials: `balancer/balancer` (dev environment)
+ - Database: `balancer_dev`
+
+### Environment Configuration
+- **Development**: `config/env/dev.env` (used by Docker Compose)
+- **Frontend Production**: `frontend/.env.production`
+ - Contains `VITE_API_BASE_URL` for production API endpoint
+- **Never commit** actual API keys - use `.env.example` as template
+- Django `SECRET_KEY` should be a long random string in production (not "foo")
+
+## Important Development Patterns
+
+### Adding a New API Endpoint
+1. Create view in appropriate `server/api/views/{module}/views.py`
+2. Add URL pattern to `server/api/views/{module}/urls.py`
+3. If new module, add to `urls` list in `server/balancer_backend/urls.py`
+4. Consider authentication requirements (add `permission_classes` if needed)
+
+### Working with MedRules
+MedRules use a many-to-many relationship with medications and embeddings:
+- `rule_type`: "INCLUDE" (beneficial) or "EXCLUDE" (contraindicated)
+- `history_type`: Patient diagnosis state (e.g., "DIAGNOSIS_DEPRESSED", "DIAGNOSIS_MANIC")
+- Access sources via `MedRuleSource` intermediate model
+- API returns benefits/risks with source citations (filename, page, text, **upload_file_guid**)
+
+### PDF Access and Authentication
+**Feature**: Conditional PDF viewing/downloading based on authentication state
+
+**Behavior**:
+- **Logged-in users**: See "View PDF" button (blue) that opens `/drugSummary` page in new tab
+- **Non-logged-in users**: See "Download PDF" button (green) that directly downloads via `/v1/api/uploadFile/` endpoint
+
+**Implementation Details**:
+- Backend: `upload_file_guid` field added to source_info in `views_riskWithSources.py` (3 locations)
+- Frontend: `isAuthenticated` prop passed through component hierarchy:
+ - `PatientManager` (gets from Redux) → `PatientSummary` → `MedicationTier` → `MedicationItem`
+- Download endpoint: `/v1/api/uploadFile/` is **public** (AllowAny permission)
+- Fallback: If `upload_file_guid` missing from API, GUID is extracted from `link_url` query parameter
+- Route: `/drugSummary` (camelCase) - fixed from inconsistent `/drugsummary` usage
+
+**Files Modified**:
+- Backend: `server/api/views/risk/views_riskWithSources.py`
+- Frontend: `frontend/src/pages/PatientManager/PatientManager.tsx`, `PatientSummary.tsx`
+- Routes: Multiple files updated for consistent `/drugSummary` casing
+- Auth: `ProtectedRoute.tsx` and `Layout_V2_Main.tsx` fixed for proper auth checking
+
+### Frontend API Calls
+- API client functions in `src/api/`
+- Use Axios with base URL from environment
+- JWT tokens managed by Redux auth state
+- Error handling should check for 401 (unauthorized) and redirect to login
+
+### Docker Networking
+Services use a custom network (192.168.0.0/24):
+- db: 192.168.0.2
+- backend: 192.168.0.3
+- frontend: 192.168.0.5
+- Services communicate using service names (e.g., `http://backend:8000`)
+
+## Testing
+
+### Backend Tests
+Limited test coverage currently. Example test:
+- `server/api/views/uploadFile/test_title.py`
+
+To run tests:
+```bash
+docker compose exec backend python manage.py test
+```
+
+### Frontend Tests
+No test framework currently configured. Consider adding Jest/Vitest for future testing.
+
+## Deployment
+
+### Local Kubernetes (using Devbox)
+```bash
+# Install Devbox first: https://www.jetify.com/devbox
+
+# Add balancertestsite.com to /etc/hosts
+sudo sh -c 'echo "127.0.0.1 balancertestsite.com" >> /etc/hosts'
+
+# Deploy to local k8s cluster
+devbox shell
+devbox create:cluster
+devbox run deploy:balancer
+
+# Access at https://balancertestsite.com:30219/
+```
+
+### Production
+- Manifests: `deploy/manifests/balancer/`
+- ConfigMap: `deploy/manifests/balancer/base/configmap.yml`
+- Secrets: `deploy/manifests/balancer/base/secret.template.yaml`
+
+## Key Files Reference
+
+- `server/balancer_backend/settings.py` - Django configuration (auth, database, CORS)
+- `server/balancer_backend/urls.py` - Root URL configuration with dynamic imports
+- `frontend/src/routes/routes.tsx` - React Router configuration
+- `frontend/src/services/store.tsx` - Redux store setup
+- `docker-compose.yml` - Local development environment
+- `config/env/dev.env.example` - Environment variables template
+
+## Project Conventions
+
+- Python: Follow Django conventions, use class-based views (APIView)
+- TypeScript: Use functional components with hooks, avoid default exports except for pages
+- CSS: Prefer Tailwind utilities, use custom CSS only when necessary
+- Git: Feature branches from `develop`, PRs to `listOfMed`
+- Code formatting: Prettier for frontend (with Tailwind plugin)
diff --git a/docs/MIGRATION_PDF_AUTH.md b/docs/MIGRATION_PDF_AUTH.md
new file mode 100644
index 00000000..d5f7df26
--- /dev/null
+++ b/docs/MIGRATION_PDF_AUTH.md
@@ -0,0 +1,307 @@
+# Migration Guide: Conditional PDF Access Feature
+
+**Date**: January 2025
+**Feature**: Authentication-based PDF viewing and downloading
+**PR/Issue**: [Link to PR if applicable]
+
+## Overview
+
+This migration adds conditional behavior to PDF source buttons based on user authentication status:
+- **Authenticated users**: "View PDF" button opens PDF viewer in new tab
+- **Unauthenticated users**: "Download PDF" button triggers direct file download
+
+## How It Works
+
+### Button Logic Flow
+
+The button checks the user's authentication state and uses the `upload_file_guid` to determine behavior:
+
+```
+User clicks medication → Expands to show sources
+ ↓
+ Check: isAuthenticated?
+ ↓
+ ┌─────────────────┴─────────────────┐
+ ↓ ↓
+ YES (Logged In) NO (Not Logged In)
+ ↓ ↓
+ "View PDF" (Blue Button) "Download PDF" (Green Button)
+ ↓ ↓
+ Opens /drugSummary page Direct download via
+ with PDF viewer /v1/api/uploadFile/
+ (target="_blank") (download attribute)
+```
+
+### When User is NOT Authenticated:
+
+```typescript
+
+ Download PDF
+
+```
+
+- Uses `upload_file_guid` to construct download URL: `/v1/api/uploadFile/`
+- The `download` attribute forces browser to download instead of opening
+- Endpoint is **public** (AllowAny permission) - no authentication required
+- File downloads directly with original filename from database
+
+### When User IS Authenticated:
+
+```typescript
+
+ View PDF
+
+```
+
+- Uses `link_url` which points to `/drugSummary` page
+- Opens in new tab with `target="_blank"`
+- The drugSummary page renders a PDF viewer with navigation controls
+- User can navigate between pages, zoom, etc.
+
+### Key Points:
+
+1. ✅ **Both auth types can access PDFs** - the download endpoint (`/v1/api/uploadFile/`) is public
+2. ✅ The difference is **presentation**:
+ - **Authenticated**: Rich PDF viewer experience with navigation
+ - **Unauthenticated**: Simple direct download to local machine
+3. ✅ The `upload_file_guid` is the primary identifier for fetching files from the database
+4. ✅ **Fallback mechanism**: If `upload_file_guid` is missing from API response, it's extracted from the `link_url` query parameter
+
+### Code Location:
+
+The conditional logic is in `frontend/src/pages/PatientManager/PatientSummary.tsx` around line 165-180:
+
+```typescript
+{s.link_url && (() => {
+ // Get GUID from API or extract from URL as fallback
+ const guid = s.upload_file_guid || extractGuidFromUrl(s.link_url);
+ if (!guid) return null;
+
+ // Render different button based on authentication
+ return isAuthenticated ? (
+ // Blue "View PDF" button for authenticated users
+ View PDF
+ ) : (
+ // Green "Download PDF" button for unauthenticated users
+ Download PDF
+ );
+})()}
+```
+
+## Breaking Changes
+
+⚠️ **None** - This is a backward-compatible enhancement
+
+## Database Changes
+
+✅ **None** - No migrations required
+
+## API Changes
+
+### Backend: `POST /v1/api/riskWithSources`
+
+**Response Schema Update**:
+```python
+# New field added to each item in sources array:
+{
+ "sources": [
+ {
+ "filename": "example.pdf",
+ "title": "Example Document",
+ "publication": "Journal Name",
+ "text": "...",
+ "rule_type": "INCLUDE",
+ "history_type": "DIAGNOSIS_MANIC",
+ "upload_fileid": 123,
+ "page": 5,
+ "link_url": "/drugSummary?guid=xxx&page=5",
+ "upload_file_guid": "xxx-xxx-xxx" // NEW FIELD
+ }
+ ]
+}
+```
+
+**File**: `server/api/views/risk/views_riskWithSources.py`
+**Lines Modified**: ~138-149, ~252-263, ~359-370
+
+## Frontend Changes
+
+### 1. Component Prop Changes
+
+**PatientManager** now retrieves and passes authentication state:
+```typescript
+// Added imports
+import { useSelector } from "react-redux";
+import { RootState } from "../../services/actions/types";
+
+// New hook call
+const { isAuthenticated } = useSelector((state: RootState) => state.auth);
+
+// New prop passed
+
+```
+
+**PatientSummary** interface updated:
+```typescript
+interface PatientSummaryProps {
+ // ... existing props
+ isAuthenticated?: boolean; // NEW
+}
+
+type SourceItem = {
+ // ... existing fields
+ upload_file_guid?: string | null; // NEW
+}
+```
+
+### 2. New Helper Function
+
+```typescript
+/**
+ * Fallback to extract GUID from URL if API doesn't provide upload_file_guid
+ */
+const extractGuidFromUrl = (url: string): string | null => {
+ try {
+ const urlObj = new URL(url, window.location.origin);
+ return urlObj.searchParams.get('guid');
+ } catch {
+ return null;
+ }
+};
+```
+
+### 3. Component Hierarchy Updates
+
+Props now flow through: `PatientManager` → `PatientSummary` → `MedicationTier` → `MedicationItem`
+
+Each intermediate component needs `isAuthenticated` and `baseURL` props added.
+
+## Route Changes
+
+### URL Consistency Fix
+
+**Old (inconsistent)**:
+- Backend: `/drugsummary` (lowercase)
+- Frontend route: `/drugSummary` (camelCase)
+
+**New (consistent)**:
+- All references now use: `/drugSummary` (camelCase)
+
+**Files Updated**:
+- `server/api/views/risk/views_riskWithSources.py`
+- `frontend/src/pages/Layout/Layout_V2_Sidebar.tsx`
+- `frontend/src/pages/Layout/Layout_V2_Header.tsx`
+- `frontend/src/pages/Files/FileRow.tsx`
+
+## Authentication Flow Fixes
+
+### ProtectedRoute Component
+
+**Problem**: Opening protected routes in new tab caused immediate redirect to login
+
+**Solution**: Wait for auth check to complete
+```typescript
+if (isAuthenticated === null) {
+ return null; // Wait for auth verification
+}
+```
+
+### Layout_V2_Main Component
+
+**Added**: `useAuth()` hook to trigger authentication check on mount
+
+## Testing Checklist
+
+### Manual Testing Steps
+
+1. **As unauthenticated user**:
+ - [ ] Navigate to medication suggester
+ - [ ] Submit patient information
+ - [ ] Expand medication to view sources
+ - [ ] Verify "Download PDF" button appears (green)
+ - [ ] Click button and verify file downloads
+ - [ ] Verify no redirect to login occurs
+
+2. **As authenticated user**:
+ - [ ] Log in to application
+ - [ ] Navigate to medication suggester
+ - [ ] Submit patient information
+ - [ ] Expand medication to view sources
+ - [ ] Verify "View PDF" button appears (blue)
+ - [ ] Click button and verify PDF viewer opens in new tab
+ - [ ] Verify new tab doesn't redirect to login
+
+3. **Edge cases**:
+ - [ ] Test with sources that have no link_url
+ - [ ] Test with sources that have link_url but no upload_file_guid
+ - [ ] Test opening protected route directly in new tab
+ - [ ] Test authentication state persistence across tabs
+
+### Automated Tests
+
+**TODO**: Add integration tests for:
+- PDF button conditional rendering
+- GUID extraction fallback
+- Protected route authentication flow
+
+## Deployment Notes
+
+### Backend Deployment
+
+1. Deploy updated Django code
+2. **No database migrations required**
+3. Restart Django application server
+4. Verify API response includes `upload_file_guid` field
+
+### Frontend Deployment
+
+1. Build frontend with updated code: `npm run build`
+2. Deploy built assets
+3. Clear CDN/browser cache if applicable
+4. Verify button behavior for both auth states
+
+### Rollback Plan
+
+If issues occur:
+1. Revert backend to previous version (API still compatible)
+2. Frontend will use fallback GUID extraction from URL
+3. Feature will degrade gracefully - button may show for all users but behavior remains functional
+
+## Environment Variables
+
+No new environment variables required. Uses existing:
+- `VITE_API_BASE_URL` - Frontend API base URL
+
+## Known Issues / Limitations
+
+1. **GUID Fallback**: If both `upload_file_guid` and `link_url` are missing/invalid, no button appears
+2. **Download Naming**: Downloaded files use server-provided filename, not customizable per-user
+3. **Public Access**: Download endpoint is public - PDFs accessible to anyone with GUID
+
+## Future Enhancements
+
+- [ ] Add loading spinner while PDF downloads
+- [ ] Add analytics tracking for PDF views/downloads
+- [ ] Implement PDF access permissions/restrictions
+- [ ] Add rate limiting to download endpoint
+
+## Support
+
+For questions or issues:
+- GitHub Issues: [Repository Issues Link]
+- Team Contact: balancerteam@codeforphilly.org
+
+## References
+
+- CHANGELOG.md - High-level changes
+- CLAUDE.md - Updated project documentation
+- Code comments in PatientSummary.tsx
diff --git a/frontend/src/api/apiClient.ts b/frontend/src/api/apiClient.ts
index 26a6ab8a..915226d6 100644
--- a/frontend/src/api/apiClient.ts
+++ b/frontend/src/api/apiClient.ts
@@ -3,7 +3,9 @@ import { FormValues } from "../pages/Feedback/FeedbackForm";
import { Conversation } from "../components/Header/Chat";
const baseURL = import.meta.env.VITE_API_BASE_URL;
-export const api = axios.create({
+export const publicApi = axios.create({ baseURL });
+
+export const adminApi = axios.create({
baseURL,
headers: {
Authorization: `JWT ${localStorage.getItem("access")}`,
@@ -11,7 +13,7 @@ export const api = axios.create({
});
// Request interceptor to set the Authorization header
-api.interceptors.request.use(
+adminApi.interceptors.request.use(
(configuration) => {
const token = localStorage.getItem("access");
if (token) {
@@ -29,7 +31,7 @@ const handleSubmitFeedback = async (
message: FormValues["message"],
) => {
try {
- const response = await api.post(`/v1/api/feedback/`, {
+ const response = await publicApi.post(`/v1/api/feedback/`, {
feedbacktype: feedbackType,
name,
email,
@@ -42,10 +44,13 @@ const handleSubmitFeedback = async (
}
};
-const handleSendDrugSummary = async (message: FormValues["message"], guid: string) => {
+const handleSendDrugSummary = async (
+ message: FormValues["message"],
+ guid: string,
+) => {
try {
const endpoint = guid ? `/v1/api/embeddings/ask_embeddings?guid=${guid}` : '/v1/api/embeddings/ask_embeddings';
- const response = await api.post(endpoint, {
+ const response = await adminApi.post(endpoint, {
message,
});
console.log("Response data:", JSON.stringify(response.data, null, 2));
@@ -58,7 +63,7 @@ const handleSendDrugSummary = async (message: FormValues["message"], guid: strin
const handleRuleExtraction = async (guid: string) => {
try {
- const response = await api.get(`/v1/api/rule_extraction_openai?guid=${guid}`);
+ const response = await adminApi.get(`/v1/api/rule_extraction_openai?guid=${guid}`);
// console.log("Rule extraction response:", JSON.stringify(response.data, null, 2));
return response.data;
} catch (error) {
@@ -67,9 +72,12 @@ const handleRuleExtraction = async (guid: string) => {
}
};
-const fetchRiskDataWithSources = async (medication: string, source: "include" | "diagnosis" | "diagnosis_depressed" = "include") => {
+const fetchRiskDataWithSources = async (
+ medication: string,
+ source: "include" | "diagnosis" | "diagnosis_depressed" = "include",
+) => {
try {
- const response = await api.post(`/v1/api/riskWithSources`, {
+ const response = await publicApi.post(`/v1/api/riskWithSources`, {
drug: medication,
source: source,
});
@@ -90,7 +98,7 @@ interface StreamCallbacks {
const handleSendDrugSummaryStream = async (
message: string,
guid: string,
- callbacks: StreamCallbacks
+ callbacks: StreamCallbacks,
): Promise => {
const token = localStorage.getItem("access");
const endpoint = `/v1/api/embeddings/ask_embeddings?stream=true${
@@ -165,12 +173,18 @@ const handleSendDrugSummaryStream = async (
}
}
} catch (parseError) {
- console.error("Failed to parse SSE data:", parseError, "Raw line:", line);
+ console.error(
+ "Failed to parse SSE data:",
+ parseError,
+ "Raw line:",
+ line,
+ );
}
}
}
} catch (error) {
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
+ const errorMessage =
+ error instanceof Error ? error.message : "Unknown error";
console.error("Error in stream:", errorMessage);
callbacks.onError?.(errorMessage);
throw error;
@@ -186,13 +200,13 @@ const handleSendDrugSummaryStreamLegacy = async (
return handleSendDrugSummaryStream(message, guid, {
onContent: onChunk,
onError: (error) => console.error("Stream error:", error),
- onComplete: () => console.log("Stream completed")
+ onComplete: () => console.log("Stream completed"),
});
};
const fetchConversations = async (): Promise => {
try {
- const response = await api.get(`/chatgpt/conversations/`);
+ const response = await publicApi.get(`/chatgpt/conversations/`);
return response.data;
} catch (error) {
console.error("Error(s) during getConversations: ", error);
@@ -202,7 +216,7 @@ const fetchConversations = async (): Promise => {
const fetchConversation = async (id: string): Promise => {
try {
- const response = await api.get(`/chatgpt/conversations/${id}/`);
+ const response = await publicApi.get(`/chatgpt/conversations/${id}/`);
return response.data;
} catch (error) {
console.error("Error(s) during getConversation: ", error);
@@ -212,7 +226,7 @@ const fetchConversation = async (id: string): Promise => {
const newConversation = async (): Promise => {
try {
- const response = await api.post(`/chatgpt/conversations/`, {
+ const response = await adminApi.post(`/chatgpt/conversations/`, {
messages: [],
});
return response.data;
@@ -228,7 +242,7 @@ const continueConversation = async (
page_context?: string,
): Promise<{ response: string; title: Conversation["title"] }> => {
try {
- const response = await api.post(
+ const response = await adminApi.post(
`/chatgpt/conversations/${id}/continue_conversation/`,
{
message,
@@ -244,7 +258,7 @@ const continueConversation = async (
const deleteConversation = async (id: string) => {
try {
- const response = await api.delete(`/chatgpt/conversations/${id}/`);
+ const response = await adminApi.delete(`/chatgpt/conversations/${id}/`);
return response.data;
} catch (error) {
console.error("Error(s) during deleteConversation: ", error);
@@ -255,9 +269,11 @@ const deleteConversation = async (id: string) => {
const updateConversationTitle = async (
id: Conversation["id"],
newTitle: Conversation["title"],
-): Promise<{status: string, title: Conversation["title"]} | {error: string}> => {
+): Promise<
+ { status: string; title: Conversation["title"] } | { error: string }
+> => {
try {
- const response = await api.patch(`/chatgpt/conversations/${id}/update_title/`, {
+ const response = await adminApi.patch(`/chatgpt/conversations/${id}/update_title/`, {
title: newTitle,
});
return response.data;
@@ -268,9 +284,12 @@ const updateConversationTitle = async (
};
// Assistant API functions
-const sendAssistantMessage = async (message: string, previousResponseId?: string) => {
+const sendAssistantMessage = async (
+ message: string,
+ previousResponseId?: string,
+) => {
try {
- const response = await api.post(`/v1/api/assistant`, {
+ const response = await publicApi.post(`/v1/api/assistant`, {
message,
previous_response_id: previousResponseId,
});
@@ -294,5 +313,5 @@ export {
handleSendDrugSummaryStream,
handleSendDrugSummaryStreamLegacy,
fetchRiskDataWithSources,
- sendAssistantMessage
-};
\ No newline at end of file
+ sendAssistantMessage,
+};
diff --git a/frontend/src/components/Header/Chat.tsx b/frontend/src/components/Header/Chat.tsx
index c6315068..a6258865 100644
--- a/frontend/src/components/Header/Chat.tsx
+++ b/frontend/src/components/Header/Chat.tsx
@@ -310,9 +310,9 @@ const Chat: React.FC = ({ showChat, setShowChat }) => {
Hi there, I'm {CHATBOT_NAME}!
- You can ask me questions about your uploaded documents.
- I'll search through them to provide accurate, cited
- answers.
+ You can ask me questions about bipolar medications.
+ I'll search through our database of verified medical
+ journal articles to provide accurate, cited answers.
Learn more about my sources.
diff --git a/frontend/src/components/Header/FeatureMenuDropDown.tsx b/frontend/src/components/Header/FeatureMenuDropDown.tsx
index b1bbf03e..36d72792 100644
--- a/frontend/src/components/Header/FeatureMenuDropDown.tsx
+++ b/frontend/src/components/Header/FeatureMenuDropDown.tsx
@@ -4,13 +4,13 @@ export const FeatureMenuDropDown = () => {
const location = useLocation();
const currentPath = location.pathname;
return (
-
- Welcome to Balancer’s first release! Found a bug or have feedback? Let us know {" "}
+ Welcome to Balancer’s first release! Found a bug or have feedback? Let
+ us know{" "}
- here {" "}
+ here{" "}
- or email {" "}
-
+ or email{" "}
+
balancerteam@codeforphilly.org
.
>
diff --git a/frontend/src/pages/RulesManager/RulesManager.tsx b/frontend/src/pages/RulesManager/RulesManager.tsx
index be4980d4..0268a4c8 100644
--- a/frontend/src/pages/RulesManager/RulesManager.tsx
+++ b/frontend/src/pages/RulesManager/RulesManager.tsx
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react";
import Layout from "../Layout/Layout";
import Welcome from "../../components/Welcome/Welcome";
import ErrorMessage from "../../components/ErrorMessage";
-import { api } from "../../api/apiClient";
+import { adminApi } from "../../api/apiClient";
import { ChevronDown, ChevronUp } from "lucide-react";
interface Medication {
@@ -69,7 +69,7 @@ function RulesManager() {
const fetchMedRules = async () => {
try {
const url = `${baseUrl}/v1/api/medRules`;
- const { data } = await api.get(url);
+ const { data } = await adminApi.get(url);
if (!data || !Array.isArray(data.results)) {
throw new Error("Invalid response format");
diff --git a/frontend/src/routes/routes.tsx b/frontend/src/routes/routes.tsx
index 2e6273d4..dc974e85 100644
--- a/frontend/src/routes/routes.tsx
+++ b/frontend/src/routes/routes.tsx
@@ -1,6 +1,7 @@
import App from "../App";
import RouteError from "../pages/404/404.tsx";
import LoginForm from "../pages/Login/Login.tsx";
+import Logout from "../pages/Logout/Logout.tsx";
import AdminPortal from "../pages/AdminPortal/AdminPortal.tsx";
import ResetPassword from "../pages/Login/ResetPassword.tsx";
import ResetPasswordConfirm from "../pages/Login/ResetPasswordConfirm.tsx";
@@ -17,6 +18,7 @@ import UploadFile from "../pages/DocumentManager/UploadFile.tsx";
import ListofFiles from "../pages/Files/ListOfFiles.tsx";
import RulesManager from "../pages/RulesManager/RulesManager.tsx";
import ManageMeds from "../pages/ManageMeds/ManageMeds.tsx";
+import ProtectedRoute from "../components/ProtectedRoute/ProtectedRoute.tsx";
const routes = [
{
@@ -26,21 +28,21 @@ const routes = [
},
{
path: "listoffiles",
- element: ,
+ element: ,
errorElement: ,
},
{
path: "rulesmanager",
- element: ,
+ element: ,
errorElement: ,
},
{
path: "uploadfile",
- element: ,
+ element: ,
},
{
path: "drugSummary",
- element: ,
+ element: ,
},
{
path: "register",
@@ -50,6 +52,10 @@ const routes = [
path: "login",
element: ,
},
+ {
+ path: "logout",
+ element: ,
+ },
{
path: "resetPassword",
element: ,
@@ -80,11 +86,11 @@ const routes = [
},
{
path: "adminportal",
- element: ,
+ element: ,
},
{
path: "Settings",
- element: ,
+ element: ,
},
{
path: "medications",
@@ -92,7 +98,7 @@ const routes = [
},
{
path: "managemeds",
- element: ,
+ element: ,
},
];
diff --git a/frontend/src/services/actions/auth.tsx b/frontend/src/services/actions/auth.tsx
index bfbfbe41..3dcfcac5 100644
--- a/frontend/src/services/actions/auth.tsx
+++ b/frontend/src/services/actions/auth.tsx
@@ -151,6 +151,10 @@ export const login =
try {
const res = await axios.post(url, body, config);
+ // Clear session data from previous unauthenticated session
+ sessionStorage.removeItem('currentConversation');
+ sessionStorage.removeItem('patientInfos');
+
dispatch({
type: LOGIN_SUCCESS,
payload: res.data,
@@ -172,8 +176,9 @@ export const login =
};
export const logout = () => async (dispatch: AppDispatch) => {
- // Clear chat conversation data on logout for security
- sessionStorage.removeItem("currentConversation");
+ // Clear session data on logout for privacy
+ sessionStorage.removeItem('currentConversation');
+ sessionStorage.removeItem('patientInfos');
dispatch({
type: LOGOUT,
diff --git a/frontend/src/services/actions/types.tsx b/frontend/src/services/actions/types.tsx
index add0dad9..c7f73b94 100644
--- a/frontend/src/services/actions/types.tsx
+++ b/frontend/src/services/actions/types.tsx
@@ -21,7 +21,8 @@ export const LOGOUT = "LOGOUT";
export interface RootState {
auth: {
error: any;
- isAuthenticated: boolean;
+ // Catch any code that doesn't handle the null case by matching the actual reducer state defined in auth.ts
+ isAuthenticated: boolean | null;
isSuperuser: boolean;
};
}
diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js
index bcc1e693..4161a741 100644
--- a/frontend/tailwind.config.js
+++ b/frontend/tailwind.config.js
@@ -10,8 +10,15 @@ export default {
lora: "'Lora', serif",
'quicksand': ['Quicksand', 'sans-serif']
},
+ keyframes: {
+ 'loading': {
+ '0%': { left: '-40%' },
+ '100%': { left: '100%' },
+ },
+ },
animation: {
- 'pulse-bounce': 'pulse-bounce 2s infinite', // Adjust duration and iteration as needed
+ 'pulse-bounce': 'pulse-bounce 2s infinite',
+ 'loading': 'loading 3s infinite',
},
plugins: [],
},
diff --git a/server/api/services/embedding_services.py b/server/api/services/embedding_services.py
index 6fd34d35..b50dd750 100644
--- a/server/api/services/embedding_services.py
+++ b/server/api/services/embedding_services.py
@@ -1,5 +1,4 @@
-# services/embedding_services.py
-
+from django.db.models import Q
from pgvector.django import L2Distance
from .sentencetTransformer_model import TransformerModel
@@ -39,17 +38,29 @@ def get_closest_embeddings(
- file_id: GUID of the source file
"""
- #
transformerModel = TransformerModel.get_instance().model
embedding_message = transformerModel.encode(message_data)
- # Start building the query based on the message's embedding
- closest_embeddings_query = (
- Embeddings.objects.filter(upload_file__uploaded_by=user)
- .annotate(
- distance=L2Distance("embedding_sentence_transformers", embedding_message)
+
+ if user.is_authenticated:
+ # User sees their own files + files uploaded by superusers
+ closest_embeddings_query = (
+ Embeddings.objects.filter(
+ Q(upload_file__uploaded_by=user) | Q(upload_file__uploaded_by__is_superuser=True)
+ )
+ .annotate(
+ distance=L2Distance("embedding_sentence_transformers", embedding_message)
+ )
+ .order_by("distance")
+ )
+ else:
+ # Unauthenticated users only see superuser-uploaded files
+ closest_embeddings_query = (
+ Embeddings.objects.filter(upload_file__uploaded_by__is_superuser=True)
+ .annotate(
+ distance=L2Distance("embedding_sentence_transformers", embedding_message)
+ )
+ .order_by("distance")
)
- .order_by("distance")
- )
# Filter by GUID if provided, otherwise filter by document name if provided
if guid:
diff --git a/server/api/views/assistant/views.py b/server/api/views/assistant/views.py
index 32089c58..f31ab475 100644
--- a/server/api/views/assistant/views.py
+++ b/server/api/views/assistant/views.py
@@ -7,7 +7,7 @@
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
-from rest_framework.permissions import IsAuthenticated
+from rest_framework.permissions import AllowAny
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
@@ -111,7 +111,7 @@ def invoke_functions_from_response(
@method_decorator(csrf_exempt, name="dispatch")
class Assistant(APIView):
- permission_classes = [IsAuthenticated]
+ permission_classes = [AllowAny]
def post(self, request):
try:
@@ -196,28 +196,42 @@ def search_documents(query: str, user=user) -> str:
return f"Error searching documents: {str(e)}. Please try again if the issue persists."
INSTRUCTIONS = """
- You are an AI assistant that helps users find and understand information about bipolar disorder
- from their uploaded bipolar disorder research documents using semantic search.
-
+ You are an AI assistant that helps users find and understand information about bipolar disorder
+ from your internal library of bipolar disorder research sources using semantic search.
+
+ IMPORTANT CONTEXT:
+ - You have access to a library of sources that the user CANNOT see
+ - The user did not upload these sources and doesn't know about them
+ - You must explain what information exists in your sources and provide clear references
+
+ TOPIC RESTRICTIONS:
+ When a prompt is received that is unrelated to bipolar disorder, mental health treatment,
+ or psychiatric medications, respond by saying you are limited to bipolar-specific conversations.
+
SEMANTIC SEARCH STRATEGY:
- Always perform semantic search using the search_documents function when users ask questions
- Use conceptually related terms and synonyms, not just exact keyword matches
- Search for the meaning and context of the user's question, not just literal words
- Consider medical terminology, lay terms, and related conditions when searching
-
+
FUNCTION USAGE:
- - When a user asks about information that might be in their documents ALWAYS use the search_documents function first
+ - When a user asks about information that might be in your source library, ALWAYS use the search_documents function first
- Perform semantic searches using concepts, symptoms, treatments, and related terms from the user's question
- - Only provide answers based on information found through document searches
-
+ - Only provide answers based on information found through your source searches
+
RESPONSE FORMAT:
After gathering information through semantic searches, provide responses that:
1. Answer the user's question directly using only the found information
2. Structure responses with clear sections and paragraphs
- 3. Include citations using this exact format: ***[Name {name}, Page {page_number}]***
- 4. Only cite information that directly supports your statements
-
- If no relevant information is found in the documents, clearly state that the information is not available in the uploaded documents.
+ 3. Explain what information you found in your sources and provide context
+ 4. Include citations using this exact format: [Name {name}, Page {page_number}]
+ 5. Only cite information that directly supports your statements
+
+ If no relevant information is found in your source library, clearly state that the information
+ is not available in your current sources.
+
+ REMEMBER: You are working with an internal library of bipolar disorder sources that the user
+ cannot see. Always search these sources first, explain what you found, and provide proper citations.
"""
MODEL_DEFAULTS = {
diff --git a/server/api/views/conversations/views.py b/server/api/views/conversations/views.py
index d5921eaf..eeb68809 100644
--- a/server/api/views/conversations/views.py
+++ b/server/api/views/conversations/views.py
@@ -1,7 +1,7 @@
from rest_framework.response import Response
from rest_framework import viewsets, status
from rest_framework.decorators import action
-from rest_framework.permissions import IsAuthenticated
+from rest_framework.permissions import AllowAny
from rest_framework.exceptions import APIException
from django.http import JsonResponse
from bs4 import BeautifulSoup
@@ -81,7 +81,7 @@ def __init__(self, detail=None, code=None):
class ConversationViewSet(viewsets.ModelViewSet):
serializer_class = ConversationSerializer
- permission_classes = [IsAuthenticated]
+ permission_classes = [AllowAny]
def get_queryset(self):
return Conversation.objects.filter(user=self.request.user)
diff --git a/server/api/views/feedback/views.py b/server/api/views/feedback/views.py
index dcbef992..d0f0e1da 100644
--- a/server/api/views/feedback/views.py
+++ b/server/api/views/feedback/views.py
@@ -1,4 +1,4 @@
-
+from rest_framework.permissions import AllowAny
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
@@ -8,6 +8,8 @@
class FeedbackView(APIView):
+ permission_classes = [AllowAny]
+
def post(self, request, *args, **kwargs):
serializer = FeedbackSerializer(data=request.data)
if serializer.is_valid():
diff --git a/server/api/views/listMeds/views.py b/server/api/views/listMeds/views.py
index 1976458e..fcd0edf2 100644
--- a/server/api/views/listMeds/views.py
+++ b/server/api/views/listMeds/views.py
@@ -1,4 +1,5 @@
from rest_framework import status
+from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView
@@ -21,6 +22,8 @@
class GetMedication(APIView):
+ permission_classes = [AllowAny]
+
def post(self, request):
data = request.data
state_query = data.get('state', '')
@@ -71,6 +74,8 @@ def post(self, request):
class ListOrDetailMedication(APIView):
+ permission_classes = [AllowAny]
+
def get(self, request):
name_query = request.query_params.get('name', None)
if name_query:
diff --git a/server/api/views/risk/views_riskWithSources.py b/server/api/views/risk/views_riskWithSources.py
index 0be43dbb..c02908fc 100644
--- a/server/api/views/risk/views_riskWithSources.py
+++ b/server/api/views/risk/views_riskWithSources.py
@@ -1,6 +1,7 @@
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
+from rest_framework.permissions import AllowAny
from api.views.listMeds.models import Medication
from api.models.model_medRule import MedRule, MedRuleSource
import openai
@@ -8,6 +9,8 @@
class RiskWithSourcesView(APIView):
+ permission_classes = [AllowAny]
+
def post(self, request):
openai.api_key = os.environ.get("OPENAI_API_KEY")
diff --git a/server/api/views/uploadFile/views.py b/server/api/views/uploadFile/views.py
index 6904e061..69dfb996 100644
--- a/server/api/views/uploadFile/views.py
+++ b/server/api/views/uploadFile/views.py
@@ -1,5 +1,5 @@
from rest_framework.views import APIView
-from rest_framework.permissions import IsAuthenticated
+from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
from rest_framework.generics import UpdateAPIView
@@ -15,16 +15,15 @@
class UploadFileView(APIView):
+ def get_permissions(self):
+ if self.request.method == 'GET':
+ return [AllowAny()] # Public access
+ return [IsAuthenticated()] # Auth required for other methods
def get(self, request, format=None):
print("UploadFileView, get list")
- # Get the authenticated user
- user = request.user
-
- # Filter the files uploaded by the authenticated user
- files = UploadFile.objects.filter(uploaded_by=user.id).defer(
- 'file').order_by('-date_of_upload')
+ files = UploadFile.objects.all().defer('file').order_by('-date_of_upload')
serializer = UploadFileSerializer(files, many=True)
return Response(serializer.data)
@@ -156,12 +155,11 @@ def delete(self, request, format=None):
class RetrieveUploadFileView(APIView):
- permission_classes = [IsAuthenticated]
+ permission_classes = [AllowAny]
def get(self, request, guid, format=None):
try:
- file = UploadFile.objects.get(
- guid=guid, uploaded_by=request.user.id)
+ file = UploadFile.objects.get(guid=guid)
response = HttpResponse(file.file, content_type='application/pdf')
# print(file.file[:100])
response['Content-Disposition'] = f'attachment; filename="{file.file_name}"'