Spaces:
Sleeping
Sleeping
| # 🚀 BREATHE — Future Enhancements | |
| A living document of planned and possible improvements to the BREATHE platform. | |
| --- | |
| ## 1. PostgreSQL — Persistent Production Database | |
| SQLite is fine for development but it resets or corrupts when the server restarts on many cloud platforms. PostgreSQL is a proper production-grade database where your data persists independently of the server. | |
| ### Why switch? | |
| - Data survives server restarts, crashes, and re-deployments | |
| - Multiple workers can read/write simultaneously (SQLite locks) | |
| - You can inspect, back up, and query data directly from any Postgres client | |
| - Required by most cloud platforms (Railway, Render, Supabase, AWS RDS, etc.) | |
| ### Step-by-step setup (local) | |
| **1. Install PostgreSQL** | |
| ```bash | |
| # macOS (Homebrew) | |
| brew install postgresql@16 | |
| brew services start postgresql@16 | |
| # Ubuntu / Debian | |
| sudo apt install postgresql postgresql-contrib | |
| sudo systemctl start postgresql | |
| ``` | |
| **2. Create the database and user** | |
| ```bash | |
| psql postgres | |
| ``` | |
| ```sql | |
| CREATE USER breathe_user WITH PASSWORD 'your_strong_password'; | |
| CREATE DATABASE breathe_db OWNER breathe_user; | |
| GRANT ALL PRIVILEGES ON DATABASE breathe_db TO breathe_user; | |
| \q | |
| ``` | |
| **3. Install the Python driver** | |
| ```bash | |
| source venv/bin/activate | |
| pip install psycopg2-binary | |
| ``` | |
| **4. Update `.env`** | |
| ``` | |
| DATABASE_URL=postgresql://breathe_user:your_strong_password@localhost:5432/breathe_db | |
| ``` | |
| **5. Run the app — tables are created automatically** | |
| ```bash | |
| python app.py | |
| ``` | |
| Flask's `db.create_all()` will create all tables in Postgres on first run. | |
| **6. Verify** | |
| ```bash | |
| psql -U breathe_user -d breathe_db -c "\dt" | |
| # Should list: users, assessments, gratitude_entries | |
| ``` | |
| ### Step-by-step setup (cloud — Supabase, free tier) | |
| 1. Go to [supabase.com](https://supabase.com) → New project | |
| 2. In **Settings → Database** copy the **Connection string** (URI format) | |
| 3. Paste it into `.env` as `DATABASE_URL` | |
| 4. The app connects and creates tables on next start — no other changes needed | |
| ### Step-by-step setup (cloud — Railway) | |
| 1. Go to [railway.app](https://railway.app) → New project → Add PostgreSQL | |
| 2. Click the Postgres service → **Connect** tab → copy the `DATABASE_URL` | |
| 3. Add it as an environment variable in your BREATHE service on Railway | |
| 4. Re-deploy — done | |
| ### Keeping data safe with backups | |
| ```bash | |
| # Dump the entire database | |
| pg_dump -U breathe_user breathe_db > backup_$(date +%Y%m%d).sql | |
| # Restore from a dump | |
| psql -U breathe_user breathe_db < backup_20260502.sql | |
| ``` | |
| --- | |
| ## 2. Sign Up / Log In with Google (OAuth 2.0) | |
| Let users authenticate with their Google account — no password to remember. | |
| ### How it works | |
| 1. User clicks "Continue with Google" | |
| 2. Browser redirects to Google's OAuth consent screen | |
| 3. Google returns an authorization code to your callback URL | |
| 4. Flask exchanges the code for an access token and reads the user's profile (name, email, avatar) | |
| 5. Your app creates or logs in the user automatically | |
| ### Backend — Flask-Dance (simplest approach) | |
| **Install** | |
| ```bash | |
| pip install flask-dance[sqla] | |
| ``` | |
| **Register a Google OAuth app** | |
| 1. Go to [console.cloud.google.com](https://console.cloud.google.com) | |
| 2. Create a new project → **APIs & Services → Credentials** | |
| 3. Click **Create Credentials → OAuth client ID** | |
| 4. Application type: **Web application** | |
| 5. Authorised redirect URI: `http://localhost:5000/login/google/authorized` | |
| 6. Copy the **Client ID** and **Client Secret** into `.env`: | |
| ``` | |
| GOOGLE_CLIENT_ID=your_client_id | |
| GOOGLE_CLIENT_SECRET=your_client_secret | |
| ``` | |
| **Add to `backend/__init__.py`** | |
| ```python | |
| from flask_dance.contrib.google import make_google_blueprint, google | |
| google_bp = make_google_blueprint( | |
| client_id=os.environ.get("GOOGLE_CLIENT_ID"), | |
| client_secret=os.environ.get("GOOGLE_CLIENT_SECRET"), | |
| scope=["openid", "email", "profile"], | |
| redirect_url="/api/auth/google/callback", | |
| ) | |
| app.register_blueprint(google_bp, url_prefix="/login") | |
| ``` | |
| **Add a callback route in `auth.py`** | |
| ```python | |
| from flask_dance.contrib.google import google | |
| @auth_bp.route("/google/callback") | |
| def google_callback(): | |
| if not google.authorized: | |
| return jsonify({"error": "Not authorized"}), 401 | |
| resp = google.get("/oauth2/v2/userinfo") | |
| info = resp.json() | |
| user = User.query.filter_by(email=info["email"]).first() | |
| if not user: | |
| user = User(username=info["name"], email=info["email"]) | |
| user.avatar = info.get("picture") | |
| db.session.add(user) | |
| db.session.commit() | |
| session["user_id"] = user.id | |
| return redirect("http://localhost:5173/app/breathe") | |
| ``` | |
| **Frontend — add a Google button to `AuthPage.jsx`** | |
| ```jsx | |
| <a href="http://localhost:5000/login/google" className="btn-google"> | |
| <img src="/google-icon.svg" alt="" /> Continue with Google | |
| </a> | |
| ``` | |
| --- | |
| ## 3. Improved UI & UX | |
| ### Ideas to implement | |
| | Area | Enhancement | | |
| |------|-------------| | |
| | Onboarding | First-time walkthrough tooltip tour (using `driver.js` or `intro.js`) | | |
| | Themes | Light mode toggle + system preference detection | | |
| | Animations | Framer Motion page transitions and micro-interactions | | |
| | Mobile | Full PWA support — installable on phone home screen | | |
| | Accessibility | ARIA labels, keyboard navigation, high-contrast mode | | |
| | Charts | Hover annotations, zoom on timeline, weekly/monthly toggle | | |
| | Notifications | Browser push notifications for daily assessment reminders | | |
| | Streaks | Gamification — show journaling streak counter on dashboard | | |
| | Data export | Download your assessment + journal history as CSV or PDF | | |
| ### Quick win — dark/light theme toggle | |
| ```js | |
| // in index.css: add a [data-theme="light"] block overriding --bg, --surface, etc. | |
| // in a ThemeToggle component: | |
| document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light') | |
| localStorage.setItem('theme', isDark ? 'dark' : 'light') | |
| ``` | |
| --- | |
| ## 4. Calm Background Music | |
| Play ambient/calming audio tracks inside the app to accompany breathing and journaling sessions. | |
| ### Option A — Self-hosted audio files (simplest) | |
| 1. Download royalty-free tracks from [freemusicarchive.org](https://freemusicarchive.org) or [pixabay.com/music](https://pixabay.com/music/) | |
| 2. Place `.mp3` files in `frontend/public/audio/` | |
| 3. Build a `MusicPlayer` component: | |
| ```jsx | |
| // frontend/src/components/MusicPlayer.jsx | |
| import { useState, useRef } from 'react' | |
| const TRACKS = [ | |
| { title: 'Forest Rain', src: '/audio/forest-rain.mp3' }, | |
| { title: 'Ocean Waves', src: '/audio/ocean-waves.mp3' }, | |
| { title: 'Tibetan Bowls', src: '/audio/tibetan-bowls.mp3' }, | |
| ] | |
| export default function MusicPlayer() { | |
| const [playing, setPlaying] = useState(false) | |
| const [track, setTrack] = useState(0) | |
| const audioRef = useRef(null) | |
| function togglePlay() { | |
| if (playing) { audioRef.current.pause() } | |
| else { audioRef.current.play() } | |
| setPlaying(!playing) | |
| } | |
| function changeTrack(i) { | |
| setTrack(i) | |
| setPlaying(false) | |
| setTimeout(() => { audioRef.current.load(); audioRef.current.play(); setPlaying(true) }, 50) | |
| } | |
| return ( | |
| <div className="music-player"> | |
| <audio ref={audioRef} loop src={TRACKS[track].src} /> | |
| <button onClick={togglePlay}>{playing ? '⏸' : '▶'}</button> | |
| <span>{TRACKS[track].title}</span> | |
| {TRACKS.map((t, i) => ( | |
| <button key={i} onClick={() => changeTrack(i)}>{t.title}</button> | |
| ))} | |
| </div> | |
| ) | |
| } | |
| ``` | |
| 4. Add `<MusicPlayer />` to `BreathePage.jsx` or the guided exercise modal | |
| ### Option B — Streaming via YouTube IFrame API (no file hosting needed) | |
| ```jsx | |
| // Embed a YouTube ambient playlist | |
| <iframe | |
| src="https://www.youtube.com/embed/videoseries?list=PLQ6T_LmSTMi17X70BIqNfSFRMC1u83M7m&autoplay=1&loop=1" | |
| allow="autoplay" | |
| style={{ display: 'none' }} // audio only | |
| /> | |
| ``` | |
| --- | |
| ## 5. Text-to-Speech for Activity Instructions | |
| Convert the step-by-step instructions in guided exercises into spoken audio so users can close their eyes and follow along hands-free. | |
| ### Option A — Web Speech API (free, no API key, built into browser) | |
| ```jsx | |
| // frontend/src/utils/tts.js | |
| export function speak(text, { rate = 0.85, pitch = 1, volume = 1 } = {}) { | |
| if (!window.speechSynthesis) return | |
| window.speechSynthesis.cancel() | |
| const utt = new SpeechSynthesisUtterance(text) | |
| utt.rate = rate | |
| utt.pitch = pitch | |
| utt.volume = volume | |
| // Pick a calm voice if available | |
| const voices = window.speechSynthesis.getVoices() | |
| const calm = voices.find(v => v.name.includes('Samantha') || v.name.includes('Karen')) | |
| if (calm) utt.voice = calm | |
| window.speechSynthesis.speak(utt) | |
| } | |
| export function stopSpeaking() { | |
| window.speechSynthesis.cancel() | |
| } | |
| ``` | |
| **Use it in `BreathePage.jsx` guided modal:** | |
| ```jsx | |
| import { speak, stopSpeaking } from '../utils/tts' | |
| // When step changes, read it aloud: | |
| useEffect(() => { | |
| speak(`${s.label}. ${s.desc}`) | |
| return () => stopSpeaking() | |
| }, [step]) | |
| // Add a toggle button: | |
| const [ttsOn, setTtsOn] = useState(false) | |
| ``` | |
| **Add a count-down timer with spoken cues for Box Breathing:** | |
| ```jsx | |
| useEffect(() => { | |
| if (!s.duration || !ttsOn) return | |
| speak(`${s.label}. ${s.duration} seconds.`) | |
| const timer = setTimeout(() => speak('Next.'), s.duration * 1000) | |
| return () => clearTimeout(timer) | |
| }, [step]) | |
| ``` | |
| ### Option B — ElevenLabs API (natural-sounding AI voices) | |
| 1. Sign up at [elevenlabs.io](https://elevenlabs.io) (free tier: 10,000 chars/month) | |
| 2. Generate MP3 files for each step offline and include them as static assets (same as Option A of music) | |
| 3. Or call the API at runtime: | |
| ```python | |
| # backend/routes/tts.py | |
| import requests, os | |
| from flask import Blueprint, jsonify, request | |
| tts_bp = Blueprint("tts", __name__, url_prefix="/api/tts") | |
| @tts_bp.route("/speak", methods=["POST"]) | |
| def speak(): | |
| text = (request.get_json() or {}).get("text", "")[:500] | |
| resp = requests.post( | |
| "https://api.elevenlabs.io/v1/text-to-speech/EXAVITQu4vr4xnSDxMaL", | |
| headers={"xi-api-key": os.environ["ELEVEN_API_KEY"]}, | |
| json={"text": text, "voice_settings": {"stability": 0.5, "similarity_boost": 0.75}}, | |
| ) | |
| return resp.content, 200, {"Content-Type": "audio/mpeg"} | |
| ``` | |
| ```js | |
| // Frontend: fetch and play | |
| const res = await fetch('/api/tts/speak', { method:'POST', body: JSON.stringify({text}), headers:{'Content-Type':'application/json'} }) | |
| const blob = await res.blob() | |
| new Audio(URL.createObjectURL(blob)).play() | |
| ``` | |
| ### Option C — Google Cloud Text-to-Speech (highest quality, WaveNet voices) | |
| ```bash | |
| pip install google-cloud-texttospeech | |
| ``` | |
| ```python | |
| from google.cloud import texttospeech | |
| client = texttospeech.TextToSpeechClient() | |
| synthesis_input = texttospeech.SynthesisInput(text=text) | |
| voice = texttospeech.VoiceSelectionParams( | |
| language_code="en-US", | |
| name="en-US-Wavenet-F", | |
| ) | |
| audio_config = texttospeech.AudioConfig(audio_encoding=texttospeech.AudioEncoding.MP3) | |
| response = client.synthesize_speech(input=synthesis_input, voice=voice, audio_config=audio_config) | |
| # response.audio_content is bytes → stream to frontend | |
| ``` | |
| --- | |
| ## 6. Spotify Integration — Play Your Playlists Inside BREATHE | |
| Let users connect their Spotify account and play their own playlists (or curated calm playlists) without leaving the app. | |
| ### How it works (Spotify Web Playback SDK + OAuth PKCE) | |
| **Step 1 — Create a Spotify app** | |
| 1. Go to [developer.spotify.com/dashboard](https://developer.spotify.com/dashboard) | |
| 2. Click **Create App** | |
| 3. Set **Redirect URI** to `http://localhost:5173/app/spotify/callback` | |
| 4. Copy your **Client ID** (no secret needed for PKCE flow) | |
| 5. Add to `.env`: `VITE_SPOTIFY_CLIENT_ID=your_client_id` | |
| **Step 2 — Authorization (PKCE, no backend needed)** | |
| ```js | |
| // frontend/src/utils/spotify.js | |
| const CLIENT_ID = import.meta.env.VITE_SPOTIFY_CLIENT_ID | |
| const REDIRECT_URI = 'http://localhost:5173/app/spotify/callback' | |
| const SCOPES = 'streaming user-read-email user-read-private user-library-read playlist-read-private' | |
| export async function loginWithSpotify() { | |
| const verifier = generateCodeVerifier(128) | |
| const challenge = await generateCodeChallenge(verifier) | |
| localStorage.setItem('spotify_verifier', verifier) | |
| const params = new URLSearchParams({ | |
| response_type: 'code', | |
| client_id: CLIENT_ID, | |
| scope: SCOPES, | |
| redirect_uri: REDIRECT_URI, | |
| code_challenge_method: 'S256', | |
| code_challenge: challenge, | |
| }) | |
| window.location = 'https://accounts.spotify.com/authorize?' + params | |
| } | |
| export async function exchangeToken(code) { | |
| const verifier = localStorage.getItem('spotify_verifier') | |
| const res = await fetch('https://accounts.spotify.com/api/token', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, | |
| body: new URLSearchParams({ | |
| grant_type: 'authorization_code', | |
| code, | |
| redirect_uri: REDIRECT_URI, | |
| client_id: CLIENT_ID, | |
| code_verifier: verifier, | |
| }), | |
| }) | |
| const data = await res.json() | |
| localStorage.setItem('spotify_token', data.access_token) | |
| localStorage.setItem('spotify_refresh', data.refresh_token) | |
| return data.access_token | |
| } | |
| // Helper: generate PKCE verifier/challenge | |
| function generateCodeVerifier(length) { | |
| const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~' | |
| return Array.from(crypto.getRandomValues(new Uint8Array(length))) | |
| .map(b => chars[b % chars.length]).join('') | |
| } | |
| async function generateCodeChallenge(verifier) { | |
| const data = new TextEncoder().encode(verifier) | |
| const digest = await crypto.subtle.digest('SHA-256', data) | |
| return btoa(String.fromCharCode(...new Uint8Array(digest))) | |
| .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '') | |
| } | |
| ``` | |
| **Step 3 — Load the Spotify Web Playback SDK** | |
| ```html | |
| <!-- frontend/index.html --> | |
| <script src="https://sdk.scdn.co/spotify-player.js"></script> | |
| ``` | |
| **Step 4 — Create a `SpotifyPlayer` component** | |
| ```jsx | |
| // frontend/src/components/SpotifyPlayer.jsx | |
| import { useEffect, useState } from 'react' | |
| export default function SpotifyPlayer({ token }) { | |
| const [player, setPlayer] = useState(null) | |
| const [deviceId, setDeviceId] = useState(null) | |
| const [playing, setPlaying] = useState(false) | |
| const [track, setTrack] = useState(null) | |
| const [playlists, setPlaylists] = useState([]) | |
| useEffect(() => { | |
| window.onSpotifyWebPlaybackSDKReady = () => { | |
| const p = new window.Spotify.Player({ | |
| name: 'BREATHE Player', | |
| getOAuthToken: cb => cb(token), | |
| volume: 0.5, | |
| }) | |
| p.addListener('ready', ({ device_id }) => setDeviceId(device_id)) | |
| p.addListener('player_state_changed', state => { | |
| if (!state) return | |
| setTrack(state.track_window.current_track) | |
| setPlaying(!state.paused) | |
| }) | |
| p.connect() | |
| setPlayer(p) | |
| } | |
| }, [token]) | |
| // Fetch user's playlists | |
| useEffect(() => { | |
| fetch('https://api.spotify.com/v1/me/playlists', { | |
| headers: { Authorization: `Bearer ${token}` } | |
| }).then(r => r.json()).then(d => setPlaylists(d.items || [])) | |
| }, [token]) | |
| async function playPlaylist(uri) { | |
| await fetch(`https://api.spotify.com/v1/me/player/play?device_id=${deviceId}`, { | |
| method: 'PUT', | |
| headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ context_uri: uri }), | |
| }) | |
| } | |
| return ( | |
| <div className="spotify-player"> | |
| {track && ( | |
| <div className="sp-now-playing"> | |
| <img src={track.album.images[0]?.url} alt="" className="sp-album-art" /> | |
| <div> | |
| <div className="sp-track-name">{track.name}</div> | |
| <div className="sp-artist">{track.artists.map(a => a.name).join(', ')}</div> | |
| </div> | |
| <button onClick={() => player.togglePlay()}>{playing ? '⏸' : '▶'}</button> | |
| <button onClick={() => player.previousTrack()}>⏮</button> | |
| <button onClick={() => player.nextTrack()}>⏭</button> | |
| </div> | |
| )} | |
| <div className="sp-playlists"> | |
| {playlists.map(pl => ( | |
| <button key={pl.id} onClick={() => playPlaylist(pl.uri)}> | |
| {pl.images[0] && <img src={pl.images[0].url} alt="" />} | |
| {pl.name} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| ) | |
| } | |
| ``` | |
| **Step 5 — Add a callback route in React** | |
| ```jsx | |
| // App.jsx — add inside <Routes> | |
| <Route path="/app/spotify/callback" element={<SpotifyCallback />} /> | |
| ``` | |
| ```jsx | |
| // pages/SpotifyCallback.jsx | |
| import { useEffect } from 'react' | |
| import { useNavigate } from 'react-router-dom' | |
| import { exchangeToken } from '../utils/spotify' | |
| export default function SpotifyCallback() { | |
| const navigate = useNavigate() | |
| useEffect(() => { | |
| const code = new URLSearchParams(window.location.search).get('code') | |
| if (code) exchangeToken(code).then(() => navigate('/app/breathe')) | |
| }, []) | |
| return <div>Connecting to Spotify…</div> | |
| } | |
| ``` | |
| **Step 6 — Add "Connect Spotify" button to the Breathe hub or Dashboard** | |
| ```jsx | |
| import { loginWithSpotify } from '../utils/spotify' | |
| const token = localStorage.getItem('spotify_token') | |
| if (!token) { | |
| return <button onClick={loginWithSpotify}>🎵 Connect Spotify</button> | |
| } | |
| return <SpotifyPlayer token={token} /> | |
| ``` | |
| > **Note:** The Spotify Web Playback SDK requires a **Spotify Premium** account to play audio. | |
| > Free accounts can still fetch playlist metadata but cannot stream tracks directly. | |
| --- | |
| ## 7. Other Future Enhancements | |
| | Enhancement | Notes | | |
| |-------------|-------| | |
| | **Wearable data sync** | Import heart rate & sleep data from Apple Health / Google Fit via their REST APIs and use it as auto-filled psychometric inputs | | |
| | **AI chat support** | Add a "Talk to BREATHE" widget using OpenAI's chat API — gives personalised coping suggestions based on the user's stress history | | |
| | **Mood tracker** | Daily one-tap mood log (separate from assessments) charted over time | | |
| | **Weekly report email** | Cron job + Flask-Mail sends a weekly PDF summary of stress trends | | |
| | **Multi-language support** | i18n with `react-i18next` — translate UI strings and TTS language | | |
| | **Community / anonymous sharing** | Opt-in feed where users share their coping strategies anonymously | | |
| | **Therapist portal** | Separate role for mental-health professionals to view consented patient dashboards | | |
| | **Mobile app** | Wrap the React frontend with Capacitor or React Native for iOS/Android | | |
| | **Offline mode** | Service Worker caches the app shell; assessments queue and sync when back online | | |
| | **Biometric login** | WebAuthn passkey support — log in with Face ID or fingerprint | | |