Skip to content
Open
Show file tree
Hide file tree
Changes from 28 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
# Final Project

Replace this readme with your own information about your project.
Avoiding building similair projects to bootcamp, create something new using the tools we have learned, while alowing us to deep-dive into them or related tools.

Start by briefly describing the assignment in a sentence or two. Keep it short and to the point.
A responsive React (Vite + Tailwind) app with Clerk auth and Zustand state offers a Clerk-hosted Login landing page, an AI Chat home, and a Mood History page with chat-bubble UI. A Node/Express + MongoDB backend stores per-user messages and moods, calls OpenAI gpt-4o-mini, and tailors replies using recent chat plus mood history. The frontend is deployed on Netlify, the backend on Render, with CORS limited to site origins and secrets kept in environment variables.

## The problem

Describe how you approached to problem, and what tools and techniques you used to solve it. How did you plan? What technologies did you use? If you had more time, what would be next?

Problem: people feeling alone and have noone to talk to.
Solution: creating an AI to chat with in a neutral and safe space.
Tools: React, Vite, Clerk, Tailwind, Zustand, custom hooks, node/express, openAI, Atlas, MongoDB, Netlify, Render
Next: linking to health resources nearby, language support, dark theme, etc.

## View it live

Every project should be deployed somewhere. Be sure to include the link to the deployed project so that the viewer can click around and see what it's all about.
Netlify: https://project-final-oscar.netlify.app/
Render: https://project-final-itk1.onrender.com/

GET / → “Mindful Chat API” (ping)
GET /health → { ok: true, ts }å
GET /messages → chat history (auth required)
POST /chat → send a message; returns { userMessage, assistantMessage } (auth required)
GET /moods → list recent moods (auth required)
POST /moods → add mood { mood, note? } (auth required)
15 changes: 11 additions & 4 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,16 @@
"@babel/core": "^7.17.9",
"@babel/node": "^7.16.8",
"@babel/preset-env": "^7.16.11",
"@clerk/clerk-react": "^5.53.2",
"@clerk/express": "^1.7.41",
"bcrypt": "^6.0.0",
"cors": "^2.8.5",
"express": "^4.17.3",
"mongoose": "^8.4.0",
"nodemon": "^3.0.1"
"dotenv": "^17.2.3",
"express": "^4.21.2",
"express-endpoints": "^1.0.0",
"mongodb": "^6.20.0",
"mongoose": "^8.19.1",
"nodemon": "^3.0.1",
"openai": "^4.104.0"
}
}
}
198 changes: 188 additions & 10 deletions backend/server.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,200 @@
import express from "express";
import cors from "cors";
import mongoose from "mongoose";
import dotenv from "dotenv";
import { clerkMiddleware, getAuth } from "@clerk/express";
import OpenAI from "openai";

dotenv.config();

const SYSTEM_PROMPT = `
You are a supportive, non-clinical mental health companion.
Primary goals: help users reflect, regulate emotions, and find next gentle steps.

SAFETY & SCOPE
- Do NOT diagnose, prescribe, or give medical/legal/financial advice.
- If user indicates imminent danger or severe self-harm risk, respond with a brief crisis message and local resources; encourage contacting emergency services. Do not argue.
- Do not confirm, amplify, or join hallucinations, delusions, conspiracies, or voices. Acknowledge the experience without validating the false content and gently suggest professional help if relevant.
- Do not provide instructions for self-harm, violence, illegal activity, or hate.

RESPECT & NEUTRALITY
- Never use or repeat slurs, hate speech, or demeaning language. If the user includes such terms, respond without repeating them and set a respectful tone.
- Do not infer or assign identity attributes (gender, pronouns, ethnicity, religion, orientation, disability, politics). Use neutral language unless the user explicitly states their preference. If the user gives pronouns, use them; otherwise avoid gendered terms.
- Do not role-play as a real person or accept a given name for yourself. Avoid personification beyond “I’m an AI companion here to support you.”
- Avoid making promises or guarantees.

CONFIDENTIALITY & LIMITS
- Remind users this is a supportive tool, not therapy or a substitute for professional care.
- Encourage seeking professional support for persistent distress, safety concerns, or diagnostic questions.

STYLE
- Warm, non-judgmental, concise. Use plain language.
- One or two short paragraphs and, when helpful, a small list of options or steps.
- Ask at most one gentle, open question at a time.
- No emojis unless the user uses them first. No medical jargon.
- If unsure, say so briefly and pivot to helpful next steps.

USE OF CONTEXT
- Use provided chat history and any mood history to personalize support (e.g., trends, recent notes). Do not over-interpret or speculate.
- If information is insufficient or uncertain, ask a clarifying question rather than assume.

REFUSALS
- If asked to do something unsafe or out of scope, briefly refuse and redirect to safer alternatives.
`.trim();

const CRISIS_RE =
/\b(kill myself|end my life|suicide|can't go on|hurt myself|harm myself|kill (him|her|them)|plan to (hurt|kill))\b/i;

const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/final-project";
mongoose.connect(mongoUrl);
mongoose.Promise = Promise;
await mongoose.connect(mongoUrl);

const port = process.env.PORT || 8080;
const app = express();
const MessageSchema = new mongoose.Schema({
userId: { type: String, index: true },
role: { type: String, enum: ["user", "assistant"], required: true },
content: { type: String, required: true },
createdAt: { type: Date, default: Date.now },
});
const Message =
mongoose.models.Message || mongoose.model("Message", MessageSchema);

app.use(cors());
const app = express();
const origins = process.env.ORIGIN
? process.env.ORIGIN.split(",")
: ["http://localhost:5173"];
app.use(cors({ origin: origins, credentials: true }));
app.use(express.json());

app.get("/", (req, res) => {
res.send("Hello Technigo!");
// 👇 Provide both keys explicitly
app.use(
clerkMiddleware({
publishableKey: process.env.CLERK_PUBLISHABLE_KEY,
secretKey: process.env.CLERK_SECRET_KEY,
})
);

const oai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

app.get("/health", (_req, res) => res.json({ ok: true, ts: Date.now() }));
app.get("/", (_req, res) => res.send("Mindful Chat API"));

const ensureAuth = (req, res) => {
const { userId } = getAuth(req);
if (!userId) {
res.status(401).json({ error: "Unauthorized" });
return null;
}
return userId;
};

app.get("/messages", async (req, res) => {
const userId = ensureAuth(req, res);
if (!userId) return;
const msgs = await Message.find({ userId }).sort({ createdAt: 1 }).limit(500);
res.json(msgs);
});

// ➕ model (put near MessageSchema)
const MoodSchema = new mongoose.Schema({
userId: { type: String, index: true },
mood: { type: String, required: true }, // 😀🙂😐😕😢
note: { type: String, default: "" },
date: { type: Date, default: Date.now },
});
const Mood = mongoose.models.Mood || mongoose.model("Mood", MoodSchema);

// ➕ moods endpoints
app.get("/moods", async (req, res) => {
const userId = ensureAuth(req, res);
if (!userId) return;
const items = await Mood.find({ userId }).sort({ date: -1 }).limit(100);
res.json(items);
});

app.post("/moods", async (req, res) => {
const userId = ensureAuth(req, res);
if (!userId) return;
const { mood, note } = req.body;
if (!mood) return res.status(400).json({ error: "mood is required" });
const doc = await Mood.create({ userId, mood, note });
res.status(201).json(doc);
});

// Start the server
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
app.post("/chat", async (req, res) => {
const userId = ensureAuth(req, res);
if (!userId) return;
try {
const { content } = req.body;
if (!content?.trim())
return res.status(400).json({ error: "content is required" });

const userMessage = await Message.create({ userId, role: "user", content });

const recentMsgs = await Message.find({ userId })
.sort({ createdAt: -1 })
.limit(20)
.lean();
const history = recentMsgs
.reverse()
.map((m) => ({ role: m.role, content: m.content }));

const recentMoods = await Mood.find({ userId })
.sort({ date: -1 })
.limit(10)
.lean();
const moodSummary = recentMoods
.map(
(m) =>
`${new Date(m.date).toLocaleDateString()}: ${m.mood}${
m.note ? ` – ${m.note}` : ""
}`
)
.join("\n");

if (CRISIS_RE.test(content)) {
const assistantMessage = await Message.create({
userId,
role: "assistant",
content: CRISIS_REPLY,
});
return res.json({
userMessage: { role: "user", content },
assistantMessage,
});
}

const completion = await oai.chat.completions.create({
model: "gpt-4o-mini",
temperature: 0.2,
messages: [
{ role: "system", content: SYSTEM_PROMPT },
{
role: "system",
content: moodSummary
? `User's recent mood history (most recent first):\n${moodSummary}`
: "No mood history available yet.",
},
...history,
{ role: "user", content },
],
});

const reply =
completion.choices?.[0]?.message?.content ||
"I'm here with you. How are you feeling right now?";
const assistantMessage = await Message.create({
userId,
role: "assistant",
content: reply,
});

res.json({ userMessage, assistantMessage });
} catch (err) {
console.error("POST /chat error:", err);
res.status(500).json({ error: "Failed to generate reply" });
}
});

const port = process.env.PORT || 8080;
app.listen(port, () =>
console.log(`API listening on http://localhost:${port}`)
);
65 changes: 62 additions & 3 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,70 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />

<!-- Styles -->
<link rel="stylesheet" href="/src/App.css" />
<link rel="stylesheet" href="/src/Themes.css" />

<!-- Favicons (PNG for best compatibility) -->
<link
rel="icon"
type="image/png"
sizes="64x64"
href="/assets/icon-64.png"
/>
<link
rel="icon"
type="image/png"
sizes="192x192"
href="/assets/icon-192.png"
/>
<link
rel="icon"
type="image/png"
sizes="512x512"
href="/assets/icon-512.png"
/>
<link rel="apple-touch-icon" href="/assets/icon-192.png" />

<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Technigo React Vite Boiler Plate</title>
<title>Oscars Project — Mindful Chat</title>

<!-- Basic SEO -->
<meta
name="description"
content="A gentle, supportive chat to help you reflect on your thoughts and feelings. Sign in to start a private conversation."
/>
<meta name="theme-color" content="#ae9dec" />

<!-- Open Graph (Facebook, LinkedIn) -->
<meta property="og:type" content="website" />
<meta
property="og:title"
content="Mindful Chat — AI-powered mental health companion"
/>
<meta
property="og:description"
content="Supportive, non-clinical AI chat with Clerk sign-in and private message history."
/>
<!-- You can keep using your mockup as the preview image -->
<meta property="og:image" content="/assets/mockups/chat-mock.png" />
<!-- Replace with your real URL after deploy -->
<meta property="og:url" content="https://example.com/" />

<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta
name="twitter:title"
content="Mindful Chat — AI-powered mental health companion"
/>
<meta
name="twitter:description"
content="Supportive, non-clinical AI chat with Clerk sign-in and private message history."
/>
<meta name="twitter:image" content="/assets/mockups/chat-mock.png" />
</head>
<body>
<body style="background: var(--color-background); color: var(--color-text)">
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
Expand Down
14 changes: 13 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,29 @@
"preview": "vite preview"
},
"dependencies": {
"@clerk/clerk-react": "^5.7.0",
"@tailwindcss/vite": "^4.1.11",
"axios": "^1.7.7",
"bcrypt": "^6.0.0",
"dayjs": "^1.11.18",
"dotenv": "^17.2.1",
"express-endpoints": "^1.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"react-router-dom": "^6.27.0",
"zustand": "^4.5.4"
},
"devDependencies": {
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@vitejs/plugin-react": "^4.0.3",
"autoprefixer": "^10.4.21",
"eslint": "^8.45.0",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.11",
"vite": "^6.3.5"
}
}
Binary file added frontend/public/assets/icon-192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/public/assets/icon-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/public/assets/icon-64.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/public/assets/mockups/chat-mock.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/public/assets/mockups/login-mock.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/public/icon-192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/public/icon-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/public/icon-64.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
9 changes: 9 additions & 0 deletions frontend/src/API.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import axios from "axios";

export const API = axios.create({
baseURL: import.meta.env.VITE_API_URL,
});

export const withAuth = async (getToken) => ({
headers: { Authorization: `Bearer ${await getToken()}` },
});
1 change: 1 addition & 0 deletions frontend/src/App.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import "tailwindcss";
Loading