A survey collection client. It supports two chat modes ("One-Shot" and "Conversational"), keeps API keys server-side behind provider modules, and syncs user sessions to a PostgreSQL database as they go.
graph TD
User([User Browser]) -->|1. Consent on Welcome page| Welcome[Welcome Page]
Welcome -->|2. Academic Details| Ident[Identification Page]
Ident -->|3. View Rules| Instructions[Instructions Page]
Instructions -->|4. Unlock Chat| MainUI[Chat Workspace]
MainUI -->|5. Toggle Tabs| ModeSwitch{Tab Mode?}
ModeSwitch -->|One-Shot| OneShot[One-Shot Chat: Disabled input after 1st query]
ModeSwitch -->|Conversational| Conversational[Conversational Chat: Multi-turn]
OneShot -->|6. Send Message| ChatStream[SSE Chat Stream]
Conversational -->|6. Send Message| ChatStream
ChatStream -->|7. POST /api/chat| FastAPIServer[FastAPI Proxy Server]
FastAPIServer -->|8. Call Provider Services| ProviderService[LLM Service Orchestrator]
ProviderService -->|9. Forward SSE Stream| Providers[Gemini, ChatGPT, DeepSeek, or Claude APIs]
Providers -->|10. Relay Chunks| FastAPIServer
FastAPIServer -->|11. Render Markdown Chunks| User
User -->|12. POST /api/sync| SyncAPI[FastAPI Sync Route]
SyncAPI -->|13. Verify & Create Table| PostgresDB[(PostgreSQL Database)]
PostgresDB -->|14. Upsert ON CONFLICT| PostgresDB
Onboarding Users go through welcome → identification → instructions → chat, each its own FastAPI route with guards so you can't skip ahead to the chat window. We hash the (lowercased) Penn State ID with SHA-256 to identify each survey response. Progress gets saved to local storage so a refresh doesn't wipe out someone's session, and the major field autocompletes against a list of Penn State engineering majors.
Two chat modes One-Shot locks the input after a single message goes out; no follow-ups. Conversational is just normal multi-turn chat. You can flip between the two tabs without losing what's already been typed in either one.
Provider layer Chat, survey, and page routes are split into their own FastAPI routers rather than one giant file. Each model provider (Gemini, OpenAI, DeepSeek, Anthropic) lives in its own module, and if a given API key isn't set, that provider just falls back gracefully instead of breaking things.
Database sync
database.py handles the connection pool and makes sure tables exist. Sessions get upserted (ON CONFLICT DO UPDATE) so Postgres stays current as people use the app.
Copy in one place
All the UI text headings, rules, labels, placeholders lives in strings.json, so you can tweak wording without touching any markup.
Mobile-friendly Tuned viewport and touch targets, since most people are hitting this from a QR code on a poster.
GEMINI_API_KEY=your_gemini_api_key
OPENAI_API_KEY=your_openai_api_key
DEEPSEEK_API_KEY=your_deepseek_api_key
ANTHROPIC_API_KEY=your_anthropic_api_key
DATABASE_URL=postgresql://username:password@host.docker.internal:5432/database_name# Build the image
docker build -t gemini-chat-client .
# Run the container
docker run -d \
--name gemini-chat-container \
-p 8080:8080 \
--env-file .env \
-v $(pwd):/usr/src/app \
gemini-chat-clientThen just open http://localhost:8080.