A supportive mental-health check-in chatbot. This app is not a substitute for professional mental health care — it's a conversational companion, with a deterministic safety check for crisis language that runs independently of the model on every message.
This app is bring-your-own-key: it has no shared OpenAI key of its own. Each visitor pastes their own OpenAI API key into the app (via the control in the top banner) before the assistant will respond to normal messages — the crisis safety check works with no key at all, for anyone.
npm install
npm run devOpen http://localhost:3000, then paste your own OpenAI API key into the banner at the top of the page.
Conversations are ephemeral: chat history is never stored server-side or in
localStorage, so refreshing the page clears it. The one exception is your
OpenAI API key itself, which is saved in your browser's localStorage so you
don't have to re-enter it every visit — see components/ApiKeyProvider.tsx
for the tradeoffs of that choice.
Next.js (App Router, TypeScript) frontend with one backend API route that
proxies to OpenAI server-side — each visitor's own API key is relayed
through this route per-request (never stored server-side, never logged) so
the deterministic crisis pre-check can keep running in front of every
message regardless of whether a key is present. Chat history lives only in
React state, no database; the API key itself lives in the browser's
localStorage (see components/ApiKeyProvider.tsx).
Browser (ChatContainer + your API key) → POST /api/chat → safety pre-check → OpenAI API (your key) → response → Browser
| File | Role |
|---|---|
components/ChatContainer.tsx |
Owns chat state (messages, isLoading, error), sends the full history plus your API key to /api/chat on each send |
components/MessageList.tsx / MessageBubble.tsx / MessageInput.tsx |
Rendering: message list with auto-scroll, individual bubbles (styled differently for safety responses), the text input |
components/DisclaimerBanner.tsx |
Always-visible banner with crisis hotline numbers and the language/API-key controls, mounted in app/layout.tsx so it's on every screen |
components/ApiKeyProvider.tsx / ApiKeySettings.tsx |
Where your OpenAI API key is entered, stored (localStorage), and read from |
app/api/chat/route.ts |
The only backend logic: validates the request, runs the safety check, calls OpenAI with the visitor-supplied key, maps errors to friendly messages |
lib/safety.ts |
Regex-based crisis-language detector (English and Spanish) — runs before any OpenAI call, independent of whether an API key is present. If it matches, returns a hardcoded response with 988/741741 and skips the API entirely |
lib/systemPrompt.ts |
The instructions given to the model: no diagnosing, encourage professional help for serious concerns, stay conversational, respond in whichever language the user writes in, optionally offer grounding techniques if someone describes rumination |
lib/openai.ts |
Builds an OpenAI client from the per-request visitor-supplied key; OPENAI_MODEL (optional) still comes from env |
lib/i18n.ts |
English/Spanish UI string dictionary |
lib/rateLimit.ts |
Best-effort per-IP rate limiting on /api/chat |
lib/types.ts |
Shared TypeScript types for the request/response shape |
Design notes
- Two independent safety layers — the regex check and the system prompt don't hand off to each other; both run on every turn, so a gap in one doesn't leave the user unprotected.
- Stateless API route — the client sends the whole conversation each time, so the server holds nothing between requests; "refresh clears everything" follows directly from that rather than needing separate cleanup logic.
- Synchronous (non-streaming) responses for v1 — simpler client code and an easier-to-reason-about safety-critical path, at the cost of replies appearing all at once rather than typed out.
scripts/ contains an unrelated standalone research tool
(nature_mh_scraper.py) for finding recent Nature Mental Health
publications — not part of the chatbot app.