# Project export: Echo Journal

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: CruzHacks 2025
- Tagline: EchoJournal is an AI journaling app that helps users understand their emotions. By analyzing journal entries with Gemini LLM, it offers insights and reflections, making journaling more meaningful.
- Devpost: https://devpost.com/software/echo-journal-ahfl9i
- GitHub: https://github.com/Gary0302/echo-BE
- Demo: https://github.com/dez03/echo-frontend
- Video: https://www.youtube.com/embed/lSSffwCQV0s?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Gary (8 commits)

## Devpost submission (written by the team)

### Inspiration

EchoJournal was created to help users build self-awareness, process their emotions, and recognize thought patterns over time, transforming journaling into a powerful feedback loop for emotional growth. Before this project, I had heard about the mental health benefits of journaling, but I often found it unfulfilling and repetitive. I wanted to use technology to enhance the experience, turning journaling into a tool for personal insight and reflection. What It Does EchoJournal makes journaling more interesting to those who otherwise wouldn’t do it. The site prompts users to write about their feelings, thoughts, and experiences. These entries are processed through Google’s Gemini LLM, which provides constructive insights that would typically be unavailable during traditional journaling. One key feature we built is “You Said, You Meant,” which helps identify deeper emotions, beliefs, or fears behind what the user wrote. Additional features include personalized reflections, mood trend visualization, and emotional theme detection. How We Built It Frontend: Next.js, Tailwindcss, Authentication: Firebase Authentication Database: MongoDB Atlas AI Processing: Python server (FAST API) running entries through Gemini’s LLM API to return insights and suggestions Challenges We Faced One of the biggest challenges was connecting the frontend to the backend API routes and managing user data across multiple services. Handling metadata and post request formats between Firebase, MongoDB, and FastAPI was particularly tricky, and combined with this being one of the most complex projects we’ve built, this led to several debugging sessions. What We Learned As a team, we grew significantly — learning to work with React, Next.js, and Tailwind CSS for the first time, while also exploring routing, protected routes, and API integration using FastAPI. We also gained valuable experience with cloud deployment and managing cross-service communication between frontend, backend, and database layers. What’s Next for EchoJournal EchoJournal has several potential directions for growth — both in terms of user features and monetization. Some ideas we hope to implement in the future include: More advanced emotion and tone analysis A mobile-first version Therapist-mode: shareable insights for mental health professionals Personalized affirmations and goal tracking Journaling streak rewards and social integrations Voice-to-text Audio Journaling. Possible modes for those with Neurodivergence or mental illness to help with emotion control

## README (from the GitHub repository)

No README available.

## Detected evidence (automated analysis)

Indexed codebase: 3 recognized source files, 35 KB.
- FastAPI (technology) — detected in the code
- Python (language) — detected in the code
- Vercel (technology) — detected in the code
- Firebase (technology) — claimed on Devpost, not found in the code
- JavaScript (language) — claimed on Devpost, not found in the code
- Next.js (technology) — claimed on Devpost, not found in the code
- React (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (6 of 6)

```
api/main.py
gemini_funcs.py
mongodb_funcs.py
requirements.txt
tmp.ipynb
vercel.json
```

### Dependencies

- requirements.txt: annotated-types@==0.7.0, anyio@==4.9.0, appnope@==0.1.4, asttokens@==3.0.0, cachetools@==5.5.2, certifi@==2025.1.31, charset-normalizer@==3.4.1, click@==8.1.8, comm@==0.2.2, debugpy@==1.8.14, decorator@==5.2.1, dnspython@==2.7.0, executing@==2.2.0, fastapi@==0.115.12, google-auth@==2.38.0, google-genai@==1.10.0, h11@==0.14.0, httpcore@==1.0.8, httpx@==0.28.1, idna@==3.10, ipykernel@==6.29.5, ipython@==9.1.0, ipython_pygments_lexers@==1.1.1, jedi@==0.19.2, jupyter_client@==8.6.3, jupyter_core@==5.7.2, matplotlib-inline@==0.1.7, nest-asyncio@==1.6.0, packaging@==24.2, parso@==0.8.4, pexpect@==4.9.0, platformdirs@==4.3.7, prompt_toolkit@==3.0.50, psutil@==7.0.0, ptyprocess@==0.7.0, pure_eval@==0.2.3, pyasn1@==0.6.1, pyasn1_modules@==0.4.2, pydantic@==2.11.3, pydantic_core@==2.33.1, Pygments@==2.19.1, pymongo@==4.12.0, python-dateutil@==2.9.0.post0, python-dotenv@==1.1.0, pyzmq@==26.4.0, requests@==2.32.3, rsa@==4.9, six@==1.17.0, sniffio@==1.3.1, stack-data@==0.6.3, starlette@==0.46.1, tornado@==6.4.2, traitlets@==5.14.3, typing_extensions@==4.13.2, typing-inspection@==0.4.0, urllib3@==2.4.0, uvicorn@==0.34.0, vercel@==0.2.1, wcwidth@==0.2.13, websockets@==15.0.1

### Recent commits (newest first)

- apr 13
- V7 F1
- V6 func call ready
- v5
- v4
- v3 api working
- v2 gemini func
- v1

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

### requirements.txt

```
annotated-types==0.7.0
anyio==4.9.0
cachetools==5.5.2
certifi==2025.1.31
charset-normalizer==3.4.1
click==8.1.8
dnspython==2.7.0
fastapi==0.115.12
google-auth==2.38.0
google-genai==1.10.0
h11==0.14.0
httpcore==1.0.8
httpx==0.28.1
idna==3.10
pyasn1==0.6.1
pyasn1_modules==0.4.2
pydantic==2.11.3
pydantic_core==2.33.1
pymongo==4.12.0
python-dotenv==1.1.0
requests==2.32.3
rsa==4.9
sniffio==1.3.1
starlette==0.46.1
typing-inspection==0.4.0
typing_extensions==4.13.2
urllib3==2.4.0
uvicorn==0.34.0
websockets==15.0.1
annotated-types==0.7.0
anyio==4.9.0
appnope==0.1.4
asttokens==3.0.0
cachetools==5.5.2
certifi==2025.1.31
charset-normalizer==3.4.1
click==8.1.8
comm==0.2.2
debugpy==1.8.14
decorator==5.2.1
dnspython==2.7.0
executing==2.2.0
fastapi==0.115.12
google-auth==2.38.0
google-genai==1.10.0
h11==0.14.0
httpcore==1.0.8
httpx==0.28.1
idna==3.10
ipykernel==6.29.5
ipython==9.1.0
ipython_pygments_lexers==1.1.1
jedi==0.19.2
jupyter_client==8.6.3
jupyter_core==5.7.2
matplotlib-inline==0.1.7
nest-asyncio==1.6.0
packaging==24.2
parso==0.8.4
pexpect==4.9.0
platformdirs==4.3.7
prompt_toolkit==3.0.50
psutil==7.0.0
ptyprocess==0.7.0
pure_eval==0.2.3
pyasn1==0.6.1
pyasn1_modules==0.4.2
pydantic==2.11.3
pydantic_core==2.33.1
Pygments==2.19.1
pymongo==4.12.0
python-dateutil==2.9.0.post0
python-dotenv==1.1.0
pyzmq==26.4.0
requests==2.32.3
rsa==4.9
six==1.17.0
sniffio==1.3.1
stack-data==0.6.3
starlette==0.46.1
tornado==6.4.2
traitlets==5.14.3
typing-inspection==0.4.0
typing_extensions==4.13.2
urllib3==2.4.0
uvicorn==0.34.0
vercel==0.2.1
wcwidth==0.2.13
websockets==15.0.1

```

### api/main.py

```python
from fastapi import FastAPI, HTTPException, Depends
from fastapi.middleware.cors import CORSMiddleware
from pymongo.mongo_client import MongoClient
from pymongo.server_api import ServerApi
import os
from dotenv import load_dotenv
from gemini_funcs import generate_reflection, generate_weekly_reflection
from mongodb_funcs import (
    db_create_user, db_create_journal, db_load_journal,  db_create_wisper, db_user_auth,
    db_load_journals_past_week, db_update_journal_streak, db_user_login
)
from pydantic import BaseModel
from typing import Optional
from datetime import datetime
import json
from fastapi.responses import JSONResponse

#init date
journal_date = datetime.now()

# env var init
load_dotenv()
mongodb_pw = os.getenv("mongodb_pw")
mongodb_acc = os.getenv("mongodb_acc")
# fastapi init
app = FastAPI()

# mongodb init

uri = f"mongodb+srv://{mongodb_acc}:{mongodb_pw}@echo.yldx4rh.mongodb.net/?appName=echo"
client = MongoClient(uri, server_api=ServerApi('1'))
try:
    client.admin.command('ping')
    print("Pinged your deployment. You successfully connected to MongoDB!")
except Exception as e:
    print(e)
db = client["echouser"]
userdata_collection = db["userdata"]
journals_collection = db["journals"]
reflections_collection = db["reflections"]
weekly_reflections_collection = db["weekly_reflections"]
wispers_collection = db["wispers"]





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

# Models for request validation
class AnonymousItem(BaseModel):
    prompt: str
    emotions: int

class FirstUser(BaseModel):
    user_id: str  # Standardized to user_id
    name: str
    prompt: str
    reflection: str
    emotions: int


class JournalEntry(BaseModel):
    user_id: str  # Standardized to user_id
    prompt: str
    emotions: int

class WisperItem(BaseModel):
    user_id: str  # Standardized to user_id
    date: str  # Date string in YYYY-MM-DD format
    wisper: Optional[str] = None
    ysym: Optional[str] = None

class WeeklyReflectionItem(BaseModel):
    user_id: str  # Standardized to user_id
    week_start: str  # Date string in YYYY-MM-DD format
    week_end: str    # Date string in YYYY-MM-DD format
    weekly_reflection: str

class UserAuth(BaseModel):
    user_id: str
    name: Optional[str] = None  # Standardized to user_id


class JournalLoadRequest(BaseModel):
    user_id: str  # Standardized to user_id
    date: Optional[str] = None  # 可以傳遞日期，但不是必填


@app.get("/")
def read_root():
    return {"stat": "function_working"}


@app.post("/api/anonymous")
async def create_anonymous_reflection(item: AnonymousItem):
    """Generate a reflection without saving to database (no user account)"""
    reflection = generate_reflection(item.prompt, item.emotions)
    return JSONResponse(
        status_code=200,
        content={"status": "success", "reflection": reflection}
    )

@app.post("/api/journal/first_upload")
async def create_first_journal(item: FirstUser):
    """Create a new user and their first journal entry"""
    db_update_journal_streak(user_id=item.user_id, journal_date=journal_date, userdata_collection=userdata_collection)
    db_create_journal(
        journals_collection,
        userdata_collection,  # Added missing parameter
        item.user_id,
        item.prompt,
        item.reflection,
        item.emotions,
    )
    return JSONResponse(
        status_code=200,
        content={"status": "success"}
    )

@app.post("/api/user_auth")
async def authenticate_user(item: UserAuth):
    db_create_user(
        userdata_collection,
        item.user_id,
        item.name
    )

    user_data = db_user_auth(
        item.name,
        user_id=item.user_id,  # Changed parameter name
        userdata_collection=userdata_collection, 
        journals_collection=journals_collection,
  
        )

    if isinstance(user_data, JSONResponse):
        return user_data  # Return error JSON directly if there was an error
    return JSONResponse(
        status_code=200,
        content={
            "status": "success",
            "user": user_data
        }
    )

@app.post("/api/user_login")
async def authenticate_user(item: UserAuth):
    if item.user_id not in userdata_collection.distinct("user_id"):
        return JSONResponse(
            status_code=500,
            content={"status": "error", "message": "User not found"}
        )

    user_data = db_user_login(
        user_id=item.user_id,  # Changed parameter name
        userdata_collection=userdata_collection, 
        journals_collection=journals_collection,
        weekly_reflections_collection=weekly_reflections_collection
        )

    if isinstance(user_data, JSONResponse):
        return user_data  # Return error JSON directly if there was an error
    return JSONResponse(
        status_code=200,
        content={
            "status": "success",
            "user": user_data
        }
    )






@app.post("/api/journal/load")
async def load_journal_entries(request: JournalLoadRequest):
    return db_load_journal(journals_collection, request.user_id, request.date)

@app.post("/api/journal/upload")
async def create_journal_entry(item: JournalEntry):
    # Generate reflection for current journal entry
    result = generate_reflection(item.prompt, item.emotions)
    
    # Create journal entry and get the response as a dictionary
    journal_data = db_create_journal(
        journals_collection=journals_collection,
        userdata_collection=userdata_collection,
        weekly_reflection_collection=weekly_reflections_collection,  # Add this parameter
        user_id=item.user_id,
        prompt=item.prompt,
        reflection=result["reflection"],
        emotions=item.emotions,
        ys=result.get("ys"),
        ym=result.get("ym")
    )
    
    # Construct response with both reflection data and weekly reflection
    response_data = {
        "status": "success",
        "user_id": item.user_id,
    
[truncated — 1901 more characters]
```

### gemini_funcs.py

```python
from google import genai
from google.genai import types
import os
import json
from dotenv import load_dotenv
from typing import Tuple, Optional
from pydantic import BaseModel

class JournalEntry(BaseModel):
    user_id: str
    prompt: str
    emotions: int

#env var init
load_dotenv()
gemini_apikey = os.getenv("gemini_apikey")

#gemini client init
client = genai.Client(api_key=gemini_apikey)

#gemini func call for ysym init
system_prompt_ysym = """
You are a thoughtful and emotionally intelligent assistant that helps users reflect more deeply on their journal entries by surfacing potential emotional subtext. This response is called a “You Said, You Meant” and should appear when it feels the entry is meaningful or revealing

Use the user’s journal entry to identify moments where there may be a deeper emotion, belief, or fear behind what they said. Your goal is to gently highlight that subtext with empathy and insight.

Your response format should be:

You said: [Direct quote from user’s journal]
You meant: [Emotional truth or hidden worry/hope beneath it]

Optional intro: “We noticed something in what you wrote… want to take a deeper look?”

Guidelines:
	•	Only respond if the subtext is emotionally significant (based on sentiment, emotional weight, or key phrases).
	•	Keep it short and impactful — no more than 2-3 lines total.
	•	Maintain a compassionate, non-judgmental tone.
	•	Avoid overanalyzing or stretching the interpretation.

This feature is meant to help the user notice patterns or feelings they may not have fully realized yet — be insightful, but always kind.

here is an example structure:
{
  "ys": "Everyone seemed to be in a good mood today. I didn’t really feel like talking though.",
  "ym": "It sounds like you might have felt a bit disconnected from the energy around you, or perhaps just needed some quiet space for yourself."
}
"""
def generate_ysym(sub_prompt):
    response = client.models.generate_content(
        model="gemini-2.0-flash",
        contents=sub_prompt,
        config=types.GenerateContentConfig(
            system_instruction=system_prompt_ysym
        )
    )
    print(response)
    return response

you_said_you_meant = {
    "name": "generate_ysym",
    "description": "generate you said you meant",
    "parameters": {
        "type": "object",
        "properties": {
            "sub_prompt": {
                "type": "string",
            },
        },
        "required": ["sub_prompt" ],
    },
}
tools = types.Tool(function_declarations=[you_said_you_meant])



system_prompt_emotion_breakdown = """
give three to five adjitive that fit the most then give it a percentage from the journals.
general output: "{adj1}-{percent},{adj2}-{percen2t},{adj3}-{percen3},{adj4}-{percent4},{adj5}-{percent5}"
demo output1 : "Happy-30,Excited-23,Disappoint-12,Curiosity-20,Calm-15"
demo output2 : "Sad-30,Self Criticizing-45,Demotivated-25"
"""
 
system_prompt_reflection = """
You are a compassionate and insightful mental wellness companion. A user has just written a short journal entry (1 to 10 sentences). Your task is to provide a reflection that mirrors their emotion, offers gentle insight, or encouragement— something they may not have consciously realized. The reflection should feel like it comes from someone deeply attuned to their feelings and subconscious mind. 
Your response is recommended to be around 3 sentences. 
Speak with warmth, wisdom, and clarity.
Avoid repeating what the user wrote— respond to it, not with it.
Focus on emotion, patterns, or deeper truths beneath the surface.
Never offer solutions or advice— only insight, reflection, or emotional resonance.
This is not a summary. It is not a reply. It is a mirror


	Here are two example outputs for an idea of what is a good output
1. “You’ve been holding it together — and that counts. Today was hard, but you still showed up.” 
2. “It's good you apologized. Acknowledge your stress, find healthy ways to release it, and rebuild trust with your friend through consistent actions.”
"""
system_prompt_reflection_with_ysym = """
fetch out the part that encludes the most amount of emotion, then let it be sub-prompt for function call, only if there is not enough emotion, then don't call the function.
"""

system_prompt_weekly_reflection = """
Analyze the mood progression over the 7 entries 
Describe the general trend: Was it mostly high, low, stable, volatile, improving, declining?
Mention any significant peaks (high points) or valleys (low points) during the week and briefly note what might have coincided with them based on the entry text.*   Provide a thoughtful, long-form reflection (approximately 4-5 sentences).
Offer gentle observations about potential connections, patterns, or areas of growth noticed during the week.
Maintain a supportive, empathetic, non-judgmental, and encouraging tone. Frame observations as possibilities, not definitive statements (e.g., "It seems like...", "Perhaps...", "You might be noticing...").
Avoid giving direct advice unless it's very general and focused on self-compassion or reflection (e.g., "It might be helpful to be kind to yourself during challenging moments.").
End on a positive or forward-looking note, encouraging continued journaling and self-discovery.

Tone & Style: Empathetic, insightful, supportive, gentle, slightly reflective, non-clinical, and non-judgmental. Use "you" when addressing the user. Speak as Echoes.
"""


def generate_emotion_breakdown(prompt):
    response = client.models.generate_content(
        model="gemini-2.0-flash",
        contents=prompt,
        config=types.GenerateContentConfig(
            system_instruction=system_prompt_emotion_breakdown
        )
    )
    result = {}
    for item in response.text.split(','):
        key, value = item.rsplit('-', 1)
        result[key.strip()] = int(value)
    return result


def generate_reflection(prompt: str, emotions: int):
    model_1 = client.models.generate_content(
        model="gemini-2.0-flash",
   
[truncated — 1954 more characters]
```

### mongodb_funcs.py

```python
from datetime import datetime, timedelta
from typing import Dict, List, Optional
from bson import ObjectId
from fastapi.responses import JSONResponse
from gemini_funcs import generate_weekly_reflection, generate_emotion_breakdown, generate_quote
import traceback

def db_update_journal_streak(user_id, journal_date, userdata_collection):
    user = userdata_collection.find_one({"user_id": user_id})

    if not user:
        return

    last_entry = user.get("last_entry_date")
    today = journal_date.date()

    if last_entry:
        last_entry = last_entry.date() if isinstance(last_entry, datetime) else datetime.strptime(last_entry, "%Y-%m-%d").date()

        diff = (today - last_entry).days
        if diff == 1:
            # 延續 streak
            new_streak = user["journal_streak"] + 1
        elif diff == 0:
            # 同一天重複寫，不更新 streak
            new_streak = user["journal_streak"]
        else:
            # 中斷 streak
            new_streak = 1
    else:
        new_streak = 1

    # 更新 longest streak
    new_longest = max(user.get("longest_journal_streak", 0), new_streak)

    # 更新資料庫
    userdata_collection.update_one(
        {"user_id": user_id},
        {"$set": {
            "journal_streak": new_streak,
            "longest_journal_streak": new_longest,
            "last_entry_date": today
        }}
    )

    print(f"[{user_id}] streak updated: {new_streak}, longest: {new_longest}")
    return new_streak, new_longest

def db_create_user(collection, user_id: str, name: str) -> JSONResponse:
    """
    Create a new user and their first journal entry
    """
    try:
        # Check if user already exists
        existing_user = collection.find_one({"user_id": user_id})
        if existing_user:
            return JSONResponse(
                status_code=400,
                content={"status": "error", "message": "User already exists"}
            )
        
        # Create user's first journal entry
        user_data = {
            "user_id": user_id,
            "name": name,
            "journal_streak": 0,
            "longest_journal_streak": 0,
            "account_created_at": datetime.now(),
            "last_journal_date": None
        }
        
        result = collection.insert_one(user_data)
        
        return JSONResponse(
            status_code=201,
            content={
                "status": "success",
                "message": "User created successfully",
                "user_id": user_id,
                "user_name": name,
                "journal_streak": 0,
                "longest_journal_streak": 0,
                "entry_id": str(result.inserted_id)
            }
        )
    
    except Exception as e:
        return JSONResponse(
            status_code=500,
            content={"status": "error", "message": str(e)}
        )

def db_create_journal(
    journals_collection,
    userdata_collection,
    weekly_reflection_collection,  # Add this parameter
    user_id: str, 
    prompt: str, 
    reflection: str, 
    emotions: int, 
    ys: Optional[str] = None,
    ym: Optional[str] = None
):
    try:
        today = datetime.now().date()

        # Step 1: Insert journal
        journal_entry = {
            "user_id": user_id,
            "prompt": prompt,
            "emotions": emotions,
            "reflection": reflection,
            "created_at": datetime.now()
        }
        if ys:
            journal_entry["ys"] = ys
        if ym:
            journal_entry["ym"] = ym
        
        journals_collection.insert_one(journal_entry)

        # Step 2: Update streak info
        user = userdata_collection.find_one({"user_id": user_id})
        if not user:
            return {"status": "error", "message": "User not found"}

        last_date = user.get("last_journal_date")
        streak = user.get("journal_streak", 0)
        longest = user.get("longest_journal_streak", 0)
        last_weekly_reflection = user.get("last_weekly_reflection")

        # 判斷 streak
        if last_date:
            last_date_obj = last_date.date() if isinstance(last_date, datetime) else datetime.strptime(last_date, "%Y-%m-%d").date()
            if last_date_obj == today:
                # 同一天寫多篇日記，不更新 streak
                pass
            elif last_date_obj == today - timedelta(days=1):
                streak += 1
            else:
                streak = 1
        else:
            streak = 1

        # 更新 longest_streak
        longest = max(longest, streak)

        update_data = {
            "journal_streak": streak,
            "longest_journal_streak": longest,
            "last_journal_date": datetime.now(),
        }

        # Step 3: 檢查是否該生成 weekly reflection
        weekly_reflection_text = None
        emotion_breakdown = None

        if streak >= 7:
            generate_weekly = False

            if last_weekly_reflection:
                last_weekly_date = last_weekly_reflection.date() if isinstance(last_weekly_reflection, datetime) else datetime.strptime(last_weekly_reflection, "%Y-%m-%d").date()
                if (today - last_weekly_date).days >= 7:
                    generate_weekly = True
            else:
                generate_weekly = True

            if generate_weekly:
                # Calculate week start and end dates
                week_end = today
                week_start = week_end - timedelta(days=7)
                
                past_week_journals = db_load_journals_past_week(journals_collection, user_id)
                if past_week_journals and len(past_week_journals) > 0:
                    # Generate weekly reflection
                    weekly_reflection_text = generate_weekly_reflection(past_week_journals)
                    
                    try:
                        emotion_breakdown = generate_emotion_breakdown(past_week_journals)
                    except Exception as e:
                        print("Emotion breakdown error:", e)
                    
                    # Sa
[truncated — 13686 more characters]
```