# Project export: Juno AI

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: UC Berkeley AI Hackathon 2025
- Tagline: Understanding your legal rights, simplified.
- Devpost: https://devpost.com/software/juno-ai
- GitHub: https://github.com/briancontreras/JunoAi-CalHacks2025
- Team: 4 GitHub contributor(s) — Santiago Del Rio Obando (17 commits), briancontreras (16 commits), Osvaldo Valdiviezo (2 commits), Sanchez-Macario (1 commits)

## Devpost submission (written by the team)

### Overview

Empowering immigrants and communities with knowledge, voice, and protection. Overview Juno is a tool designed to help individuals, especially immigrants, understand and assert their rights when interacting with law enforcement or local authorities. The system allows users to: Access legal rights based on state,and federal laws Get guidance on how to navigate future interactions Why We Built This Many community members—especially immigrants—feel vulnerable during encounters with authorities due to language gaps, legal uncertainty, or fear. Juno empowers users with knowledge, emotional support, and a practical toolset to protect their rights. Key Features Location-based rights lookup for personalized legal insight Conversational AI that helps users feel prepared and informed Privacy and Security Security is a core principle of Juno. We prioritize: Transparent data use policies Local or consent-based data storage Status This project is being developed as part of the UC Berkeley AI Hackathon—rapid prototyping in progress! Built with care by a team passionate about justice, tech, and community resilience.

## README (from the GitHub repository)

# Juno Legal AI Assistant

A legal AI assistant powered by GROQ LLM with advanced session memory capabilities.

## Features

### 🤖 AI-Powered Legal Assistance
- Legal reasoning based on U.S. federal and state law
- Location-aware legal advice
- Voice input and text-to-speech capabilities
- Real-time transcription

### 💾 Session Memory System
- **Persistent Conversations**: Each chat session maintains conversation history
- **Context Awareness**: AI remembers previous questions and provides contextual responses
- **Session Management**: Create, switch between, and delete multiple chat sessions
- **Automatic Cleanup**: Sessions expire after 24 hours to manage memory
- **Session Limits**: Maximum 100 active sessions to prevent memory issues

### 📍 Location-Based Legal Advice
- Automatic location detection using GPS
- Manual location selection for all U.S. states
- Legal advice tailored to your specific state and city laws

### 🎤 Voice Features
- Voice-to-text transcription for hands-free interaction
- Text-to-speech for AI responses
- Support for multiple voice input methods

## Session Memory Features

### Backend Session Management
- **Session Creation**: Automatic session creation for new conversations
- **Conversation History**: Stores all messages with timestamps
- **Context Window**: Uses last 6 messages for context in legal reasoning
- **Session APIs**: 
  - `POST /api/sessions/new` - Create new session
  - `GET /api/sessions/{id}` - Get session details
  - `DELETE /api/sessions/{id}` - Delete session
  - `GET /api/sessions` - List all sessions
  - `GET /api/health` - Health check with session count

### Frontend Session Interface
- **Session Panel**: Toggle to show/hide session management
- **Session List**: View all active sessions with creation dates and message counts
- **Session Switching**: Click any session to load its conversation history
- **Session Deletion**: Remove sessions with one click
- **Active Session Indicator**: Shows current session ID and message count

## Setup

### Backend Setup
```bash
cd CalHacks2025/backend
pip install -r requirements.txt
python main.py
```

### Frontend Setup
```bash
cd CalHacks2025/frontend
npm install
npm run dev
```

### Environment Variables
Create a `.env` file in the backend directory:
```env
GROQ_API_KEY=your_groq_api_key_here
GOOGLE_API_KEY=your_google_api_key_here
```

## Usage

1. **Enable Location**: Allow location access or manually select your state
2. **Start Chatting**: Ask legal questions and get AI-powered responses
3. **Manage Sessions**: 
   - Click "Show Sessions" to view all conversations
   - Click "New Session" to start fresh
   - Click any session to switch to it
   - Click "×" to delete a session
4. **Voice Input**: Use the microphone button for hands-free interaction

## Technical Details

### Session Storage
- In-memory storage for fast access
- Automatic cleanup after 24 hours of inactivity
- Maximum 100 sessions to prevent memory overflow
- Session data includes:
  - Unique session ID
  - Creation timestamp
  - Last activity timestamp
  - Complete conversation history

### Context Management
- Last 6 messages used for context in legal reasoning
- Maintains conversation flow and continuity
- Preserves legal context across multiple questions

### Memory Optimization
- Automatic session cleanup
- Configurable session limits
- Efficient message storage
- Health monitoring endpoints

## API Endpoints

### Legal Assistance
- `POST /legal-response` - Main legal advice endpoint with session support
- `POST /transcribe` - Audio transcription
- `POST /speak` - Text-to-speech conversion
- `POST /location` - Location detection

### Session Management
- `POST /api/sessions/new` - Create new session
- `GET /api/sessions/{session_id}` - Get session details
- `DELETE /api/sessions/{session_id}` - Delete session
- `GET /api/sessions` - List all sessions
- `GET /api/health` - Health check

## Legal Disclaimer

This AI assistant provides general legal information and should not be considered as legal advice. For specific legal matters, please consult with a qualified attorney.


## Detected evidence (automated analysis)

Indexed codebase: 72 recognized source files, 212 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — 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

## Codebase structure (from repository index)

### Files (81 of 81)

```
.gitignore
ABOUT.md
backend/main.py
backend/requirements.txt
frontend/bun.lockb
frontend/components.json
frontend/eslint.config.js
frontend/index.html
frontend/package.json
frontend/postcss.config.js
frontend/public/robots.txt
frontend/README.md
frontend/src/App.css
frontend/src/App.tsx
frontend/src/components/ChatMessage.tsx
frontend/src/components/LocationDetector.tsx
frontend/src/components/ui/accordion.tsx
frontend/src/components/ui/alert-dialog.tsx
frontend/src/components/ui/alert.tsx
frontend/src/components/ui/aspect-ratio.tsx
frontend/src/components/ui/avatar.tsx
frontend/src/components/ui/badge.tsx
frontend/src/components/ui/breadcrumb.tsx
frontend/src/components/ui/button.tsx
frontend/src/components/ui/calendar.tsx
frontend/src/components/ui/card.tsx
frontend/src/components/ui/carousel.tsx
frontend/src/components/ui/chart.tsx
frontend/src/components/ui/checkbox.tsx
frontend/src/components/ui/collapsible.tsx
frontend/src/components/ui/command.tsx
frontend/src/components/ui/context-menu.tsx
frontend/src/components/ui/dialog.tsx
frontend/src/components/ui/drawer.tsx
frontend/src/components/ui/dropdown-menu.tsx
frontend/src/components/ui/form.tsx
frontend/src/components/ui/hover-card.tsx
frontend/src/components/ui/input-otp.tsx
frontend/src/components/ui/input.tsx
frontend/src/components/ui/label.tsx
frontend/src/components/ui/menubar.tsx
frontend/src/components/ui/navigation-menu.tsx
frontend/src/components/ui/pagination.tsx
frontend/src/components/ui/popover.tsx
frontend/src/components/ui/progress.tsx
frontend/src/components/ui/radio-group.tsx
frontend/src/components/ui/resizable.tsx
frontend/src/components/ui/scroll-area.tsx
frontend/src/components/ui/select.tsx
frontend/src/components/ui/separator.tsx
frontend/src/components/ui/sheet.tsx
frontend/src/components/ui/sidebar.tsx
frontend/src/components/ui/skeleton.tsx
frontend/src/components/ui/slider.tsx
frontend/src/components/ui/sonner.tsx
frontend/src/components/ui/switch.tsx
frontend/src/components/ui/table.tsx
frontend/src/components/ui/tabs.tsx
frontend/src/components/ui/textarea.tsx
frontend/src/components/ui/toast.tsx
frontend/src/components/ui/toaster.tsx
frontend/src/components/ui/toggle-group.tsx
frontend/src/components/ui/toggle.tsx
frontend/src/components/ui/tooltip.tsx
frontend/src/components/ui/use-toast.ts
frontend/src/components/VoiceInput.tsx
frontend/src/hooks/use-mobile.tsx
frontend/src/hooks/use-speech.tsx
frontend/src/hooks/use-toast.ts
frontend/src/index.css
frontend/src/lib/utils.ts
frontend/src/main.tsx
frontend/src/pages/Index.tsx
frontend/src/pages/NotFound.tsx
frontend/src/vite-env.d.ts
frontend/tailwind.config.ts
frontend/tsconfig.app.json
frontend/tsconfig.json
frontend/tsconfig.node.json
frontend/vite.config.ts
README.md
```

### Dependencies

- backend/requirements.txt: annotated-types@==0.7.0, anyio@==4.9.0, beautifulsoup4@==4.13.4, cachetools@==5.5.2, certifi@==2025.6.15, charset-normalizer@==3.4.2, click@==8.2.1, colorama@==0.4.6, distro@==1.9.0, exceptiongroup@==1.3.0, fastapi@==0.104.1, geographiclib@==2.0, geopy@==2.4.0, google@==3.0.0, google-api-core@==2.25.1, google-auth@==2.40.3, google-cloud-texttospeech@==2.27.0, googleapis-common-protos@==1.70.0, groq@==0.4.2, grpcio@==1.73.0, grpcio-status@==1.73.0, h11@==0.16.0, httpcore@==1.0.9, httpx@==0.25.2, httpx-sse@==0.4.0, idna@==3.10, letta-client@==0.1.167, proto-plus@==1.26.1, protobuf@==6.31.1, pyasn1@==0.6.1, pyasn1_modules@==0.4.2, pydantic@==2.5.0, pydantic_core@==2.33.2, python-dotenv@==1.0.0, python-multipart@==0.0.20, requests@==2.32.4, rsa@==4.9.1, sniffio@==1.3.1, soupsieve@==2.7, starlette@==0.46.2, typing_extensions@==4.14.0, typing-inspection@==0.4.1, urllib3@==2.5.0, uvicorn@==0.24.0
- frontend/package.json: @eslint/js@^9.9.0, @hookform/resolvers@^3.9.0, @radix-ui/react-accordion@^1.2.0, @radix-ui/react-alert-dialog@^1.1.1, @radix-ui/react-aspect-ratio@^1.1.0, @radix-ui/react-avatar@^1.1.0, @radix-ui/react-checkbox@^1.1.1, @radix-ui/react-collapsible@^1.1.0, @radix-ui/react-context-menu@^2.2.1, @radix-ui/react-dialog@^1.1.2, @radix-ui/react-dropdown-menu@^2.1.1, @radix-ui/react-hover-card@^1.1.1, @radix-ui/react-label@^2.1.0, @radix-ui/react-menubar@^1.1.1, @radix-ui/react-navigation-menu@^1.2.0, @radix-ui/react-popover@^1.1.1, @radix-ui/react-progress@^1.1.0, @radix-ui/react-radio-group@^1.2.0, @radix-ui/react-scroll-area@^1.1.0, @radix-ui/react-select@^2.1.1, @radix-ui/react-separator@^1.1.0, @radix-ui/react-slider@^1.2.0, @radix-ui/react-slot@^1.1.0, @radix-ui/react-switch@^1.1.0, @radix-ui/react-tabs@^1.1.0, @radix-ui/react-toast@^1.2.1, @radix-ui/react-toggle@^1.1.0, @radix-ui/react-toggle-group@^1.1.0, @radix-ui/react-tooltip@^1.1.4, @tailwindcss/typography@^0.5.15, @tanstack/react-query@^5.56.2, @types/node@^22.5.5, @types/react@^18.3.3, @types/react-dom@^18.3.0, @vitejs/plugin-react-swc@^3.5.0, autoprefixer@^10.4.20, class-variance-authority@^0.7.1, clsx@^2.1.1, cmdk@^1.0.0, date-fns@^3.6.0, embla-carousel-react@^8.3.0, eslint@^9.9.0, eslint-plugin-react-hooks@^5.1.0-rc.0, eslint-plugin-react-refresh@^0.4.9, globals@^15.9.0, input-otp@^1.2.4, lovable-tagger@^1.1.7, lucide-react@^0.462.0, next-themes@^0.3.0, postcss@^8.4.47, react@^18.3.1, react-day-picker@^8.10.1, react-dom@^18.3.1, react-hook-form@^7.53.0, react-markdown@^10.1.0, react-resizable-panels@^2.1.3, react-router-dom@^6.26.2, recharts@^2.12.7, sonner@^1.5.0, tailwind-merge@^2.5.2, tailwindcss@^3.4.11, tailwindcss-animate@^1.0.7, typescript@^5.5.3, typescript-eslint@^8.0.1, vaul@^0.9.3, vite@^5.4.1, zod@^3.23.8

### Recent commits (newest first)

- Merge pull request #1 from briancontreras/session
- Session Method added to record conversatoins
- Added logo and Tab Name
- Update Index.tsx
- updated speech to work with google cloud api
- added summarization of legal response with Groq
- Merge branch 'main' of https://github.com/briancontreras/CalHacks2025
- file change and stuff
- Implemented automatic Location Detection
- lil push, we do a lil push
- update main.py client variable
- Create ABOUT.md
- Merge branch 'main' of https://github.com/briancontreras/CalHacks2025
- Clean up backend code: remove test code, fix legal_reasoning endpoint, and update requirements
- Update requirements.txt
- Minor Push
- Update README.md
- Merge branch 'main' of https://github.com/briancontreras/CalHacks2025
- fixed Location problem
- Merge branch 'main' of https://github.com/briancontreras/CalHacks2025

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

### ABOUT.md

```markdown
# Juno

**Empowering immigrants and communities with knowledge, voice, and protection.**

## Overview

*Juno* is a tool designed to help individuals, especially immigrants, understand and assert their rights when interacting with law enforcement or local authorities. The system allows users to:

- Access legal rights based on state,and federal laws
- Get guidance on how to navigate future interactions

## Why We Built This

Many community members—especially immigrants—feel vulnerable during encounters with authorities due to language gaps, legal uncertainty, or fear. *Juno* empowers users with knowledge, emotional support, and a practical toolset to protect their rights.

## Key Features

- **Location-based rights lookup** for personalized legal insight
- **Conversational AI** that helps users feel prepared and informed

## Privacy and Security

Security is a core principle of *Juno*. We prioritize:

- Transparent data use policies
- Local or consent-based data storage

## Status

This project is being developed as part of the UC Berkeley AI Hackathon—rapid prototyping in progress!

---

*Built with care by a team passionate about justice, tech, and community resilience.*

```

### backend/requirements.txt

```
annotated-types==0.7.0
anyio==4.9.0
beautifulsoup4==4.13.4
cachetools==5.5.2
certifi==2025.6.15
charset-normalizer==3.4.2
click==8.2.1
colorama==0.4.6
distro==1.9.0
exceptiongroup==1.3.0
fastapi==0.104.1
geographiclib==2.0
geopy==2.4.0
google==3.0.0
google-api-core==2.25.1
google-auth==2.40.3
google-cloud-texttospeech==2.27.0
googleapis-common-protos==1.70.0
groq==0.4.2
grpcio==1.73.0
grpcio-status==1.73.0
h11==0.16.0
httpcore==1.0.9
httpx==0.25.2
httpx-sse==0.4.0
idna==3.10
letta-client==0.1.167
proto-plus==1.26.1
protobuf==6.31.1
pyasn1==0.6.1
pyasn1_modules==0.4.2
pydantic==2.5.0
pydantic_core==2.33.2
python-dotenv==1.0.0
python-multipart==0.0.20
requests==2.32.4
rsa==4.9.1
sniffio==1.3.1
soupsieve==2.7
starlette==0.46.2
typing-inspection==0.4.1
typing_extensions==4.14.0
urllib3==2.5.0
uvicorn==0.24.0

```

### frontend/package.json

```
{
  "name": "vite_react_shadcn_ts",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "build:dev": "vite build --mode development",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "@hookform/resolvers": "^3.9.0",
    "@radix-ui/react-accordion": "^1.2.0",
    "@radix-ui/react-alert-dialog": "^1.1.1",
    "@radix-ui/react-aspect-ratio": "^1.1.0",
    "@radix-ui/react-avatar": "^1.1.0",
    "@radix-ui/react-checkbox": "^1.1.1",
    "@radix-ui/react-collapsible": "^1.1.0",
    "@radix-ui/react-context-menu": "^2.2.1",
    "@radix-ui/react-dialog": "^1.1.2",
    "@radix-ui/react-dropdown-menu": "^2.1.1",
    "@radix-ui/react-hover-card": "^1.1.1",
    "@radix-ui/react-label": "^2.1.0",
    "@radix-ui/react-menubar": "^1.1.1",
    "@radix-ui/react-navigation-menu": "^1.2.0",
    "@radix-ui/react-popover": "^1.1.1",
    "@radix-ui/react-progress": "^1.1.0",
    "@radix-ui/react-radio-group": "^1.2.0",
    "@radix-ui/react-scroll-area": "^1.1.0",
    "@radix-ui/react-select": "^2.1.1",
    "@radix-ui/react-separator": "^1.1.0",
    "@radix-ui/react-slider": "^1.2.0",
    "@radix-ui/react-slot": "^1.1.0",
    "@radix-ui/react-switch": "^1.1.0",
    "@radix-ui/react-tabs": "^1.1.0",
    "@radix-ui/react-toast": "^1.2.1",
    "@radix-ui/react-toggle": "^1.1.0",
    "@radix-ui/react-toggle-group": "^1.1.0",
    "@radix-ui/react-tooltip": "^1.1.4",
    "@tanstack/react-query": "^5.56.2",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "cmdk": "^1.0.0",
    "date-fns": "^3.6.0",
    "embla-carousel-react": "^8.3.0",
    "input-otp": "^1.2.4",
    "lucide-react": "^0.462.0",
    "next-themes": "^0.3.0",
    "react": "^18.3.1",
    "react-day-picker": "^8.10.1",
    "react-dom": "^18.3.1",
    "react-hook-form": "^7.53.0",
    "react-markdown": "^10.1.0",
    "react-resizable-panels": "^2.1.3",
    "react-router-dom": "^6.26.2",
    "recharts": "^2.12.7",
    "sonner": "^1.5.0",
    "tailwind-merge": "^2.5.2",
    "tailwindcss-animate": "^1.0.7",
    "vaul": "^0.9.3",
    "zod": "^3.23.8"
  },
  "devDependencies": {
    "@eslint/js": "^9.9.0",
    "@tailwindcss/typography": "^0.5.15",
    "@types/node": "^22.5.5",
    "@types/react": "^18.3.3",
    "@types/react-dom": "^18.3.0",
    "@vitejs/plugin-react-swc": "^3.5.0",
    "autoprefixer": "^10.4.20",
    "eslint": "^9.9.0",
    "eslint-plugin-react-hooks": "^5.1.0-rc.0",
    "eslint-plugin-react-refresh": "^0.4.9",
    "globals": "^15.9.0",
    "lovable-tagger": "^1.1.7",
    "postcss": "^8.4.47",
    "tailwindcss": "^3.4.11",
    "typescript": "^5.5.3",
    "typescript-eslint": "^8.0.1",
    "vite": "^5.4.1"
  }
}

```

### backend/main.py

```python
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from geopy.geocoders import Nominatim
from geopy.location import Location
from typing import cast, Optional, List, Dict
import uvicorn
import asyncio
from typing import Optional
from groq import Groq
from dotenv import load_dotenv
import os
from fastapi import UploadFile, File, HTTPException
from fastapi.responses import Response
import httpx
import base64
import uuid
from datetime import datetime, timedelta

##API KEYS
load_dotenv()

google_api_key = os.getenv("GOOGLE_API_KEY")
api_key = os.getenv("api_key")
client = Groq(api_key=api_key)

app = FastAPI()

geolocator = Nominatim(user_agent="calhacks2025-app")

# Session memory storage
sessions: Dict[str, Dict] = {}
MAX_SESSIONS = 100
SESSION_TIMEOUT = timedelta(hours=24)

class LocationInput(BaseModel):
    lat: float
    lon: float

class ChatMessage(BaseModel):
    role: str
    content: str
    timestamp: Optional[datetime] = None

class LegalRequest(BaseModel):
    question: str
    location: dict
    session_id: Optional[str] = None  # Add session support

class SpeechRequest(BaseModel):
    text: str

class SessionInfo(BaseModel):
    session_id: str
    created_at: datetime
    message_count: int
    last_activity: datetime

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

def cleanup_expired_sessions():
    """Remove expired sessions"""
    current_time = datetime.now()
    expired_sessions = [
        session_id for session_id, session_data in sessions.items()
        if current_time - session_data["last_activity"] > SESSION_TIMEOUT
    ]
    for session_id in expired_sessions:
        del sessions[session_id]

def create_new_session() -> str:
    """Create a new session and return session ID"""
    cleanup_expired_sessions()
    
    if len(sessions) >= MAX_SESSIONS:
        oldest_session = min(sessions.keys(), 
                           key=lambda x: sessions[x]["created_at"])
        del sessions[oldest_session]
    
    session_id = str(uuid.uuid4())
    sessions[session_id] = {
        "messages": [],
        "created_at": datetime.now(),
        "last_activity": datetime.now()
    }
    return session_id

@app.post("/location") 
async def get_location(data: LocationInput):
    raw_location = await asyncio.to_thread(
        geolocator.reverse, (data.lat, data.lon), exactly_one=True
    )
    location = cast(Optional[Location], raw_location)

    if location and "address" in location.raw:
        address = location.raw["address"]
        city = address.get("city") or address.get("town") or address.get("village")
        state = address.get("state")

        print(f"Detected location - City: {city}, State: {state}")

        return {
            "city": city,
            "state": state
        }
    return {"error": "Unable to determine city/state"}

@app.post("/transcribe")
async def transcribe_audio(request: Request, file: UploadFile = File(...)):
    audio_bytes = await file.read()

    headers = {
        "Authorization": f"Bearer {api_key}"
    }

    files = {
        "file": (file.filename, audio_bytes, file.content_type),
        "model": (None, "whisper-large-v3-turbo")
    }

    try:
        async with httpx.AsyncClient() as client:
            response = await client.post(
                "https://api.groq.com/openai/v1/audio/transcriptions",
                headers=headers,
                files=files
            )
        response.raise_for_status()
    except httpx.HTTPStatusError as e:
        error_detail = e.response.text
        print(f"Groq API returned error: {error_detail}")
        raise HTTPException(status_code=500, detail=f"Groq API error: {error_detail}")
    except Exception as e:
        print(f"Unexpected error: {str(e)}")
        raise HTTPException(status_code=500, detail="Unexpected error during transcription")

    data = response.json()
    return {"transcription": data.get("text")}

@app.post("/legal_reasoning")
def legal_reasoning(user_input, location, city):
    client = Groq(api_key=api_key)
    reasoning_prompt = f"""
    You are a legal reasoning AI.
    Given a user's situation, determine:
    1. What is happening?
    2. What rights are relevant?
    3. What should they say or not say?
    Only use logic based on U.S. law, in the state of {location}, and city of {city}. Be clear and concise.

    User text: "{user_input}"
    Respond in JSON format:
    {{
    "situation_summary": "...",
    "legal_context": "...",
    "advice": "...",
    "confidence": 0-1
    }}
    """

    completion = client.chat.completions.create(
        model="deepseek-r1-distill-llama-70b",
        messages=[
            {
                "role": "user",
                "content": reasoning_prompt
            }
        ],
        temperature=0.3,
        max_completion_tokens=512,
        top_p=0.9,
        stream=True,
        reasoning_format="raw"
    )

@app.post("/legal-response")
async def legal_response(request: LegalRequest):
    # Handle session management
    if not request.session_id or request.session_id not in sessions:
        request.session_id = create_new_session()
    
    session = sessions[request.session_id]
    session["last_activity"] = datetime.now()
    
    # Add user message to session history
    user_message = ChatMessage(
        role="user",
        content=request.question,
        timestamp=datetime.now()
    )
    session["messages"].append(user_message)
    
    # Build context from conversation history
    conversation_context = ""
    if len(session["messages"]) > 1:
        recent_messages = session["messages"][-6:]  # Last 6 messages for context
        conversation_context = "\n\nPrevious conversation:\n"
        for msg in recent_messages[:-1]:  # Exclude current message
            conversation_context += f"{msg.role}: {msg.content}\n"
    
    # Ste
[truncated — 7138 more characters]
```

### frontend/src/main.tsx

```typescript
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import './index.css'

createRoot(document.getElementById("root")!).render(<App />);

```

### frontend/src/App.tsx

```typescript
import { Toaster } from "@/components/ui/toaster";
import { Toaster as Sonner } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Index from "./pages/Index";
import NotFound from "./pages/NotFound";

const queryClient = new QueryClient();

const App = () => (
  <QueryClientProvider client={queryClient}>
    <TooltipProvider>
      <Toaster />
      <Sonner />
      <BrowserRouter>
        <Routes>
          <Route path="/" element={<Index />} />
          {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
          <Route path="*" element={<NotFound />} />
        </Routes>
      </BrowserRouter>
    </TooltipProvider>
  </QueryClientProvider>
);

export default App;

```

### frontend/src/pages/Index.tsx

```typescript
import React, { useState, useRef, useEffect } from 'react';
import { Mic, MicOff, Send, MapPin, Volume2, VolumeX } from 'lucide-react';
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { useToast } from "@/hooks/use-toast";
import LocationDetector from "@/components/LocationDetector";
import ChatMessage from "@/components/ChatMessage";
import VoiceInput from "@/components/VoiceInput";
import { useSpeech } from '@/hooks/use-speech';
import { Badge } from '../components/ui/badge';
import { Separator } from '../components/ui/separator';

interface Message {
  id: string;
  text: string;
  isUser: boolean;
  timestamp: Date;
}

interface ChatMessageType {
  role: 'user' | 'assistant';
  content: string;
  timestamp?: string;
}

interface SessionInfo {
  session_id: string;
  created_at: string;
  last_activity: string;
  message_count: number;
  conversation_history: ChatMessageType[];
}

const Index = () => {
  const [messages, setMessages] = useState<ChatMessageType[]>([]);
  const [input, setInput] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [isRecording, setIsRecording] = useState(false);
  const [isAudioEnabled, setIsAudioEnabled] = useState(true);
  const [sessionId, setSessionId] = useState<string | null>(null);
  const [sessions, setSessions] = useState<SessionInfo[]>([]);
  const [showSessions, setShowSessions] = useState(false);
  const [location, setLocation] = useState<{city: string, state: string}>({ city: "", state: "" });
  const [hasInitialized, setHasInitialized] = useState(false);
  const [hasShownWelcome, setHasShownWelcome] = useState(false);
  const { toast } = useToast();
  const messagesEndRef = useRef<HTMLDivElement>(null);
  const { speakWithGoogle } = useSpeech();

  const API_BASE = 'http://localhost:8000';

  // Auto-detect location on startup
  useEffect(() => {
    if (!hasInitialized) {
      autoDetectLocation();
      setHasInitialized(true);
    }
  }, [hasInitialized]);

  // Load sessions on component mount
  useEffect(() => {
    loadSessions();
  }, []);

  // Show welcome message if location is set and no messages yet
  useEffect(() => {
    if (location.state && messages.length === 0 && !hasShownWelcome) {
      sendInitialWelcomeMessage(location);
      setHasShownWelcome(true);
    }
  }, [location.state, messages.length, hasShownWelcome]);

  const autoDetectLocation = async () => {
    try {
      if ('geolocation' in navigator) {
        const position = await new Promise<GeolocationPosition>((resolve, reject) => {
          navigator.geolocation.getCurrentPosition(resolve, reject, {
            timeout: 10000,
            enableHighAccuracy: true
          });
        });

        const { latitude, longitude } = position.coords;

        const response = await fetch(`${API_BASE}/location`, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ lat: latitude, lon: longitude }),
        });

        if (response.ok) {
          const data = await response.json();
          if (!data.error) {
            const detectedLocation = {
              city: data.city || "Unknown City",
              state: data.state || "Unknown State"
            };
            setLocation(detectedLocation);
          }
        }
      }
    } catch (error) {
      console.log('Auto-location detection failed, user can set manually');
    }
  };

  const sendInitialWelcomeMessage = (detectedLocation: {city: string, state: string}) => {
    const welcomeMessage = `Hello! I'm JUNO AI, your legal assistant. I can help you understand your legal rights and provide guidance based on ${detectedLocation.state} law. 

What legal question can I help you with today? You can ask me about:
• Your rights in various situations
• Legal procedures and processes
• Understanding laws and regulations
• What to do in legal emergencies

Just type or speak your question, and I'll provide clear, helpful guidance.`;

    const initialMessage: ChatMessageType = {
      role: 'assistant',
      content: welcomeMessage,
      timestamp: new Date().toISOString(),
    };

    setMessages([initialMessage]);
  };

  const loadSessions = async () => {
    try {
      const response = await fetch(`${API_BASE}/api/sessions`);
      if (response.ok) {
        const sessionsData = await response.json();
        setSessions(sessionsData);
      }
    } catch (error) {
      console.error('Failed to load sessions:', error);
    }
  };

  const createNewSession = async () => {
    try {
      const response = await fetch(`${API_BASE}/api/sessions/new`, {
        method: 'POST',
      });
      if (response.ok) {
        const { session_id } = await response.json();
        setSessionId(session_id);
        setMessages([]);
        setHasShownWelcome(false);
        
        // Send welcome message for new session
        if (location.state) {
          sendInitialWelcomeMessage(location);
          setHasShownWelcome(true);
        }
        
        toast({
          title: "New session created",
          description: "You can now start a new conversation",
        });
        loadSessions();
      }
    } catch (error) {
      toast({
        title: "Error",
        description: "Failed to create new session",
        variant: "destructive",
      });
    }
  };

  const loadSession = async (sessionId: string) => {
    try {
      const response = await fetch(`${API_BASE}/api/sessions/${sessionId}`);
      if (response.ok) {
        const sessionData = await response.json();
        setSessionId(sessionId);
        setMessages(sessionData.conversation_history || []);
        setShowSessions(false);
        setHasShownWelcome(true); // Prevent welcome message for loaded sessions
        toast({
          title: "Session loaded",
          description: `Loaded conversation with ${se
[truncated — 13604 more characters]
```

### frontend/postcss.config.js

```javascript
export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

```

### frontend/vite.config.ts

```typescript
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import path from "path";
import { componentTagger } from "lovable-tagger";

// https://vitejs.dev/config/
export default defineConfig(({ mode }) => ({
  server: {
    host: "::",
    port: 8080,
  },
  plugins: [
    react(),
    mode === 'development' &&
    componentTagger(),
  ].filter(Boolean),
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
}));

```

### frontend/eslint.config.js

```javascript
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";

export default tseslint.config(
  { ignores: ["dist"] },
  {
    extends: [js.configs.recommended, ...tseslint.configs.recommended],
    files: ["**/*.{ts,tsx}"],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
    },
    plugins: {
      "react-hooks": reactHooks,
      "react-refresh": reactRefresh,
    },
    rules: {
      ...reactHooks.configs.recommended.rules,
      "react-refresh/only-export-components": [
        "warn",
        { allowConstantExport: true },
      ],
      "@typescript-eslint/no-unused-vars": "off",
    },
  }
);

```

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