# Project export: Glossa

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: Language learning that automatically adapts to your everyday interests. Instead of random sentences, connect and learn about topics you actually care about.
- Devpost: https://devpost.com/software/glossa-m0uq2z
- GitHub: https://github.com/aashvibusa/Berkeley-AI-Hackathon
- Team: 2 GitHub contributor(s) — KKaradi (4 commits), aashvibusa (3 commits)

## Devpost submission (written by the team)

### Inspiration

We were inspired by the ability of anyone to learn language and removing the barriers to do so. By integrating content a user actually knows, it removes the process, in other language apps, of training on information that is not often used in everyday conversation. This is powerful because interacting with language learning content repeatedly is a large contributor to final proficiency. Glossa Glossa is an interactive language learning assistant meant to help integrate language practice into your daily routine. The chrome extension designs daily quizzes using the videos you watch and articles you read as a basis for your study material. This way, you can focus on learning about topics tailored to you and your interests. How We Built It Letta: To structure concise topics and questions. Vapi: For all voice-powered interactivity. Fast API: As an integration layer between the chrome extension, frontend, database, Letta, and Vapi. SQLite: To store collected data and meta-data. Groq: To quickly translate text faster than other AI tools. Challenges We were stuck figuring out if we wanted to use Plotly Dash or Fast API for handling the application. Plotly Dash would work as a single framework, handling both backend logic and frontend dashboards. However, we chose to use Fast API because a stand alone frontend solution would allow easier Vapi integration. Storage of collected user chunks and data had us choosing between an SQLite database or solely relying on the memory blocks of Letta. Currently, we are relying on memory blocks, but to make the responses more robust and predictable, a dedicated chunk solution would be best. What We Are Proud Of We are proud of creating an application that: Adapts to the user’s interests Provides curated AI-powered study material Integrates multiple angles of learning into the user’s purview What We Learned Glossa helped us learn how to better implement AI tools and agents into our projects. Specifically, we gained knowledge in: Processing and storing data using AI agents. Controlling application functionality using voice. Creating chrome extensions that can communicate with our backend

### What's next

While Glossa provides a way to learn Spanish, we have yet to expand the application to support other languages. We hope to integrate all the common languages, which may pose difficulties for ones such as Japanese or Mandarin with special characters. We also hope to explore ways to help with grammar as our main focus right now is vocabulary. The main goal is to adapt the learning content and style to a user’s weak points and/or preferences. Does Glossa serve as an addition to other learning platforms? Or is Glossa a complete course with no need for additional learning platforms.

## README (from the GitHub repository)

# Berkeley-AI-Hackathon

## Detected evidence (automated analysis)

Indexed codebase: 17 recognized source files, 87 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
- PyTorch (technology) — detected in the code
- React (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (23 of 23)

```
.gitignore
application/hi.txt
chrome-extention/background.js
chrome-extention/content.js
chrome-extention/manifest.json
chrome-extention/popup.css
chrome-extention/popup.html
chrome-extention/popup.js
chrome-extention/README.md
letta_agent/agent.yaml
letta_agent/tools/save.py
letta_agent/tools/speech_input.py
letta_agent/tools/speech_output.py
letta_agent/tools/translate.py
README.md
server/env_template.txt
server/load_env.py
server/main.py
server/README.md
server/requirements.txt
server/state_manager.py
server/store.json
server/test_websocket.py
```

### Dependencies

- server/requirements.txt: fastapi@==0.104.1, httpx@==0.25.2, numpy@==1.24.3, openai-whisper@==20231117, pydantic@==2.5.0, python-dotenv@==1.0.0, torch@==2.1.1, uvicorn[standard]@==0.24.0, websockets@==12.0

### Recent commits (newest first)

- bert
- m
- e
- fix
- updated letta tools
- intial file set-up
- Initial commit

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

### server/requirements.txt

```
fastapi==0.104.1
uvicorn[standard]==0.24.0
pydantic==2.5.0
httpx==0.25.2
python-dotenv==1.0.0
websockets==12.0
openai-whisper==20231117
torch==2.1.1
numpy==1.24.3 
```

### server/main.py

```python
from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import uvicorn
import os
import httpx
import whisper
import tempfile
import io
import wave
import numpy as np
from typing import Optional
from dotenv import load_dotenv
from state_manager import StateManager

# Load environment variables from .env file
load_dotenv()

app = FastAPI(title="Highlight Logger API", version="1.0.0")

# Add CORS middleware to allow requests from Chrome extensions
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # Allows all origins
    allow_credentials=True,
    allow_methods=["*"],  # Allows all methods
    allow_headers=["*"],  # Allows all headers
)

# Environment variables for APIs
LETTA_API_KEY = os.getenv("LETTA_API_KEY")
LETTA_AGENT_ID = os.getenv("LETTA_AGENT_ID")
LETTA_BASE_URL = "https://api.letta.ai/v1"
GROQ_API_KEY = os.getenv("GROQ_API_KEY")

# Initialize state manager
state_manager = StateManager()

# Initialize Whisper model
print("Loading Whisper model...")
whisper_model = whisper.load_model("base")
print("Whisper model loaded successfully!")

# WebSocket connection manager
class ConnectionManager:
    def __init__(self):
        self.active_connections: list[WebSocket] = []
        self.is_listening = False

    async def connect(self, websocket: WebSocket):
        await websocket.accept()
        self.active_connections.append(websocket)
        print(f"WebSocket connected. Total connections: {len(self.active_connections)}")

    def disconnect(self, websocket: WebSocket):
        self.active_connections.remove(websocket)
        print(f"WebSocket disconnected. Total connections: {len(self.active_connections)}")

    async def send_personal_message(self, message: str, websocket: WebSocket):
        await websocket.send_text(message)

    async def broadcast(self, message: str):
        for connection in self.active_connections:
            try:
                await connection.send_text(message)
            except:
                # Remove broken connections
                self.active_connections.remove(connection)

manager = ConnectionManager()

# Language code to full name mapping
LANGUAGE_MAP = {
    'en': 'English',
    'es': 'Spanish',
    'fr': 'French',
    'de': 'German',
    'it': 'Italian',
    'pt': 'Portuguese',
    'ja': 'Japanese',
    'zh': 'Chinese',
    'ko': 'Korean',
    'ru': 'Russian',
    'ar': 'Arabic',
    'hi': 'Hindi',
    'auto': 'auto'
}

def expand_language_code(language_code: str) -> str:
    """
    Expand a language code to its full name.
    If the code is not found, return the original code.
    """
    if not language_code:
        return "auto"
    
    # Convert to lowercase for case-insensitive matching
    code_lower = language_code.lower()
    
    # Check if it's already a full name (capitalized)
    if language_code[0].isupper():
        return language_code
    
    # Look up in the language map
    return LANGUAGE_MAP.get(code_lower, language_code)

class HighlightRequest(BaseModel):
    highlight: str
    user_id: Optional[str] = "default_user"

class TranslateRequest(BaseModel):
    text: str
    user_id: Optional[str] = None

class UserLanguageRequest(BaseModel):
    user_id: str
    source_language: Optional[str] = None
    target_language: Optional[str] = None

class LoginRequest(BaseModel):
    user_id: str
    password: str

class RegisterRequest(BaseModel):
    user_id: str
    password: str

@app.get("/")
async def root():
    return {"message": "Highlight Logger API is running!"}

@app.get("/store/stats")
async def get_store_stats():
    """Get statistics about the store."""
    return state_manager.get_store_stats()

@app.get("/users/{user_id}")
async def get_user_data(user_id: str):
    """Get user data including languages and highlighted words."""
    user_data = state_manager.get_user(user_id)
    return {
        "user_id": user_id,
        "data": user_data
    }

@app.post("/users/register")
async def register_user(request: RegisterRequest):
    """Register a new user with hashed password."""
    try:
        # Check if user already exists
        if request.user_id in state_manager.store["users"]:
            raise HTTPException(status_code=400, detail="User already exists")
        
        # Create new user with hashed password
        state_manager.store["users"][request.user_id] = {
            "source_language": "auto",
            "target_language": "Spanish",
            "highlighted_words": [],
            "password": request.password  # Store hashed password
        }
        
        # Save to store
        state_manager.save_store()
        
        # Return user data (without password)
        user_data = state_manager.get_user(request.user_id)
        user_data.pop("password", None)  # Remove password from response
        
        return {
            "status": "success",
            "message": "User registered successfully",
            "user": {
                "user_id": request.user_id,
                "data": user_data
            }
        }
    except Exception as e:
        print(f"Error registering user: {e}")
        raise HTTPException(status_code=500, detail="Registration failed")

@app.post("/users/login")
async def login_user(request: LoginRequest):
    """Login user with password verification."""
    try:
        # Check if user exists
        if request.user_id not in state_manager.store["users"]:
            raise HTTPException(status_code=401, detail="Invalid credentials")
        
        user_data = state_manager.store["users"][request.user_id]
        stored_password = user_data.get("password", "")
        
        # Verify password
        if stored_password != request.password:
            raise HTTPException(status_code=401, detail="Invalid credentials")
        
        # Return user data (without password)
        user_data_copy = user_data.copy()
        user_data_copy
[truncated — 9600 more characters]
```

### letta_agent/agent.yaml

```yaml
name: LangLearner
description: A language learning assistant that interacts with user speech, web content, and images to tutor users in real time.

tools:
  - id: speech_input
    description: Accepts user speech converted to text.
    input_type: string
    output_type: string

  - id: translate
    description: Translates or explains language phrases.
    input_type: string
    output_type: string

  - id: save
    description: Saves learned vocabulary or grammar gaps.
    input_type: string
    output_type: null

  - id: speech_output
    description: Converts text response into speech-ready output.
    input_type: string
    output_type: string

memory:
  schema:
    vocab_log:
      type: list
      items: string
    grammar_gaps:
      type: list
      items: string

plan:
  - tool: speech_input
  - tool: translate
  - tool: save
  - tool: speech_output

```

### server/test_websocket.py

```python
#!/usr/bin/env python3
"""
Test script for WebSocket audio endpoint
"""
import asyncio
import websockets
import wave
import numpy as np
import io

async def test_websocket():
    """Test the WebSocket audio endpoint"""
    uri = "ws://localhost:8000/ws/audio"
    
    try:
        async with websockets.connect(uri) as websocket:
            print("Connected to WebSocket")
            
            # Create a simple test audio signal (1 second of 440Hz sine wave)
            sample_rate = 16000
            duration = 1.0
            frequency = 440.0
            
            t = np.linspace(0, duration, int(sample_rate * duration), False)
            audio_signal = np.sin(2 * np.pi * frequency * t)
            
            # Convert to 16-bit PCM
            audio_data = (audio_signal * 32767).astype(np.int16)
            
            # Create a simple WAV file in memory
            with io.BytesIO() as wav_buffer:
                with wave.open(wav_buffer, 'wb') as wav_file:
                    wav_file.setnchannels(1)
                    wav_file.setsampwidth(2)
                    wav_file.setframerate(sample_rate)
                    wav_file.writeframes(audio_data.tobytes())
                
                # Send the audio data
                await websocket.send(wav_buffer.getvalue())
                print("Sent test audio data")
            
            # Wait for response
            try:
                response = await asyncio.wait_for(websocket.recv(), timeout=10.0)
                print(f"Received response: {response}")
            except asyncio.TimeoutError:
                print("No response received within 10 seconds")
                
    except Exception as e:
        print(f"Error: {e}")

if __name__ == "__main__":
    asyncio.run(test_websocket()) 
```

### server/load_env.py

```python
import os
from dotenv import load_dotenv

def load_environment_variables():
    """
    Load environment variables from a .env file.
    This function will load all variables from .env into the environment.
    """
    # Load environment variables from .env file
    load_dotenv()
    
    # Example: Access environment variables
    letta_api_key = os.getenv("LETTA_API_KEY")
    letta_agent_id = os.getenv("LETTA_AGENT_ID")
    
    # Print loaded variables (be careful with sensitive data in production)
    print("Environment variables loaded:")
    print(f"LETTA_API_KEY: {'***' if letta_api_key else 'Not set'}")
    print(f"LETTA_AGENT_ID: {letta_agent_id or 'Not set'}")
    
    return {
        "LETTA_API_KEY": letta_api_key,
        "LETTA_AGENT_ID": letta_agent_id
    }

def load_env_with_validation():
    """
    Load environment variables and validate that required ones are present.
    """
    # Load the .env file
    load_dotenv()
    
    # Define required environment variables
    required_vars = ["LETTA_API_KEY", "LETTA_AGENT_ID"]
    missing_vars = []
    
    # Check if all required variables are present
    for var in required_vars:
        if not os.getenv(var):
            missing_vars.append(var)
    
    if missing_vars:
        print(f"Error: Missing required environment variables: {missing_vars}")
        print("Please check your .env file and ensure all required variables are set.")
        return False
    
    print("All required environment variables are loaded successfully!")
    return True

def load_env_with_defaults():
    """
    Load environment variables with default values for optional ones.
    """
    # Load the .env file
    load_dotenv()
    
    # Load with defaults
    config = {
        "LETTA_API_KEY": os.getenv("LETTA_API_KEY"),
        "LETTA_AGENT_ID": os.getenv("LETTA_AGENT_ID"),
        "DEBUG": os.getenv("DEBUG", "False").lower() == "true",  # Default to False
        "PORT": int(os.getenv("PORT", "8000")),  # Default to 8000
        "HOST": os.getenv("HOST", "0.0.0.0")  # Default to 0.0.0.0
    }
    
    print("Configuration loaded:")
    for key, value in config.items():
        if key == "LETTA_API_KEY" and value:
            print(f"{key}: ***")
        else:
            print(f"{key}: {value}")
    
    return config

if __name__ == "__main__":
    print("=== Basic Environment Loading ===")
    load_environment_variables()
    
    print("\n=== Environment Validation ===")
    load_env_with_validation()
    
    print("\n=== Environment with Defaults ===")
    load_env_with_defaults() 
```

### server/state_manager.py

```python
import json
import os
from typing import Dict, List, Optional

class StateManager:
    def __init__(self, store_file: str = "store.json"):
        self.store_file = store_file
        self.store = self.load_store()
    
    def load_store(self) -> Dict:
        """Load the store from JSON file."""
        try:
            if os.path.exists(self.store_file):
                with open(self.store_file, 'r', encoding='utf-8') as f:
                    store = json.load(f)
                    print(f"Store loaded from {self.store_file}")
                    return store
            else:
                # Create initial store structure
                initial_store = {"users": {}}
                self.save_store(initial_store)
                print(f"Created new store file: {self.store_file}")
                return initial_store
        except Exception as e:
            print(f"Error loading store: {e}")
            # Return default structure if loading fails
            return {"users": {}}
    
    def save_store(self, store: Optional[Dict] = None) -> bool:
        """Save the store to JSON file."""
        try:
            store_to_save = store if store is not None else self.store
            with open(self.store_file, 'w', encoding='utf-8') as f:
                json.dump(store_to_save, f, indent=2, ensure_ascii=False)
            print(f"Store saved to {self.store_file}")
            return True
        except Exception as e:
            print(f"Error saving store: {e}")
            return False
    
    def get_user(self, user_id: str) -> Dict:
        """Get user data, create if doesn't exist."""
        if user_id not in self.store["users"]:
            self.store["users"][user_id] = {
                "source_language": "auto",
                "target_language": "Spanish",
                "highlighted_words": []
            }
            self.save_store()
        return self.store["users"][user_id]
    
    def update_user_languages(self, user_id: str, source_language: str = None, target_language: str = None) -> bool:
        """Update user's language preferences."""
        try:
            user = self.get_user(user_id)
            if source_language:
                user["source_language"] = source_language
            if target_language:
                user["target_language"] = target_language
            self.save_store()
            return True
        except Exception as e:
            print(f"Error updating user languages: {e}")
            return False
    
    def add_highlighted_word(self, user_id: str, word: str) -> bool:
        """Add a highlighted word to user's list."""
        try:
            user = self.get_user(user_id)
            if word not in user["highlighted_words"]:
                user["highlighted_words"].append(word)
                self.save_store()
                print(f"Added word '{word}' for user {user_id}")
            return True
        except Exception as e:
            print(f"Error adding highlighted word: {e}")
            return False
    
    def get_highlighted_words(self, user_id: str) -> List[str]:
        """Get user's highlighted words list."""
        user = self.get_user(user_id)
        return user["highlighted_words"]
    
    def remove_highlighted_word(self, user_id: str, word: str) -> bool:
        """Remove a highlighted word from user's list."""
        try:
            user = self.get_user(user_id)
            if word in user["highlighted_words"]:
                user["highlighted_words"].remove(word)
                self.save_store()
                print(f"Removed word '{word}' for user {user_id}")
            return True
        except Exception as e:
            print(f"Error removing highlighted word: {e}")
            return False
    
    def get_all_users(self) -> Dict:
        """Get all users data."""
        return self.store["users"]
    
    def delete_user(self, user_id: str) -> bool:
        """Delete a user and their data."""
        try:
            if user_id in self.store["users"]:
                del self.store["users"][user_id]
                self.save_store()
                print(f"Deleted user {user_id}")
            return True
        except Exception as e:
            print(f"Error deleting user: {e}")
            return False
    
    def get_store_stats(self) -> Dict:
        """Get statistics about the store."""
        total_users = len(self.store["users"])
        total_words = sum(len(user["highlighted_words"]) for user in self.store["users"].values())
        return {
            "total_users": total_users,
            "total_highlighted_words": total_words,
            "users": list(self.store["users"].keys())
        } 
```

### chrome-extention/background.js

```javascript
// // Background script for audio capture and WebSocket communication
// let audioStream = null;
// let mediaRecorder = null;
// let websocket = null;
// let isListening = false;

// // Handle messages from popup
// chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
//     if (request.action === 'toggleListening') {
//         if (request.enabled) {
//             startListening();
//         } else {
//             stopListening();
//         }
//         sendResponse({ success: true });
//     }
//     return true;
// });

// // Handle messages from content script
// chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
//     if (request.action === 'audioStreamReady') {
//         console.log('Audio stream ready from content script');
//         // The content script will handle the WebSocket connection
//     }
//     return true;
// });

// // Start audio listening
// async function startListening() {
//     try {
//         // Get the active tab
//         const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
        
//         if (!tab) {
//             console.error('No active tab found');
//             return;
//         }

//         console.log('Attempting to capture audio from tab:', tab.id);

//         // Capture tab audio using the callback-based API
//         const streamId = await new Promise((resolve, reject) => {
//             chrome.tabCapture.capture({
//                 audio: true,
//                 video: false
//             }, (streamId) => {
//                 if (chrome.runtime.lastError) {
//                     reject(new Error(chrome.runtime.lastError.message));
//                 } else {
//                     resolve(streamId);
//                 }
//             });
//         });

//         if (!streamId) {
//             throw new Error('Failed to get stream ID');
//         }

//         console.log('Got stream ID:', streamId);

//         // Get the stream from the stream ID
//         audioStream = await navigator.mediaDevices.getUserMedia({
//             audio: {
//                 mandatory: {
//                     chromeMediaSource: 'tab',
//                     chromeMediaSourceId: streamId
//                 }
//             }
//         });

//         if (!audioStream) {
//             throw new Error('Failed to get audio stream');
//         }

//         console.log('Audio stream obtained successfully');

//         // Create MediaRecorder
//         mediaRecorder = new MediaRecorder(audioStream, {
//             mimeType: 'audio/webm;codecs=opus'
//         });

//         // Connect to WebSocket
//         websocket = new WebSocket('ws://localhost:8000/ws/audio');
        
//         websocket.onopen = () => {
//             console.log('WebSocket connected');
//             isListening = true;
            
//             // Start recording
//             mediaRecorder.start(1000); // Send chunks every second
            
//             // Notify popup that listening has started
//             chrome.runtime.sendMessage({
//                 action: 'listeningStatusChanged',
//                 isListening: true
//             });
//         };

//         websocket.onerror = (error) => {
//             console.error('WebSocket error:', error);
//             stopListening();
//         };

//         websocket.onclose = () => {
//             console.log('WebSocket closed');
//             stopListening();
//         };

//         // Handle audio data
//         mediaRecorder.ondataavailable = (event) => {
//             if (websocket && websocket.readyState === WebSocket.OPEN) {
//                 // Convert blob to array buffer and send
//                 event.data.arrayBuffer().then(buffer => {
//                     websocket.send(buffer);
//                 });
//             }
//         };

//     } catch (error) {
//         console.error('Error starting audio listening:', error);
//         stopListening();
//     }
// }

// // Stop audio listening
// function stopListening() {
//     isListening = false;
    
//     // Stop media recorder
//     if (mediaRecorder && mediaRecorder.state !== 'inactive') {
//         mediaRecorder.stop();
//     }
    
//     // Close WebSocket
//     if (websocket) {
//         websocket.close();
//         websocket = null;
//     }
    
//     // Stop audio stream
//     if (audioStream) {
//         audioStream.getTracks().forEach(track => track.stop());
//         audioStream = null;
//     }
    
//     // Notify popup that listening has stopped
//     chrome.runtime.sendMessage({
//         action: 'listeningStatusChanged',
//         isListening: false
//     });
    
//     console.log('Audio listening stopped');
// }

// // Handle extension installation
// chrome.runtime.onInstalled.addListener(() => {
//     console.log('Glossa extension installed');
// }); 

chrome.action.onClicked.addListener((tab) => {
    chrome.tabCapture.capture(
      {
        audio: true,
        video: false
      },
      (stream) => {
        if (chrome.runtime.lastError) {
          console.error("Error capturing tab:", chrome.runtime.lastError.message);
          return;
        }
        console.log("Audio stream captured!", stream);
  
        // Example: connect to Web Audio API for visualization/processing
        const audioCtx = new AudioContext();
        const source = audioCtx.createMediaStreamSource(stream);
        const analyser = audioCtx.createAnalyser();
  
        source.connect(analyser);
        analyser.connect(audioCtx.destination);
  
        console.log("Audio connected to Web Audio API");
      }
    );
  });
  
```

### chrome-extention/popup.css

```css
/* Reset and base styles */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    background: linear-gradient(135deg, #ff6b35 0%, #f7931e 100%);
    color: #333;
    width: 450px;
    min-height: 500px;
    overflow: hidden;
}

#app {
    position: relative;
    width: 100%;
    height: 100vh;
}

/* Loading Screen */
.loading-screen {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: linear-gradient(135deg, #ff6b35 0%, #f7931e 100%);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 1000;
    animation: fadeIn 0.5s ease-out;
}

.app-title {
    text-align: center;
    color: white;
    animation: slideUp 0.8s ease-out 0.3s both;
}

.app-title h1 {
    font-size: 3rem;
    font-weight: 700;
    margin-bottom: 0.5rem;
    text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.app-title p {
    font-size: 1rem;
    opacity: 0.9;
    font-weight: 300;
}

/* Screen Management */
.screen {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: white;
    padding: 20px;
    overflow-y: auto;
    transition: transform 0.3s ease-out, opacity 0.3s ease-out;
}

.screen.hidden {
    transform: translateX(100%);
    opacity: 0;
    pointer-events: none;
}

.screen.active {
    transform: translateX(0);
    opacity: 1;
    pointer-events: all;
}

/* Header */
.header {
    text-align: center;
    margin-bottom: 30px;
    padding-top: 10px;
}

.header h2 {
    font-size: 1.8rem;
    font-weight: 600;
    color: #333;
    margin-bottom: 8px;
}

.header p {
    color: #666;
    font-size: 0.9rem;
}

/* Form Styles */
.form-container {
    max-width: 100%;
}

.form-group {
    margin-bottom: 20px;
}

.form-group label {
    display: block;
    margin-bottom: 8px;
    font-weight: 500;
    color: #333;
    font-size: 0.9rem;
}

.form-group input {
    width: 100%;
    padding: 12px 16px;
    border: 2px solid #e1e5e9;
    border-radius: 8px;
    font-size: 1rem;
    transition: border-color 0.3s ease, box-shadow 0.3s ease;
    background: white;
}

.form-group input:focus {
    outline: none;
    border-color: #ff6b35;
    box-shadow: 0 0 0 3px rgba(255, 107, 53, 0.1);
}

/* Button Styles */
.btn {
    width: 100%;
    padding: 12px 20px;
    border: none;
    border-radius: 8px;
    font-size: 1rem;
    font-weight: 500;
    cursor: pointer;
    transition: all 0.3s ease;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    margin-bottom: 12px;
}

.btn-primary {
    background: linear-gradient(135deg, #ff6b35 0%, #f7931e 100%);
    color: white;
    box-shadow: 0 4px 12px rgba(255, 107, 53, 0.3);
}

.btn-primary:hover {
    transform: translateY(-2px);
    box-shadow: 0 6px 16px rgba(255, 107, 53, 0.4);
}

.btn-secondary {
    background: white;
    color: #ff6b35;
    border: 2px solid #ff6b35;
}

.btn-secondary:hover {
    background: #ff6b35;
    color: white;
}

.btn-text {
    background: transparent;
    color: #666;
    padding: 8px 16px;
    margin-bottom: 0;
}

.btn-text:hover {
    color: #ff6b35;
    background: rgba(255, 107, 53, 0.1);
}

/* Divider */
.divider {
    text-align: center;
    margin: 20px 0;
    position: relative;
}

.divider::before {
    content: '';
    position: absolute;
    top: 50%;
    left: 0;
    right: 0;
    height: 1px;
    background: #e1e5e9;
}

.divider span {
    background: white;
    padding: 0 16px;
    color: #666;
    font-size: 0.9rem;
}

/* Stats Container */
.stats-container {
    margin-bottom: 30px;
}

.stat-card {
    background: linear-gradient(135deg, #fff5f0 0%, #fff 100%);
    border: 1px solid #ffe4d6;
    border-radius: 12px;
    padding: 20px;
    display: flex;
    align-items: center;
    gap: 16px;
    box-shadow: 0 2px 8px rgba(255, 107, 53, 0.1);
}

.stat-card i {
    font-size: 1.5rem;
    color: #ff6b35;
    width: 40px;
    height: 40px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(255, 107, 53, 0.1);
    border-radius: 8px;
}

.stat-info {
    flex: 1;
}

.stat-number {
    display: block;
    font-size: 1.5rem;
    font-weight: 700;
    color: #333;
}

.stat-label {
    font-size: 0.8rem;
    color: #666;
    font-weight: 400;
}

/* Listening Mode */
.listening-mode {
    margin-bottom: 25px;
    padding: 20px;
    background: #f8f9fa;
    border-radius: 12px;
    border: 1px solid #e1e5e9;
}

.listening-mode h3 {
    font-size: 1.1rem;
    font-weight: 600;
    color: #333;
    margin-bottom: 15px;
}

.toggle-container {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-bottom: 8px;
}

.toggle-switch {
    position: relative;
    display: inline-block;
    width: 50px;
    height: 24px;
    cursor: pointer;
}

.toggle-switch input {
    opacity: 0;
    width: 0;
    height: 0;
}

.toggle-slider {
    position: absolute;
    cursor: pointer;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: #ccc;
    transition: 0.3s;
    border-radius: 24px;
}

.toggle-slider:before {
    position: absolute;
    content: "";
    height: 18px;
    width: 18px;
    left: 3px;
    bottom: 3px;
    background-color: white;
    transition: 0.3s;
    border-radius: 50%;
}

.toggle-switch input:checked + .toggle-slider {
    background: linear-gradient(135deg, #ff6b35 0%, #f7931e 100%);
}

.toggle-switch input:checked + .toggle-slider:before {
    transform: translateX(26px);
}

.toggle-label {
    font-weight: 500;
    color: #333;
    font-size: 0.9rem;
}

.toggle-description {
    font-size: 0.8rem;
    color: #666;
    margin-bottom: 12px;
}

.listening-status {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 12px;
    background: rgba(255, 107, 53, 0.1);
    border-radius: 6px;
    color: #ff6b35;
    font-size: 0.8rem;
    font-weight: 500;
}

.listening-status i {
  
[truncated — 3087 more characters]
```

### chrome-extention/content.js

```javascript
// Function to check if user is logged in
async function getCurrentUser() {
    try {
        const result = await chrome.storage.local.get(['currentUser']);
        return result.currentUser;
    } catch (error) {
        console.log('Error getting current user:', error);
        return null;
    }
}

// Function to translate text using the server
async function translateText(text) {
    try {
        // Get current user from Chrome storage
        const currentUser = await getCurrentUser();
        console.log('Current user for translation:', currentUser ? currentUser.user_id : 'None');
        
        const requestBody = {
            text: text,
            user_id: currentUser ? currentUser.user_id : null
        };
        
        console.log('Translation request:', requestBody);
        
        const response = await fetch('http://localhost:8000/translate', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify(requestBody)
        });
        
        if (response.ok) {
            const result = await response.json();
            console.log('Translation successful:', result);
            console.log(`Translated from ${result.source_language} to ${result.target_language}`);
            return result.translated_text;
        } else {
            console.error('Translation failed:', response.status, response.statusText);
            return null;
        }
    } catch (error) {
        console.error('Error translating text:', error);
        return null;
    }
}

// Function to create and show popup animation
async function showHighlightPopup(selectedText) {
    // Remove any existing popups
    const existingPopup = document.querySelector('.highlight-popup');
    if (existingPopup) {
        existingPopup.remove();
    }
    
    // Get selection range to position the popup
    const selection = window.getSelection();
    if (!selection.rangeCount) return;
    
    const range = selection.getRangeAt(0);
    const rect = range.getBoundingClientRect();
    
    // Get current user for personalized feedback
    const currentUser = await getCurrentUser();
    
    // Create popup element
    const popup = document.createElement('div');
    popup.className = 'highlight-popup';
    
    // Create main text element (original text)
    const mainText = document.createElement('div');
    mainText.textContent = selectedText;
    mainText.style.cssText = `
        font-size: 14px;
        font-weight: 500;
        margin-bottom: 4px;
    `;
    
    // Create translated text element (initially shows loading)
    const translatedText = document.createElement('div');
    translatedText.textContent = 'Translating...';
    translatedText.style.cssText = `
        font-size: 12px;
        font-weight: 400;
        color: #666;
        font-style: italic;
        margin-bottom: 4px;
    `;
    
    // Create tiny instruction text
    const instructionText = document.createElement('div');
    if (currentUser) {
        instructionText.textContent = `press shift to save word (${currentUser.user_id})`;
    } else {
        instructionText.textContent = 'press shift to save word (guest)';
    }
    instructionText.style.cssText = `
        font-size: 10px;
        font-weight: 400;
        opacity: 0.8;
        line-height: 1.2;
    `;
    
    // Add text elements to popup
    popup.appendChild(mainText);
    popup.appendChild(translatedText);
    popup.appendChild(instructionText);
    
    // Style the popup
    popup.style.cssText = `
        position: fixed;
        top: ${rect.top - 70}px;
        left: ${rect.left}px;
        background: linear-gradient(135deg,rgb(237, 189, 99) 0%,rgb(255, 255, 255) 100%);
        color: #333;
        padding: 10px 14px;
        border-radius: 5px;
        box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
        z-index: 10000;
        opacity: 0;
        transform: translateY(10px);
        transition: all 0.3s ease-out;
        pointer-events: none;
        max-width: 300px;
        word-wrap: break-word;
        text-align: center;
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    `;
    
    // Add to page
    document.body.appendChild(popup);
    
    // Trigger fade-in animation
    setTimeout(() => {
        popup.style.opacity = '1';
        popup.style.transform = 'translateY(0)';
    }, 10);
    
    // Store the selected text and popup reference for shift key handling
    window.currentHighlightData = {
        text: selectedText,
        popup: popup
    };
    
    // Get translation
    const translatedResult = await translateText(selectedText);
    if (translatedResult) {
        translatedText.textContent = translatedResult;
        translatedText.style.color = '#2c5aa0';
        translatedText.style.fontStyle = 'normal';
    } else {
        translatedText.textContent = 'Translation failed';
        translatedText.style.color = '#e74c3c';
    }
}

// Function to close popup with animation
function closePopup(popup) {
    if (popup && popup.parentNode) {
        popup.style.opacity = '0';
        popup.style.transform = 'translateY(-10px)';
        setTimeout(() => {
            if (popup.parentNode) {
                popup.remove();
            }
        }, 300);
    }
    // Clear the stored data
    window.currentHighlightData = null;
}

// Function to send highlighted text to the server
async function sendHighlightToServer(highlightedText) {
    try {
        // Get current user from Chrome storage
        const currentUser = await getCurrentUser();
        console.log('Current user for highlight:', currentUser ? currentUser.user_id : 'Default');
        
        const requestBody = {
            highlight: highlightedText,
            user_id: currentUser ? currentUser.user_id : "chrome_extension_user"
        };
        
        console.log('Highlight request:', requestBody);
        
        const respon
[truncated — 3311 more characters]
```

### letta_agent/tools/speech_input.py

```python
# Accepts user speech converted to text.
```

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