EmpowerHer / FRONTEND /src /App.tsx
Disini Ruhansa Kodagoda Hettige
Update chatbot responses
23b0ad9
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<SymptomType, string> = {
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 (
<div className={`row ${role}`}>
<div className={`bubble ${role}`}>
<div className="bubbleText">{text}</div>
{role === "bot" && isUrgent && (
<div className="escalationBanner">
<strong>
{meta?.escalation_level === "critical" ? "Urgent safety action needed" : "Red-flag symptoms detected"}
</strong>
<span>
Tell a trusted adult and seek in-person care instead of relying only on chat.
</span>
</div>
)}
{role === "bot" && meta && (
<div className="meta">
<span className="chip">Intent: {meta.intent ?? "-"}</span>
<span className="chip">Topic: {meta.topic ?? "-"}</span>
<span className="chip">
Emotions:{" "}
{meta.emotions && meta.emotions.length
? meta.emotions.join(", ")
: "-"}
</span>
<span className="chip">
KB:{" "}
{meta.kb_sources && meta.kb_sources.length
? meta.kb_sources.join(", ")
: "-"}
</span>
<span className="chip">
Escalation: {meta.escalation_level ?? "-"}
</span>
<span className="chip">
Flags:{" "}
{meta.escalation_reasons && meta.escalation_reasons.length
? meta.escalation_reasons.join(", ")
: "-"}
</span>
</div>
)}
</div>
</div>
);
}
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<ChatMessage[]>([initialBotMessage]);
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
const [user, setUser] = useState<User | null>(null);
const [authBusy, setAuthBusy] = useState(false);
const [authReady, setAuthReady] = useState(!isFirebaseConfigured);
const [historyReady, setHistoryReady] = useState(!isFirebaseConfigured);
const [authMessage, setAuthMessage] = useState<string | null>(null);
const [symptomChecker, setSymptomChecker] = useState<SymptomCheckerState>(
initialSymptomCheckerState
);
const [conversationSummaries, setConversationSummaries] = useState<
ConversationSummary[]
>([]);
const [activeConversationId, setActiveConversationId] = useState<string | null>(
null
);
const endRef = useRef<HTMLDivElement | null>(null);
const messagesRef = useRef<ChatMessage[]>([initialBotMessage]);
const saveQueueRef = useRef<Map<string, Promise<void>>>(new Map());
const sectionRefs: Record<SectionKey, React.RefObject<HTMLDivElement | null>> =
{
home: useRef<HTMLDivElement | null>(null),
about: useRef<HTMLDivElement | null>(null),
features: useRef<HTMLDivElement | null>(null),
why: useRef<HTMLDivElement | null>(null),
support: useRef<HTMLDivElement | null>(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 = <K extends keyof SymptomCheckerState>(
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<HTMLTextAreaElement> = (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 (
<div className="bootScreen">
<div className="bootCard">
<div className="bootLogo">
<img src={appLogo} alt="EmpowerHer logo" />
</div>
<h1>EmpowerHer</h1>
<p>Preparing your safe support space...</p>
<div className="bootBar">
<span />
</div>
</div>
</div>
);
}
if (showLanding) {
return (
<div className="siteShell">
<header className="siteNav">
<button className="siteBrand" onClick={() => scrollToSection("home")}>
EmpowerHer
</button>
<nav className="siteLinks">
<button onClick={() => scrollToSection("home")}>Home</button>
<button onClick={() => scrollToSection("about")}>About</button>
<button onClick={() => scrollToSection("features")}>Features</button>
<button onClick={() => scrollToSection("why")}>Why EmpowerHer</button>
<button className="navCta" onClick={() => scrollToSection("support")}>
Get Support
</button>
</nav>
</header>
<section className="heroSection" ref={sectionRefs.home}>
<div className="heroOverlay" />
<div className="heroContent">
<p className="heroLead">A safe and supportive</p>
<h1>Menstrual Health Chatbot</h1>
<h2>for Adolescents</h2>
<p className="heroSubtext">
Private menstrual health support with emotionally supportive responses,
clear guidance, and a safe space to ask sensitive questions.
</p>
<div className="heroActions">
<button className="primaryBtn" onClick={() => setShowLanding(false)}>
Explore the Chatbot
</button>
<button className="secondaryBtn" onClick={() => scrollToSection("support")}>
Learn More
</button>
</div>
</div>
</section>
<section className="introBand" ref={sectionRefs.about}>
<div className="container narrow">
<p>
EmpowerHer provides private, emotionally supportive, and easy-to-understand
menstrual health support for adolescents and young women seeking safe digital guidance.
</p>
</div>
</section>
<section className="contentSection">
<div className="container">
<div className="sectionHeading">
<h2>Why EmpowerHer Matters</h2>
<p>
EmpowerHer was created for adolescents who may feel shy, anxious, or unsupported
when seeking menstrual health information.
</p>
</div>
<div className="featureGrid">
<article className="featureCard">
<div className="featureIcon">🔒</div>
<h3>Private and Safe</h3>
<p>
A judgment-free place where users can ask sensitive questions without worrying
about privacy or stigma.
</p>
</article>
<article className="featureCard">
<div className="featureIcon">💞</div>
<h3>Emotionally Supportive</h3>
<p>
Responses are designed to acknowledge fear, stress, confusion, and other emotional
needs around menstruation.
</p>
</article>
<article className="featureCard">
<div className="featureIcon">📘</div>
<h3>Educational</h3>
<p>
Reliable, age-appropriate menstrual health guidance using a curated local knowledge base.
</p>
</article>
</div>
</div>
</section>
<section className="splitSection" ref={sectionRefs.features}>
<div className="container splitLayout">
<div className="imagePanel">
<img
src="/images/Celebrating sisterhood in soft pastels.png"
alt="EmpowerHer support illustration"
/>
</div>
<div className="textPanel">
<h2>What EmpowerHer Offers</h2>
<ul className="offerList">
<li>
<strong>24/7 chatbot support</strong>
<span>Ask menstrual health questions anytime in a private space.</span>
</li>
<li>
<strong>Emotional reassurance</strong>
<span>Get supportive responses that validate feelings and reduce fear.</span>
</li>
<li>
<strong>Health education</strong>
<span>Learn about periods, symptoms, hygiene, food, and warning signs.</span>
</li>
<li>
<strong>Confidence building</strong>
<span>Understand your body with simple explanations designed for adolescents.</span>
</li>
</ul>
</div>
</div>
</section>
<section className="lavenderSection" ref={sectionRefs.why}>
<div className="container">
<div className="sectionHeading">
<h2>Who It Is For</h2>
<p>
EmpowerHer is designed for young users seeking trustworthy menstrual health support.
</p>
</div>
<div className="miniGrid">
<article className="miniCard">
<div className="miniIcon">👩</div>
<h3>Adolescents</h3>
<p>Young women starting their menstrual health journey.</p>
</article>
<article className="miniCard">
<div className="miniIcon">🛡️</div>
<h3>Privacy Seekers</h3>
<p>Users who prefer anonymous and supportive health conversations.</p>
</article>
<article className="miniCard">
<div className="miniIcon">📝</div>
<h3>Learners</h3>
<p>Anyone who wants reliable menstrual health information in simple language.</p>
</article>
<article className="miniCard">
<div className="miniIcon">💬</div>
<h3>Support Seekers</h3>
<p>Users who want emotional reassurance alongside health guidance.</p>
</article>
</div>
</div>
</section>
<section className="contentSection" ref={sectionRefs.support}>
<div className="container supportGrid">
<div className="signupCard">
<h2>Get Support</h2>
<p>
Start chatting with EmpowerHer for private menstrual health support, period guidance,
and gentle reassurance.
</p>
<div className="ctaStack">
<button className="primaryBtn wide" onClick={() => setShowLanding(false)}>
Open Chat Support
</button>
<button className="secondaryBtn wide" onClick={() => scrollToSection("features")}>
View Features
</button>
</div>
<p className="finePrint">
EmpowerHer provides general support and education. It does not replace medical care.
</p>
</div>
<div className="benefitPanel">
<h2>Why Join Us?</h2>
<p>
A private digital space built for menstrual health confidence, awareness, and emotional support.
</p>
<div className="benefitList">
<div>
<strong>Private chat support</strong>
<span>Ask questions without fear of judgment or embarrassment.</span>
</div>
<div>
<strong>Educational resources</strong>
<span>Access guidance about periods, symptoms, hygiene, and healthy habits.</span>
</div>
<div>
<strong>Safe reassurance</strong>
<span>Receive calm responses designed for adolescents and young women.</span>
</div>
<div>
<strong>Always available</strong>
<span>Use the chatbot whenever you need support, day or night.</span>
</div>
</div>
</div>
</div>
</section>
<footer className="siteFooter">
<div className="container footerGrid">
<div>
<h3>EmpowerHer</h3>
<p>
A safe, private, and emotionally supportive menstrual health chatbot designed to
help adolescents and young women understand their bodies and receive reliable guidance.
</p>
</div>
<div>
<h3>Quick Links</h3>
<button onClick={() => scrollToSection("home")}>Home</button>
<button onClick={() => scrollToSection("about")}>About</button>
<button onClick={() => scrollToSection("features")}>Features</button>
<button onClick={() => setShowLanding(false)}>Chatbot</button>
<button onClick={() => scrollToSection("support")}>Contact</button>
</div>
<div>
<h3>Follow Us</h3>
<span>Facebook</span>
<span>Instagram</span>
<span>Twitter (X)</span>
<span>LinkedIn</span>
<span>YouTube</span>
</div>
</div>
<div className="copyrightBar">
<span>Copyright 2026 EmpowerHer. A final year project dedicated to adolescent menstrual health support.</span>
</div>
</footer>
</div>
);
}
return (
<div className={`app ${chatView === "chat" ? "chatPage" : ""}`}>
<header className="topbar">
<div className="brand">
<div className="logo">
<img src={appLogo} alt="EmpowerHer logo" />
</div>
<div>
<h1>EmpowerHer</h1>
<p>Private chat support for periods, moods, and menstrual health questions.</p>
</div>
</div>
<div className="topActions">
{chatView === "medical" && (
<button className="ghost" onClick={() => setChatView("chat")}>
Back to Chat
</button>
)}
<button className="ghost" onClick={() => setShowLanding(true)}>
Back to Home
</button>
<button className="ghost" onClick={() => void clearChat()}>
Clear Chat
</button>
</div>
</header>
{chatView === "medical" ? (
<main className="medicalShell">
<section className="medicalHero card">
<h2>Medical Information</h2>
<p>
General menstrual health guidance about doctors, clinics, hospitals, and when you should
seek urgent help.
</p>
</section>
<section className="medicalGrid">
<article className="card medicalCard">
<h3>When to See a Doctor or Clinic</h3>
<ul className="medicalList">
<li>Periods are missing for several months or are repeatedly very irregular.</li>
<li>Cramps are severe or stop you from school, sleep, or normal activities.</li>
<li>You have unusual discharge, strong smell, itching, burning, or pain.</li>
<li>You feel worried about changes in bleeding, smell, or cycle timing.</li>
</ul>
</article>
<article className="card medicalCard">
<h3>When to Go to a Hospital Urgently</h3>
<ul className="medicalList">
<li>Very heavy bleeding that soaks pads quickly.</li>
<li>Fainting, severe dizziness, chest pain, or extreme weakness.</li>
<li>Strong pain with fever, vomiting, or feeling very unwell.</li>
<li>Any situation where you feel unsafe or cannot manage at home.</li>
</ul>
</article>
<article className="card medicalCard">
<h3>What to Tell the Doctor</h3>
<ul className="medicalList">
<li>How long symptoms have been happening.</li>
<li>How heavy the bleeding is and whether there are clots.</li>
<li>If there is pain, fever, dizziness, smell, itching, or unusual discharge.</li>
<li>Your age, the date of your last period, and what makes symptoms worse or better.</li>
</ul>
</article>
<article className="card medicalCard">
<h3>Finding Help</h3>
<p className="muted">
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.
</p>
<div className="medicalActions">
<button className="primaryBtn" onClick={() => setChatView("chat")}>
Back to Chat Support
</button>
</div>
</article>
<article className="card medicalCard medicalDoctors">
<h3>Menstrual Health / Gynecology Doctors (Sri Lanka)</h3>
<div className="doctorList">
<div>
<strong>1. Dr. Nadira Dassanayake</strong>
<p>Obstetrics and Gynecology</p>
<p>Focus: Menstrual disorders, PCOS, fertility</p>
<p>Hospitals: Asiri Hospital, Nawaloka Hospital</p>
<p>Why important: Well-known consultant for women's reproductive health</p>
</div>
<div>
<strong>2. Dr. Harsha Atapattu</strong>
<p>Consultant Obstetrician and Gynecologist</p>
<p>Focus: Hormonal issues, menstrual irregularities, adolescent gynecology</p>
<p>Hospitals: Lanka Hospitals</p>
<p>Strength: Strong experience with teenage and young women's health</p>
</div>
<div>
<strong>3. Dr. Rishya Manikavasagar</strong>
<p>Obstetrics and Gynecology</p>
<p>Focus: Menstrual pain, reproductive health, counseling</p>
<p>Hospitals: Durdans Hospital</p>
<p>Why useful: Good for emotional and physical health combined care</p>
</div>
<div>
<strong>4. Dr. Kapila Jayaratne</strong>
<p>Obstetrics and Gynecology</p>
<p>Focus: PCOS, menstrual cycle management, fertility</p>
<p>Hospitals: Ninewells Hospital</p>
<p>Highlight: Popular among young women</p>
</div>
<div>
<strong>5. Dr. Shiromi Maduwage</strong>
<p>Women's health and pregnancy care</p>
<p>Focus: Menstrual health education, reproductive wellbeing</p>
<p>Hospitals: Castle Street Hospital for Women</p>
<p>Why important: Government sector expertise</p>
</div>
</div>
</article>
<article className="card medicalCard">
<h3>Government Clinics and Public Health Support</h3>
<ul className="medicalList">
<li><strong>Family Planning Association of Sri Lanka</strong> offers menstrual health advice, clinics, and education support.</li>
<li><strong>Ministry of Health Sri Lanka</strong> provides public health services, education programs, and government clinic access.</li>
<li>These services can be important for free or lower-cost menstrual health guidance and reproductive wellbeing support.</li>
</ul>
</article>
</section>
</main>
) : (
<main className="main">
<section className="chatArea">
<div className="chatIntroCard">
<div className="chatIntroTop">
<div>
<h2>Get Support</h2>
<p>
Ask a question, describe a symptom, or continue a conversation in a private space.
</p>
</div>
<div className="introBadge">
{user ? "History syncing" : "Private and supportive"}
</div>
</div>
<div className="statusBar">{statusText}</div>
{authMessage && <div className="statusNote">{authMessage}</div>}
{latestBotMeta?.escalation_level &&
latestBotMeta.escalation_level !== "none" && (
<div className="escalationCard">
<strong>
{latestBotMeta.escalation_level === "critical"
? "Immediate safety support needed"
: "Urgent medical follow-up recommended"}
</strong>
<span>
{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.
</span>
</div>
)}
</div>
<div className="chatBox">
{messages.map((m, i) => (
<Bubble
key={`${m.role}-${i}-${m.text.slice(0, 8)}`}
role={m.role}
text={m.text}
meta={m.meta}
/>
))}
{loading && (
<div className="row bot">
<div className="bubble bot">
<div className="typing">
<span />
<span />
<span />
</div>
</div>
</div>
)}
<div ref={endRef} />
</div>
<div className="composer">
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={onKeyDown}
placeholder="Type your question here..."
rows={2}
disabled={!historyReady}
/>
<button
className="sendBtn"
disabled={loading || !input.trim() || !historyReady}
onClick={() => void sendMessage()}
>
Send
</button>
</div>
</section>
<aside className="sidePanel">
<AuthPanel
authBusy={authBusy}
authEnabled={isFirebaseConfigured}
authReady={authReady}
historyReady={historyReady}
user={user}
onEmailAuth={handleEmailAuth}
onGoogleAuth={handleGoogleAuth}
onSignOut={handleSignOut}
/>
{user && (
<div className="card conversationCard">
<div className="conversationHeader">
<h2>Saved Chats</h2>
<button
className="ghost conversationAction"
onClick={() => void startNewConversation()}
disabled={!historyReady || loading}
>
New
</button>
</div>
<div className="conversationList">
{conversationSummaries.length === 0 ? (
<p className="muted">
No saved chats yet. Start a new conversation and it will appear here.
</p>
) : (
conversationSummaries.map((summary) => (
<button
key={summary.id}
className={
summary.id === activeConversationId
? "conversationItem active"
: "conversationItem"
}
onClick={() => void switchConversation(summary.id)}
disabled={!historyReady || loading}
>
<strong>{summary.title}</strong>
<span>{summary.preview || "No preview yet."}</span>
<small>{summary.messageCount} messages</small>
</button>
))
)}
</div>
{activeConversationId && (
<button
className="ghost wide deleteConversationBtn"
onClick={() => void removeConversation()}
disabled={!historyReady || loading}
>
Delete Selected Chat
</button>
)}
</div>
)}
<div className="card">
<h2>Symptom Checker</h2>
<p className="muted">
Use the guided form if you want a more structured answer than free-text chat.
</p>
<div className="symptomForm">
<label className="symptomField">
<span>Main symptom</span>
<select
value={symptomChecker.symptomType}
onChange={(e) =>
updateSymptomChecker("symptomType", e.target.value as SymptomType)
}
disabled={!historyReady || loading}
>
{Object.entries(symptomTypeLabels).map(([value, label]) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
</label>
<div className="symptomSplit">
<label className="symptomField">
<span>Severity</span>
<select
value={symptomChecker.severity}
onChange={(e) =>
updateSymptomChecker(
"severity",
e.target.value as SymptomCheckerState["severity"]
)
}
disabled={!historyReady || loading}
>
<option value="mild">Mild</option>
<option value="moderate">Moderate</option>
<option value="severe">Severe</option>
</select>
</label>
<label className="symptomField">
<span>Duration</span>
<input
type="text"
value={symptomChecker.duration}
onChange={(e) => updateSymptomChecker("duration", e.target.value)}
placeholder="e.g. 2 days, 1 week"
disabled={!historyReady || loading}
/>
</label>
</div>
<div className="symptomChecks">
<label><input type="checkbox" checked={symptomChecker.affectsSchoolOrSleep} onChange={(e) => updateSymptomChecker("affectsSchoolOrSleep", e.target.checked)} disabled={!historyReady || loading} />Affects school or sleep</label>
<label><input type="checkbox" checked={symptomChecker.fever} onChange={(e) => updateSymptomChecker("fever", e.target.checked)} disabled={!historyReady || loading} />Fever or very unwell</label>
<label><input type="checkbox" checked={symptomChecker.faintingOrDizziness} onChange={(e) => updateSymptomChecker("faintingOrDizziness", e.target.checked)} disabled={!historyReady || loading} />Fainting or dizziness</label>
<label><input type="checkbox" checked={symptomChecker.veryHeavyBleeding} onChange={(e) => updateSymptomChecker("veryHeavyBleeding", e.target.checked)} disabled={!historyReady || loading} />Very heavy bleeding</label>
<label><input type="checkbox" checked={symptomChecker.itchingOrBurning} onChange={(e) => updateSymptomChecker("itchingOrBurning", e.target.checked)} disabled={!historyReady || loading} />Itching or burning</label>
<label><input type="checkbox" checked={symptomChecker.strongSmell} onChange={(e) => updateSymptomChecker("strongSmell", e.target.checked)} disabled={!historyReady || loading} />Strong smell</label>
<label><input type="checkbox" checked={symptomChecker.unusualColor} onChange={(e) => updateSymptomChecker("unusualColor", e.target.checked)} disabled={!historyReady || loading} />Yellow or green discharge</label>
<label><input type="checkbox" checked={symptomChecker.overthinkingOrPanic} onChange={(e) => updateSymptomChecker("overthinkingOrPanic", e.target.checked)} disabled={!historyReady || loading} />Panicky or overwhelmed</label>
</div>
<label className="symptomField">
<span>Extra notes</span>
<textarea
value={symptomChecker.notes}
onChange={(e) => updateSymptomChecker("notes", e.target.value)}
placeholder="Anything else important?"
rows={3}
disabled={!historyReady || loading}
/>
</label>
<button
className="primaryBtn wide"
onClick={() => void submitSymptomChecker()}
disabled={!historyReady || loading}
>
Check Symptoms
</button>
</div>
</div>
<div className="card">
<h2>Quick Prompts</h2>
<p className="muted">Tap one to start faster.</p>
<div className="suggestions">
{suggestions.map((s) => (
<button
key={s}
className="suggestionBtn"
onClick={() => void sendMessage(s)}
disabled={!historyReady || loading}
>
{s}
</button>
))}
</div>
</div>
<div className="card">
<h2>Red-Flag Escalation</h2>
<p className="muted">
If there is severe pain, fainting, fever, soaking pads quickly, chest pain, or you
feel unsafe, do not rely only on chat. Tell a trusted adult and go to a clinic or
hospital.
</p>
</div>
<div className="card compactInfo medicalShortcut">
<h2>Need Medical Info?</h2>
<p className="muted">
Read quick guidance about menstrual health, clinics, doctors, and hospital warning signs.
</p>
<button className="primaryBtn wide" onClick={() => setChatView("medical")}>
Medical Info
</button>
</div>
</aside>
</main>
)}
</div>
);
}