import type { KeyboardEventHandler } from "react"; import { useEffect, useMemo, useRef, useState } from "react"; import type { User } from "firebase/auth"; import { AuthPanel } from "./components/AuthPanel"; import { isFirebaseConfigured } from "./lib/firebase"; import { signInWithGoogle, signInWithEmail, signOutUser, signUpWithEmail, subscribeToAuthChanges, } from "./services/auth"; import { appendMessageToConversation, clearConversationMessages, createConversation, deleteConversation, listConversations, loadConversationMessages, } from "./services/chatHistory"; import type { ChatMessage, ChatMeta, ChatResponse, ConversationSummary, } from "./types/chat"; import "./App.css"; import appLogo from "../images/Logo.png"; type SectionKey = "home" | "about" | "features" | "why" | "support"; type AuthMode = "signin" | "signup"; type SymptomType = | "pain_cramps" | "late_period" | "heavy_bleeding" | "normal_discharge" | "mood_swings"; type SymptomCheckerState = { symptomType: SymptomType; duration: string; severity: "mild" | "moderate" | "severe"; affectsSchoolOrSleep: boolean; fever: boolean; faintingOrDizziness: boolean; veryHeavyBleeding: boolean; itchingOrBurning: boolean; strongSmell: boolean; unusualColor: boolean; overthinkingOrPanic: boolean; notes: string; }; const symptomTypeLabels: Record = { pain_cramps: "Cramps or period pain", late_period: "Late or missed period", heavy_bleeding: "Heavy bleeding", normal_discharge: "Discharge or smell changes", mood_swings: "Mood changes", }; const initialSymptomCheckerState: SymptomCheckerState = { symptomType: "pain_cramps", duration: "", severity: "moderate", affectsSchoolOrSleep: false, fever: false, faintingOrDizziness: false, veryHeavyBleeding: false, itchingOrBurning: false, strongSmell: false, unusualColor: false, overthinkingOrPanic: false, notes: "", }; const initialBotMessage: ChatMessage = { role: "bot", text: "Hi, I'm EmpowerHer. I'm here to listen and help with questions about periods, moods, or anything you're feeling. What's on your mind today?", }; function Bubble({ role, text, meta }: ChatMessage) { const isUrgent = meta?.escalation_level === "urgent" || meta?.escalation_level === "critical"; return (
{text}
{role === "bot" && isUrgent && (
{meta?.escalation_level === "critical" ? "Urgent safety action needed" : "Red-flag symptoms detected"} Tell a trusted adult and seek in-person care instead of relying only on chat.
)} {role === "bot" && meta && (
Intent: {meta.intent ?? "-"} Topic: {meta.topic ?? "-"} Emotions:{" "} {meta.emotions && meta.emotions.length ? meta.emotions.join(", ") : "-"} KB:{" "} {meta.kb_sources && meta.kb_sources.length ? meta.kb_sources.join(", ") : "-"} Escalation: {meta.escalation_level ?? "-"} Flags:{" "} {meta.escalation_reasons && meta.escalation_reasons.length ? meta.escalation_reasons.join(", ") : "-"}
)}
); } function hasUserConversation(history: ChatMessage[]) { return history.some((message) => message.role === "user"); } function createConversationId() { return `conversation-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; } function buildSymptomCheckerPrompt(form: SymptomCheckerState) { const details: string[] = []; details.push(`Main issue: ${symptomTypeLabels[form.symptomType]}.`); details.push(`Severity: ${form.severity}.`); if (form.duration.trim()) { details.push(`Duration: ${form.duration.trim()}.`); } if (form.affectsSchoolOrSleep) { details.push("It affects school, sleep, or normal activities."); } if (form.fever) { details.push("I also have fever or feel very unwell."); } if (form.faintingOrDizziness) { details.push("I feel dizzy, faint, or nearly faint."); } if (form.veryHeavyBleeding) { details.push("Bleeding is very heavy or I am soaking pads quickly."); } if (form.itchingOrBurning) { details.push("There is itching or burning."); } if (form.strongSmell) { details.push("There is a strong or unusual smell."); } if (form.unusualColor) { details.push("The discharge color is unusual, such as yellow or green."); } if (form.overthinkingOrPanic) { details.push("I feel panicky, overwhelmed, or I cannot calm down."); } if (form.notes.trim()) { details.push(`Extra details: ${form.notes.trim()}.`); } return `Symptom checker summary: ${details.join(" ")} Please tell me what this could mean, what I can do safely at home, and whether I should tell a trusted adult or go to a clinic.`; } function buildSummary(conversationId: string, messages: ChatMessage[]): ConversationSummary { const firstUser = messages.find((message) => message.role === "user"); const lastMessage = messages[messages.length - 1]; const titleSource = firstUser?.text || "New conversation"; const previewSource = lastMessage?.text || ""; return { id: conversationId, title: titleSource.length > 48 ? `${titleSource.slice(0, 48).trim()}...` : titleSource, preview: previewSource.length > 80 ? `${previewSource.slice(0, 80).trim()}...` : previewSource, messageCount: messages.length, }; } export default function App() { const [booting, setBooting] = useState(true); const [showLanding, setShowLanding] = useState(true); const [chatView, setChatView] = useState<"chat" | "medical">("chat"); const [messages, setMessages] = useState([initialBotMessage]); const [input, setInput] = useState(""); const [loading, setLoading] = useState(false); const [user, setUser] = useState(null); const [authBusy, setAuthBusy] = useState(false); const [authReady, setAuthReady] = useState(!isFirebaseConfigured); const [historyReady, setHistoryReady] = useState(!isFirebaseConfigured); const [authMessage, setAuthMessage] = useState(null); const [symptomChecker, setSymptomChecker] = useState( initialSymptomCheckerState ); const [conversationSummaries, setConversationSummaries] = useState< ConversationSummary[] >([]); const [activeConversationId, setActiveConversationId] = useState( null ); const endRef = useRef(null); const messagesRef = useRef([initialBotMessage]); const saveQueueRef = useRef>>(new Map()); const sectionRefs: Record> = { home: useRef(null), about: useRef(null), features: useRef(null), why: useRef(null), support: useRef(null), }; const suggestions = useMemo( () => [ "My period is late and I'm scared.", "How can I reduce cramps naturally?", "Is it okay to eat ice cream during periods?", "Why does my period smell fishy?", "Help me calm down, I'm overthinking.", ], [] ); const latestBotMeta = [...messages] .reverse() .find((message) => message.role === "bot" && message.meta)?.meta; useEffect(() => { messagesRef.current = messages; endRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages, loading]); useEffect(() => { const timer = window.setTimeout(() => setBooting(false), 1200); return () => window.clearTimeout(timer); }, []); useEffect(() => { if (!isFirebaseConfigured) { return undefined; } const unsubscribe = subscribeToAuthChanges(async (nextUser) => { setUser(nextUser); setAuthReady(true); setHistoryReady(false); setAuthMessage(null); if (!nextUser) { setConversationSummaries([]); setActiveConversationId(null); setMessages([initialBotMessage]); setHistoryReady(true); return; } try { const summaries = await listConversations(nextUser.uid); if (summaries.length > 0) { setConversationSummaries(summaries); setActiveConversationId(summaries[0].id); const remoteMessages = await loadConversationMessages( nextUser.uid, summaries[0].id ); setMessages(remoteMessages.length > 0 ? remoteMessages : [initialBotMessage]); } else if (hasUserConversation(messagesRef.current)) { const conversationId = createConversationId(); await createConversation(nextUser.uid, conversationId, messagesRef.current); setConversationSummaries([buildSummary(conversationId, messagesRef.current)]); setActiveConversationId(conversationId); } else { setConversationSummaries([]); setActiveConversationId(null); setMessages([initialBotMessage]); } } catch (error) { console.error(error); setAuthMessage( "Authentication worked, but loading saved messages failed. Check Firestore setup and security rules." ); } finally { setHistoryReady(true); } }); return unsubscribe; }, []); const scrollToSection = (key: SectionKey) => { sectionRefs[key].current?.scrollIntoView({ behavior: "smooth", block: "start", }); }; const upsertSummary = (summary: ConversationSummary) => { setConversationSummaries((prev) => [ summary, ...prev.filter((item) => item.id !== summary.id), ]); }; const persistConversationMessage = ( conversationId: string, message: ChatMessage, nextMessages: ChatMessage[] ) => { if (!user) { return Promise.resolve(); } const summary = buildSummary(conversationId, nextMessages); upsertSummary(summary); const previousTask = saveQueueRef.current.get(conversationId) ?? Promise.resolve(); const nextTask = previousTask .catch(() => undefined) .then(async () => { await appendMessageToConversation( user.uid, conversationId, message, nextMessages ); }) .catch((error) => { console.error(error); setAuthMessage( "Signed in, but Firestore could not save this conversation. Check database rules and indexes." ); }); saveQueueRef.current.set(conversationId, nextTask); return nextTask; }; const ensureActiveConversation = async (seedMessages: ChatMessage[]) => { if (!user) { return null; } if (activeConversationId) { return activeConversationId; } const conversationId = createConversationId(); try { await createConversation(user.uid, conversationId, seedMessages); setActiveConversationId(conversationId); upsertSummary(buildSummary(conversationId, seedMessages)); return conversationId; } catch (error) { console.error(error); setAuthMessage( "Could not create a new saved conversation. Check Firestore setup." ); return null; } }; const startNewConversation = async () => { setChatView("chat"); setAuthMessage(null); if (!user) { setMessages([initialBotMessage]); return; } const conversationId = createConversationId(); try { await createConversation(user.uid, conversationId, [initialBotMessage]); setActiveConversationId(conversationId); setMessages([initialBotMessage]); upsertSummary(buildSummary(conversationId, [initialBotMessage])); saveQueueRef.current.set(conversationId, Promise.resolve()); } catch (error) { console.error(error); setAuthMessage("Could not create a new saved conversation."); } }; const switchConversation = async (conversationId: string) => { if (!user || conversationId === activeConversationId) { return; } setHistoryReady(false); setAuthMessage(null); setMessages([initialBotMessage]); try { const nextMessages = await loadConversationMessages(user.uid, conversationId); setActiveConversationId(conversationId); setMessages(nextMessages.length > 0 ? nextMessages : [initialBotMessage]); setChatView("chat"); } catch (error) { console.error(error); setAuthMessage("Could not open that saved conversation."); } finally { setHistoryReady(true); } }; const clearChat = async () => { setMessages([initialBotMessage]); setChatView("chat"); if (!user || !activeConversationId) { return; } try { await clearConversationMessages(user.uid, activeConversationId, [ initialBotMessage, ]); upsertSummary(buildSummary(activeConversationId, [initialBotMessage])); setAuthMessage("Saved conversation cleared."); } catch (error) { console.error(error); setAuthMessage("Could not clear saved chat history from Firestore."); } }; const removeConversation = async () => { if (!user || !activeConversationId) { return; } const conversationId = activeConversationId; const previousMessages = messagesRef.current; const previousSummaries = conversationSummaries; const remainingSummaries = conversationSummaries.filter( (summary) => summary.id !== conversationId ); const nextConversationId = remainingSummaries.length > 0 ? remainingSummaries[0].id : null; setHistoryReady(false); setMessages([initialBotMessage]); setConversationSummaries(remainingSummaries); setActiveConversationId(nextConversationId); try { await deleteConversation(user.uid, conversationId); saveQueueRef.current.delete(conversationId); if (nextConversationId) { const nextMessages = await loadConversationMessages( user.uid, nextConversationId ); setMessages(nextMessages.length > 0 ? nextMessages : [initialBotMessage]); } setAuthMessage("Conversation deleted."); } catch (error) { console.error(error); setAuthMessage("Could not delete the selected conversation."); setConversationSummaries(previousSummaries); setActiveConversationId(conversationId); setMessages(previousMessages); } finally { setHistoryReady(true); } }; const updateSymptomChecker = ( key: K, value: SymptomCheckerState[K] ) => { setSymptomChecker((prev) => ({ ...prev, [key]: value })); }; const submitSymptomChecker = async () => { await sendMessage(buildSymptomCheckerPrompt(symptomChecker)); }; const sendMessage = async (text?: string) => { const msg = (text ?? input).trim(); if (!msg || loading) return; const currentMessages = messagesRef.current; const userMessage: ChatMessage = { role: "user", text: msg }; const optimisticMessages = [...currentMessages, userMessage]; const historyForRequest = currentMessages.slice(-6); setMessages(optimisticMessages); setInput(""); setLoading(true); const conversationId = await ensureActiveConversation(currentMessages); if (conversationId) { void persistConversationMessage( conversationId, userMessage, optimisticMessages ); } try { const res = await fetch("/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message: msg, history: historyForRequest, }), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data: ChatResponse = await res.json(); const reply = data.reply?.trim() || "Sorry, I could not generate a reply right now."; const meta: ChatMeta = { intent: data.intent ?? undefined, topic: data.topic ?? undefined, emotions: Array.isArray(data.emotions) ? data.emotions : undefined, kb_sources: Array.isArray(data.kb_sources) ? data.kb_sources : undefined, escalation_level: data.escalation_level ?? undefined, escalation_reasons: Array.isArray(data.escalation_reasons) ? data.escalation_reasons : undefined, }; const botMessage: ChatMessage = { role: "bot", text: reply, meta }; const finalMessages = [...optimisticMessages, botMessage]; setMessages(finalMessages); if (conversationId) { void persistConversationMessage( conversationId, botMessage, finalMessages ); } } catch (error) { console.error(error); const fallbackBotMessage: ChatMessage = { role: "bot", text: "Oops, I could not connect to the backend. Is Flask running on http://127.0.0.1:5000 ?", }; const fallbackMessages = [...optimisticMessages, fallbackBotMessage]; setMessages(fallbackMessages); if (conversationId) { void persistConversationMessage( conversationId, fallbackBotMessage, fallbackMessages ); } } finally { setLoading(false); } }; const handleEmailAuth = async ( mode: AuthMode, email: string, password: string ) => { setAuthBusy(true); setAuthMessage(null); try { if (mode === "signup") { await signUpWithEmail(email, password); setAuthMessage("Account created. Your chat history will now be saved."); } else { await signInWithEmail(email, password); setAuthMessage("Signed in. Saved chat history is available on this device."); } } catch (error) { console.error(error); setAuthMessage( error instanceof Error ? error.message : "Authentication failed." ); } finally { setAuthBusy(false); } }; const handleGoogleAuth = async () => { setAuthBusy(true); setAuthMessage(null); try { await signInWithGoogle(); setAuthMessage("Signed in with Google."); } catch (error) { console.error(error); setAuthMessage( error instanceof Error ? error.message : "Google sign-in failed." ); } finally { setAuthBusy(false); } }; const handleSignOut = async () => { setAuthBusy(true); setAuthMessage(null); try { await signOutUser(); setAuthMessage("Signed out. Guest chats will stay local only."); } catch (error) { console.error(error); setAuthMessage(error instanceof Error ? error.message : "Could not sign out."); } finally { setAuthBusy(false); } }; const onKeyDown: KeyboardEventHandler = (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); void sendMessage(); } }; const statusText = !isFirebaseConfigured ? "Firebase is not configured yet. Chat works in guest mode only." : !authReady || !historyReady ? "Checking account and saved conversation..." : user ? `Signed in as ${user.email ?? "Google user"}` : "Guest mode. Sign in to save chat history."; if (booting) { return (
EmpowerHer logo

EmpowerHer

Preparing your safe support space...

); } if (showLanding) { return (

A safe and supportive

Menstrual Health Chatbot

for Adolescents

Private menstrual health support with emotionally supportive responses, clear guidance, and a safe space to ask sensitive questions.

EmpowerHer provides private, emotionally supportive, and easy-to-understand menstrual health support for adolescents and young women seeking safe digital guidance.

Why EmpowerHer Matters

EmpowerHer was created for adolescents who may feel shy, anxious, or unsupported when seeking menstrual health information.

🔒

Private and Safe

A judgment-free place where users can ask sensitive questions without worrying about privacy or stigma.

💞

Emotionally Supportive

Responses are designed to acknowledge fear, stress, confusion, and other emotional needs around menstruation.

📘

Educational

Reliable, age-appropriate menstrual health guidance using a curated local knowledge base.

EmpowerHer support illustration

What EmpowerHer Offers

  • 24/7 chatbot support Ask menstrual health questions anytime in a private space.
  • Emotional reassurance Get supportive responses that validate feelings and reduce fear.
  • Health education Learn about periods, symptoms, hygiene, food, and warning signs.
  • Confidence building Understand your body with simple explanations designed for adolescents.

Who It Is For

EmpowerHer is designed for young users seeking trustworthy menstrual health support.

👩

Adolescents

Young women starting their menstrual health journey.

🛡️

Privacy Seekers

Users who prefer anonymous and supportive health conversations.

📝

Learners

Anyone who wants reliable menstrual health information in simple language.

💬

Support Seekers

Users who want emotional reassurance alongside health guidance.

Get Support

Start chatting with EmpowerHer for private menstrual health support, period guidance, and gentle reassurance.

EmpowerHer provides general support and education. It does not replace medical care.

Why Join Us?

A private digital space built for menstrual health confidence, awareness, and emotional support.

Private chat support Ask questions without fear of judgment or embarrassment.
Educational resources Access guidance about periods, symptoms, hygiene, and healthy habits.
Safe reassurance Receive calm responses designed for adolescents and young women.
Always available Use the chatbot whenever you need support, day or night.

EmpowerHer

A safe, private, and emotionally supportive menstrual health chatbot designed to help adolescents and young women understand their bodies and receive reliable guidance.

Quick Links

Follow Us

Facebook Instagram Twitter (X) LinkedIn YouTube
Copyright 2026 EmpowerHer. A final year project dedicated to adolescent menstrual health support.
); } return (
EmpowerHer logo

EmpowerHer

Private chat support for periods, moods, and menstrual health questions.

{chatView === "medical" && ( )}
{chatView === "medical" ? (

Medical Information

General menstrual health guidance about doctors, clinics, hospitals, and when you should seek urgent help.

When to See a Doctor or Clinic

  • Periods are missing for several months or are repeatedly very irregular.
  • Cramps are severe or stop you from school, sleep, or normal activities.
  • You have unusual discharge, strong smell, itching, burning, or pain.
  • You feel worried about changes in bleeding, smell, or cycle timing.

When to Go to a Hospital Urgently

  • Very heavy bleeding that soaks pads quickly.
  • Fainting, severe dizziness, chest pain, or extreme weakness.
  • Strong pain with fever, vomiting, or feeling very unwell.
  • Any situation where you feel unsafe or cannot manage at home.

What to Tell the Doctor

  • How long symptoms have been happening.
  • How heavy the bleeding is and whether there are clots.
  • If there is pain, fever, dizziness, smell, itching, or unusual discharge.
  • Your age, the date of your last period, and what makes symptoms worse or better.

Finding Help

If symptoms are worrying, speak to a trusted adult, school nurse, clinic, family doctor, gynecology service, or the nearest hospital. For urgent symptoms, do not wait for the chatbot.

Menstrual Health / Gynecology Doctors (Sri Lanka)

1. Dr. Nadira Dassanayake

Obstetrics and Gynecology

Focus: Menstrual disorders, PCOS, fertility

Hospitals: Asiri Hospital, Nawaloka Hospital

Why important: Well-known consultant for women's reproductive health

2. Dr. Harsha Atapattu

Consultant Obstetrician and Gynecologist

Focus: Hormonal issues, menstrual irregularities, adolescent gynecology

Hospitals: Lanka Hospitals

Strength: Strong experience with teenage and young women's health

3. Dr. Rishya Manikavasagar

Obstetrics and Gynecology

Focus: Menstrual pain, reproductive health, counseling

Hospitals: Durdans Hospital

Why useful: Good for emotional and physical health combined care

4. Dr. Kapila Jayaratne

Obstetrics and Gynecology

Focus: PCOS, menstrual cycle management, fertility

Hospitals: Ninewells Hospital

Highlight: Popular among young women

5. Dr. Shiromi Maduwage

Women's health and pregnancy care

Focus: Menstrual health education, reproductive wellbeing

Hospitals: Castle Street Hospital for Women

Why important: Government sector expertise

Government Clinics and Public Health Support

  • Family Planning Association of Sri Lanka offers menstrual health advice, clinics, and education support.
  • Ministry of Health Sri Lanka provides public health services, education programs, and government clinic access.
  • These services can be important for free or lower-cost menstrual health guidance and reproductive wellbeing support.
) : (

Get Support

Ask a question, describe a symptom, or continue a conversation in a private space.

{user ? "History syncing" : "Private and supportive"}
{statusText}
{authMessage &&
{authMessage}
} {latestBotMeta?.escalation_level && latestBotMeta.escalation_level !== "none" && (
{latestBotMeta.escalation_level === "critical" ? "Immediate safety support needed" : "Urgent medical follow-up recommended"} {latestBotMeta.escalation_reasons?.length ? `Detected: ${latestBotMeta.escalation_reasons.join(", ")}.` : "This conversation includes red-flag symptoms."}{" "} Please tell a trusted adult and seek clinic or hospital care if symptoms are severe.
)}
{messages.map((m, i) => ( ))} {loading && (
)}