# Project export: Fast Aid PI

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: Cal Hacks 12.0
- Tagline: An AI-powered prediagnosis and communication tool that eases patient stress, speeds up doctor handoffs, and bridges gaps between patients and practitioners with secure, structured insights.
- Devpost: https://devpost.com/software/fast-aid-pi
- GitHub: https://github.com/ericqn/fast-aid-pi
- Video: https://www.youtube.com/embed/eswqNSnWVDc?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — ericqn (13 commits), Cappillen Lee (10 commits)

## Devpost submission (written by the team)

### Inspiration

Healthcare workers are generally very stressed and the technology used in many hospitals and healthcare firms are very outdated. One of my friends told me about how in the hospital he works at, they still page doctors instead of just instantly messaging them, causing inefficient doctor transfers and slower communication. And many of these inefficiencies transfer over to the patients, who are usually anxious waiting for answers from the doctor. Fast Aid PI tackles a specific area: patient prediagnoses. We hope to relieve stress for the patient, give them a course of action before their doctor appointments, and speed up data transfer from patient to doctor.

### What it does

Our app takes in medical data from the patient, prediagnoses them using a machine learning model (mocked by an LLM API call), gives them a recommended path of action, and transfers the data to the doctor to take notes for their upcoming appointments.

### How we built it

For the planning phase, we consulted friends who worked in the medical space and asked for any large problems or concerns they experience. We built it using a node.js frontend and fastAPI backend server.

### Challenges we ran into

We had various challenges while prototyping the idea for this app: How to stay within HIPAA guidelines for sharing information and data How to prevent users from getting overwhelmed by information online How to ensure users effectively document their well-being to the doctor How to make sure doctors can see and view information effectively As for technical challenges, the main challenge came via communicating clear goals and linking the client and server architectures.

### Accomplishments we're proud of

Following the structure of the challenges above, here is how we tackled each one of them: Via providing an easily scalable authentication framework with password encryption, preventing views from users and practitioners unless users give consent Providing a simple, easily followable structure given in the prediagnosis phase rather than listing out all possible medications. We want the actual diagnosis responsibility to be the doctor's. Providing an AI-powered chatbot to help ease potential stress induced on the user and clarify any questions before going to a practitioner Providing persistence of chats, forms, and prediagnoses on the user side Providing a summary view of user prediagnoses that doctors can take notes on

### What we learned

We learned a lot about the problems in healthcare, prototyped various ideas, and decided to tackle prediagnosis and appointment stress. Through researching problems and communicating with people working and/or studying healthcare, we were able to determine the best course of action for each feature of our app.

### What's next

There are a lot of improvements that can be made! We have some stretch features that we were not able to get to and some bigger issues that we couldn't tackle: Doctors should be able to make edits and highlight certain parts of prediagnosis results sent from patients. Adding a system where doctors can easily share patient diagnoses and treatment with one another. This tackles the communication error so common when patients must deal with multiple specialists to treat their cause. Users registered as doctors can add a patient medical history to preprocess the AI-powered prediagnosis step to create a more accurate report. Enforcing stronger authentication and encryption guidelines to abide by HIPAA guidelines. Training an actual ML model based on real patient cases and doctor prescriptions to improve runtime and cost efficiencies. Constantly calling a powerful LLM is unnecessary for the prediagnosis of symptoms and potential diseases.

## README (from the GitHub repository)

## Fast Aid PI

Bridging the gap between patients and doctors through AI-powered prediagnosis and communication. Various features to help prediagnose patients, offer solutions, refer to specialists and relevant practitioners, alongside features to help doctors provide care more efficiently.

## Features:

- Secure authentication and role-based access
- AI chatbot for prediagnosis and patient reassurance
- Prediagnosis Feature: Analyzes questionnaire then offers action plan, recommended practitioners, and health support.
- Persistent chat and prediagnosis records
- Patients can add relevant recommended doctors to their conversations and prediagnoses.

## Tech Stack:

- Frontend: Typescript, React, Tailwind, ESLint, Anthropic
- Backend: Python, FastAPI, uvicorn, SQLAlchemy, Anthropic, pyjwt, passlib, 

## Getting Started:

Backend: Uses uv to handle packages.

```
$ cd fast-aid-pi/server
$ uv install
$ uv run server.py
```

Server should be running on http://localhost:8000

---

Frontend: Packages managed by npm.

```
$ cd fast-aid-pi/server
$ npm install
$ npm run dev
```

Frontend components should be running and visible on http://localhost:3000

## Next Steps:

- Allow doctors to edit and highlight patient summaries
- Enable cross-specialist communication for holistic treatment
- Add patient history integration for more accurate prediagnoses
- Improve encryption and compliance
- Train a custom ML model for prediagnosis instead of relying on an LLM

## Detected evidence (automated analysis)

Indexed codebase: 30 recognized source files, 161 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- JavaScript (language) — detected in the code
- Next.js (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Node.js (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (39 of 39)

```
client/.gitignore
client/app/api/chat/route.ts
client/app/doctor/dashboard/page.tsx
client/app/globals.css
client/app/layout.tsx
client/app/page.tsx
client/components/AuthContext.tsx
client/components/AuthModal.tsx
client/components/DoctorDashboard.tsx
client/components/MarkdownParser.tsx
client/components/Questionnaire.tsx
client/components/UserProfile.tsx
client/eslint.config.mjs
client/next.config.js
client/next.config.ts
client/package.json
client/postcss.config.js
client/README.md
client/tailwind.config.js
client/tsconfig.json
README.md
server/.gitignore
server/.python-version
server/database.db
server/pyproject.toml
server/server.py
server/source/__init__.py
server/source/app.py
server/source/database/__init__.py
server/source/database/auth.py
server/source/database/models.py
server/source/database/operations.py
server/source/ml_models/__init__.py
server/source/ml_models/suggestions.py
server/source/schemas.py
server/test_api.py
server/test_suggestions.py
server/test.sh
server/uv.lock
```

### Dependencies

- client/package.json: @anthropic-ai/sdk@^0.67.0, @tailwindcss/postcss@^4, @types/node@^20, @types/react@^19, @types/react-dom@^19, eslint@^9, eslint-config-next@16.0.0, next@16.0.0, react@19.2.0, react-dom@19.2.0, tailwindcss@^4, typescript@^5
- server/pyproject.toml: anthropic@>=0.71.0, bcrypt@>=4.0.0,<5.0.0, email-validator@>=2.1.0, fastapi@>=0.120.0, passlib@>=1.7.4, pyjwt@>=2.8.0, python-dotenv@>=1.1.1, python-multipart@>=0.0.6, requests@>=2.31.0, sqlalchemy@>=2.0.44, uvicorn@>=0.38.0

### Recent commits (newest first)

- Updated README
- Merge branch 'main' of github.com:ericqn/fast-aid-pi
- Merge pull request #4 from ericqn/capb
- questionnaire and quick actions
- Merge branch 'main' of github.com:ericqn/fast-aid-pi
- Merge pull request #3 from ericqn/capb
- persist chats
- added removal doctor endpoint + more tests
- adding tests
- testing endpoints
- Merge branch 'main' of github.com:ericqn/fast-aid-pi
- testing endpoints
- Merge pull request #2 from ericqn/capb
- touch up /
- add action items sidebar
- added endpoints
- Merge pull request #1 from ericqn/capb
- min viable design
- building db schemas
- init client app

## Key source files (fetched from GitHub, selected and truncated for size)

### server/pyproject.toml

```
[project]
name = "server"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
    "anthropic>=0.71.0",
    "fastapi>=0.120.0",
    "python-dotenv>=1.1.1",
    "sqlalchemy>=2.0.44",
    "uvicorn>=0.38.0",
    "passlib>=1.7.4",
    "bcrypt>=4.0.0,<5.0.0",
    "pyjwt>=2.8.0",
    "python-multipart>=0.0.6",
    "email-validator>=2.1.0",
    "requests>=2.31.0",
]

```

### client/package.json

```
{
  "name": "client",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint"
  },
  "dependencies": {
    "@anthropic-ai/sdk": "^0.67.0",
    "next": "16.0.0",
    "react": "19.2.0",
    "react-dom": "19.2.0"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "16.0.0",
    "tailwindcss": "^4",
    "typescript": "^5"
  }
}

```

### server/server.py

```python
from source.app import app

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)
```

### client/app/layout.tsx

```typescript
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
import { AuthProvider } from '@/components/AuthContext'

const inter = Inter({ subsets: ['latin'] })

export const metadata: Metadata = {
  title: 'HealthChat - Your Medical Assistant',
  description: 'Modern healthcare chat application',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <AuthProvider>{children}</AuthProvider>
      </body>
    </html>
  )
}

```

### server/source/app.py

```python
from fastapi import FastAPI, Depends, HTTPException, status, APIRouter
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.orm import Session
from typing import Optional, List

from .database import models, operations, auth as auth_module
from .database.models import get_db
from .ml_models.suggestions import generate_prediagnosis
from . import schemas

# Initialize FastAPI app
router = APIRouter()

app = FastAPI(
    title="Fast Aid API",
    description="Medical prediagnosis and consultation API",
    version="1.0.0"
)

# CORS middleware configuration
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # Update with specific origins in production
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Security
security = HTTPBearer()

# ============= DEPENDENCY FUNCTIONS =============


def get_current_user(
    credentials: HTTPAuthorizationCredentials = Depends(security),
    db: Session = Depends(get_db)
) -> models.User:
    """Extract and verify current user from JWT token"""
    token = credentials.credentials
    user = auth_module.get_user_from_token(db, token)

    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid authentication credentials",
            headers={"WWW-Authenticate": "Bearer"},
        )

    return user


def get_current_patient(current_user: models.User = Depends(get_current_user)) -> models.User:
    """Ensure current user is a patient"""
    if current_user.role != models.UserRole.PATIENT:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Access denied. Patient role required."
        )
    return current_user


def get_current_doctor(current_user: models.User = Depends(get_current_user)) -> models.User:
    """Ensure current user is a doctor"""
    if current_user.role != models.UserRole.DOCTOR:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Access denied. Doctor role required."
        )
    return current_user


# ============= HEALTH CHECK =============

@router.get("/", tags=["Health"])
def health_check():
    """Health check endpoint"""
    return {"status": "healthy", "service": "Fast Aid API"}


# ============= AUTHENTICATION ENDPOINTS =============

@router.post("/auth/register", response_model=schemas.UserResponse, tags=["Authentication"])
def register(user_data: schemas.UserRegister, db: Session = Depends(get_db)):
    """Register a new user (patient, doctor, or admin)"""
    # Check if user already exists
    existing_user = operations.get_user_by_email(db, user_data.email)
    if existing_user:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Email already registered"
        )

    # Convert role string to enum
    role_map = {
        "patient": models.UserRole.PATIENT,
        "doctor": models.UserRole.DOCTOR,
        "admin": models.UserRole.ADMIN
    }

    role = role_map.get(user_data.role, models.UserRole.PATIENT)

    # Register user
    user = auth_module.register_user(
        db=db,
        name=user_data.name,
        email=user_data.email,
        password=user_data.password,
        role=role
    )

    return user


@router.post("/auth/login", response_model=schemas.Token, tags=["Authentication"])
def login(credentials: schemas.UserLogin, db: Session = Depends(get_db)):
    """Login and receive JWT access token"""
    result = auth_module.login(db, credentials.email, credentials.password)

    if not result:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect email or password",
            headers={"WWW-Authenticate": "Bearer"},
        )

    return result


@router.get("/auth/me", response_model=schemas.UserResponse, tags=["Authentication"])
def get_current_user_info(current_user: models.User = Depends(get_current_user)):
    """Get current authenticated user information"""
    return current_user


# ============= USER & MEDICAL HISTORY ENDPOINTS =============

@router.get("/users/{user_id}", response_model=schemas.UserResponse, tags=["Users"])
def get_user(
    user_id: int,
    current_user: models.User = Depends(get_current_user),
    db: Session = Depends(get_db)
):
    """Get user by ID (patients can only view their own profile)"""
    # Patients can only view their own profile
    if current_user.role == models.UserRole.PATIENT and current_user.id != user_id:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Access denied"
        )

    user = operations.get_user_by_id(db, user_id)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="User not found"
        )

    return user


@router.put("/users/{user_id}/medical-history", response_model=schemas.UserResponse, tags=["Users"])
def update_medical_history(
    user_id: int,
    medical_history: schemas.MedicalHistoryUpdate,
    current_user: models.User = Depends(get_current_user),
    db: Session = Depends(get_db)
):
    """Update patient's medical history"""
    # Patients can only update their own medical history
    if current_user.role == models.UserRole.PATIENT and current_user.id != user_id:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Access denied"
        )

    user = operations.update_user_medical_history(
        db, user_id, medical_history.model_dump(exclude_unset=True)
    )

    if not user:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="User not found"
        )

    return user


# ============= CONVERSATION ENDPOINTS =============

@router.post("/conversations", response_model=schemas.ConversationResponse, tags=["Conversatio
[truncated — 11252 more characters]
```

### client/app/page.tsx

```typescript
'use client'

import { useAuth } from '@/components/AuthContext'
import { AuthModal } from '@/components/AuthModal'
import { InlineMarkdownRenderer } from '@/components/MarkdownParser'
import { Questionnaire } from '@/components/Questionnaire'
import { UserProfile } from '@/components/UserProfile'
import { useEffect, useState } from 'react'

type Message = {
  id: string
  content: string | React.ReactNode
  role: 'user' | 'assistant'
  timestamp: Date
}

type Chat = {
  id: string
  title: string
  messages: Message[]
  lastUpdated: Date
}

type ActionItem = {
  id: string
  title: string
  description: string
  priority: 'urgent' | 'high' | 'normal'
  createdAt: Date
}

type ConversationResponse = {
  id: string
  patient_id: number
  doctor_id?: number | null
  title: string
  created_at: string
  updated_at: string
}

type PrediagnosisResponse = {
  id: number
  conversation_id: string
  patient_id: number
  potential_diseases: string
  course_of_action: string
  support_messages: string
  recommended_practitioners: string
  created_at: string
}

export default function Home() {
  const [chats, setChats] = useState<Chat[]>([])
  const { user, isAuthenticated, token, isLoading: authLoading } = useAuth()
  const [activeChat, setActiveChat] = useState<string>('')
  const [inputMessage, setInputMessage] = useState('')
  const [isSidebarOpen, setIsSidebarOpen] = useState(false)
  const [isAuthModalOpen, setIsAuthModalOpen] = useState(false);
  const [isRightSidebarOpen, setIsRightSidebarOpen] = useState(false);
  const [isLoadingConversations, setIsLoadingConversations] = useState(false);
  const [loadedConversations, setLoadedConversations] = useState<Set<string>>(new Set());
  const [isLoadingMessages, setIsLoadingMessages] = useState(false);
  const [isFinishedQuestionnaire, setIsFinishedQuestionnaire] = useState(false);
  const [isLoadingPrediagnoses, setIsLoadingPrediagnoses] = useState(false);
  const [actionItems, setActionItems] = useState<ActionItem[]>([
    {
      id: '1',
      title: 'Schedule Follow-up Appointment',
      description: 'Book a follow-up appointment to discuss your test results and treatment plan.',
      priority: 'high',
      createdAt: new Date(Date.now() - 3600000),
    },
    {
      id: '2',
      title: 'Update Medication List',
      description: 'Please update your current medications in your health profile.',
      priority: 'normal',
      createdAt: new Date(Date.now() - 7200000),
    },
    {
      id: '3',
      title: 'Complete Health Questionnaire',
      description: 'Fill out the pre-visit questionnaire before your next appointment.',
      priority: 'normal',
      createdAt: new Date(Date.now() - 86400000),
    },
  ]);



  // Fetch conversations from API
  const fetchConversations = async () => {
    setIsLoadingConversations(true);
    try {
      const response = await fetch(
        process.env.NEXT_PUBLIC_SERVER_ENDPOINT + '/api/conversations/',
        {
          method: 'GET',
          headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${token}`
          },
        }
      );

      if (!response.ok) {
        throw new Error(`Failed to fetch conversations: ${response.statusText}`);
      }

      const conversations: ConversationResponse[] = await response.json();

      // Create chats with empty messages
      const apiChats: Chat[] = conversations.map(conv => ({
        id: conv.id,
        title: conv.title,
        messages: [], // Will be loaded on demand
        lastUpdated: new Date(conv.updated_at)
      }));

      setChats(apiChats);

      // Set the first chat as active if available
      if (apiChats.length > 0) {
        setActiveChat(apiChats[0].id);
      }
    } catch (error) {
      console.error('Error fetching conversations:', error);
    } finally {
      setIsLoadingConversations(false);
    }
  };
  const loadConversationMessages = async (conversationId: string) => {
    // Don't reload if already loaded
    if (loadedConversations.has(conversationId)) {
      return;
    }

    if (!token) return;
    setIsLoadingMessages(true);
    try {
      const response = await fetch(
        `${process.env.NEXT_PUBLIC_SERVER_ENDPOINT}/api/conversations/${conversationId}/messages`,
        {
          method: 'GET',
          headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${token}`,
          },
        }
      );

      if (!response.ok) {
        throw new Error('Failed to load messages');
      }

      const messages = await response.json();

      // Transform messages
      const transformedMessages: Message[] = messages.map((msg: any) => ({
        id: msg.id,
        content: msg.content,
        role: msg.role,
        timestamp: new Date(msg.created_at),
      }));

      // Update the chat with loaded messages
      setChats(prevChats =>
        prevChats.map(chat =>
          chat.id === conversationId
            ? { ...chat, messages: transformedMessages }
            : chat
        )
      );

      // Mark as loaded
      setLoadedConversations(prev => new Set([...prev, conversationId]));
    } catch (error) {
      console.error('Error loading conversation messages:', error);
    } finally {
      setIsLoadingMessages(false);
    }
  };

  // Create a new conversation via API
  const createConversationAPI = async (title: string): Promise<ConversationResponse | null> => {
    if (!token) {
      console.log('No token available, cannot create conversation');
      return null;
    }

    try {
      const response = await fetch(process.env.NEXT_PUBLIC_SERVER_ENDPOINT + '/api/conversations', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${token}`,
        },
        body: JSON.stringify({ title }),
      });

      if (!response.ok) {
        throw new Error(`Failed to create conversation: ${response.statusText}`);
      }

     
[truncated — 30253 more characters]
```

### client/app/doctor/dashboard/page.tsx

```typescript
import { DoctorDashboard } from '../../../components/DoctorDashboard'

export default function DoctorPage() {
  return <DoctorDashboard />
}

```

### client/app/api/chat/route.ts

```typescript
import Anthropic from '@anthropic-ai/sdk';
import { NextRequest, NextResponse } from 'next/server';

// Initialize the Anthropic client
const anthropic = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY,
});

export async function POST(request: NextRequest) {
  try {
    // Parse the request body
    const body = await request.json();
    const { messages, conversationId } = body;

    // Validate the request
    if (!messages || !Array.isArray(messages) || messages.length === 0) {
      return NextResponse.json(
        { error: 'Messages array is required' },
        { status: 400 }
      );
    }

    // Format messages for Anthropic API
    // Remove system messages and format for Anthropic's expected structure
    const formattedMessages = messages
      .filter((msg: any) => msg.role !== 'system')
      .map((msg: any) => ({
        role: msg.role,
        content: msg.content,
      }));

    // Create a readable stream
    const encoder = new TextEncoder();
    const stream = new ReadableStream({
      async start(controller) {
        try {
          // Create the streaming message
          const messageStream = await anthropic.messages.create({
            model: 'claude-sonnet-4-5-20250929',
            max_tokens: 4096,
            system: `You are a helpful healthcare AI assistant. You provide information and support but always remind users to consult with healthcare professionals for medical advice. Be empathetic, clear, and professional.`,
            messages: formattedMessages,
            stream: true,
          });

          // Process the stream
          for await (const event of messageStream) {
            if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {
              // Send the text chunk to the client
              const text = event.delta.text;
              controller.enqueue(encoder.encode(`data: ${JSON.stringify({ text })}\n\n`));
            }

            if (event.type === 'message_stop') {
              // Send completion signal
              controller.enqueue(encoder.encode(`data: ${JSON.stringify({ done: true })}\n\n`));
            }
          }

          controller.close();
        } catch (error: any) {
          console.error('Stream error:', error);
          controller.enqueue(
            encoder.encode(`data: ${JSON.stringify({ error: error.message })}\n\n`)
          );
          controller.close();
        }
      },
    });

    // Return the stream with appropriate headers
    return new NextResponse(stream, {
      headers: {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive',
      },
    });
  } catch (error: any) {
    console.error('API error:', error);
    return NextResponse.json(
      { error: error.message || 'Internal server error' },
      { status: 500 }
    );
  }
}

```

### client/postcss.config.js

```javascript
module.exports = {
  plugins: {
    '@tailwindcss/postcss': {},
  },
}

```

### client/next.config.js

```javascript
/** @type {import('next').NextConfig} */
const nextConfig = {}

module.exports = nextConfig

```

[20 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]