From cdba8325359de168b5dd2265c4f53375c99b10d7 Mon Sep 17 00:00:00 2001 From: Sumit Suthar Date: Fri, 5 Jun 2026 16:46:27 +0530 Subject: [PATCH 1/2] fix: align Personal Safety Hub styling with Legal Hub --- src/pages/Resources.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/pages/Resources.tsx b/src/pages/Resources.tsx index a50144d..ffe1265 100644 --- a/src/pages/Resources.tsx +++ b/src/pages/Resources.tsx @@ -512,10 +512,12 @@ export default function Resources() {
-
-
- -

Personal Safety Hub

+
+
+ +

+ Personal Safety Hub +

From b2fee0897a056dc474d363493749c34250e15e4f Mon Sep 17 00:00:00 2001 From: Sumit Suthar Date: Fri, 5 Jun 2026 18:14:41 +0530 Subject: [PATCH 2/2] feat: add AI-powered crisis intervention flow --- netlify/functions/classify-crisis.cjs | 27 ++++++++-- src/components/EmergencyModal.tsx | 73 +++++++++++++++++++++++++++ src/pages/Resources.tsx | 51 +------------------ src/pages/ShareStory.tsx | 37 +++++++++++--- src/utils/constants.ts | 52 +++++++++++++++++++ 5 files changed, 178 insertions(+), 62 deletions(-) create mode 100644 src/components/EmergencyModal.tsx create mode 100644 src/utils/constants.ts diff --git a/netlify/functions/classify-crisis.cjs b/netlify/functions/classify-crisis.cjs index 0f3993b..1c8cf83 100644 --- a/netlify/functions/classify-crisis.cjs +++ b/netlify/functions/classify-crisis.cjs @@ -69,25 +69,42 @@ Post to analyze: ${textToAnalyze} """ -Respond with ONLY one word: LOW, MEDIUM, or HIGH. No explanation, no punctuation, nothing else.`; +Respond ONLY with a raw JSON object formatted exactly like this, with no markdown formatting or code blocks: +{ + "riskLevel": "HIGH", + "reason": "Brief explanation of why this risk level was chosen" +}`; const result = await model.generateContent(prompt); - const rawText = result?.response?.text()?.trim().toUpperCase() || 'LOW'; + let rawText = result?.response?.text()?.trim() || ''; + + // Safely strip potential markdown code blocks + if (rawText.startsWith('```')) { + rawText = rawText.replace(/^```(json)?\n?/, '').replace(/\n?```$/, '').trim(); + } + + let parsedResult = { riskLevel: 'LOW', reason: '' }; + try { + parsedResult = JSON.parse(rawText); + } catch (parseError) { + console.error('Failed to parse JSON from AI:', rawText); + } const VALID_LEVELS = ['LOW', 'MEDIUM', 'HIGH']; - const riskLevel = VALID_LEVELS.includes(rawText) ? rawText : 'LOW'; + const riskLevel = VALID_LEVELS.includes(parsedResult.riskLevel?.toUpperCase()) ? parsedResult.riskLevel.toUpperCase() : 'LOW'; + const reason = parsedResult.reason || ''; return { statusCode: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - body: JSON.stringify({ riskLevel }), + body: JSON.stringify({ riskLevel, reason }), }; } catch (error) { console.error('Crisis classification error:', error); return { statusCode: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - body: JSON.stringify({ error: 'Classification failed', riskLevel: 'LOW' }), + body: JSON.stringify({ error: 'Classification failed', riskLevel: 'LOW', reason: '' }), }; } }; \ No newline at end of file diff --git a/src/components/EmergencyModal.tsx b/src/components/EmergencyModal.tsx new file mode 100644 index 0000000..657a12a --- /dev/null +++ b/src/components/EmergencyModal.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { X, AlertTriangle } from 'lucide-react'; +import { QUICK_HELPLINES } from '../utils/constants'; + +interface EmergencyModalProps { + isOpen: boolean; + onClose: () => void; +} + +export function EmergencyModal({ isOpen, onClose }: EmergencyModalProps) { + if (!isOpen) return null; + + return ( +
+
+ + {/* Header */} +
+
+ +

Immediate Support Needed?

+
+ +
+ + {/* Body */} +
+

+ We noticed your story mentions signs of urgent distress. Your safety is our top priority. Please do not hesitate to reach out to these free, confidential, and 24/7 helplines right now. +

+ +
+ {QUICK_HELPLINES.map((helpline, idx) => { + const Icon = helpline.icon; + return ( + +
+ +
+
+

{helpline.title}

+

{helpline.number}

+

{helpline.description}

+
+
+ ); + })} +
+
+ + {/* Footer */} +
+ +
+
+
+ ); +} diff --git a/src/pages/Resources.tsx b/src/pages/Resources.tsx index ffe1265..be0a3f3 100644 --- a/src/pages/Resources.tsx +++ b/src/pages/Resources.tsx @@ -113,56 +113,7 @@ const SAFETY_GUIDE: SafetySection[] = [ ], }, ]; - const QUICK_HELPLINES = [ - { - icon: Phone, - title: "Women's Helpline", - number: "1091 / 181", - description: "24/7 support for women in distress", - }, - { - icon: AlertTriangle, - title: "Emergency Response", - number: "112", - description: "National emergency assistance", - }, - { - icon: Heart, - title: "Child Helpline", - number: "1098", - description: "Support for children in need", - }, - { - icon: Globe, - title: "Cyber Crime Helpline", - number: "1930", - description: "Report cyber fraud, online harassment, and digital abuse", - }, - { - icon: Heart, - title: "Ambulance Services", - number: "108", - description: "Emergency medical assistance", - }, - { - icon: Shield, - title: "Police", - number: "100", - description: "Immediate law enforcement assistance", - }, - { - icon: AlertTriangle, - title: "Fire Services", - number: "101", - description: "Fire and rescue emergency services", - }, - { - icon: Heart, - title: "Kiran Mental Health Helpline", - number: "1800-599-0019", - description: "24/7 mental health and emotional support", - }, -]; +import { QUICK_HELPLINES } from "../utils/constants"; function Accordion({ title, children, diff --git a/src/pages/ShareStory.tsx b/src/pages/ShareStory.tsx index 72c80a5..a07512f 100644 --- a/src/pages/ShareStory.tsx +++ b/src/pages/ShareStory.tsx @@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom'; import { toast } from 'react-hot-toast'; import { auth } from '../lib/firebase'; import { Edit, Trash2, CheckSquare, Loader2, Sparkles } from 'lucide-react'; +import { EmergencyModal } from '../components/EmergencyModal'; import { User } from 'firebase/auth'; // Firebase imports @@ -104,6 +105,7 @@ function resolveMediaItem(entry: string | MediaItem): MediaItem { } export default function ShareStory() { + const [showEmergencyModal, setShowEmergencyModal] = useState(false); const [title, setTitle] = useState(''); const [content, setContent] = useState(''); const [selectedTags, setSelectedTags] = useState([]); @@ -426,7 +428,7 @@ export default function ShareStory() { try { // Add a new document to Firestore - const riskLevel = await classifyPostRisk(title, storyText); + const classification = await classifyPostRisk(title, storyText); await addDoc(collection(db, 'stories'), { title, @@ -434,11 +436,17 @@ export default function ShareStory() { tags: selectedTags, author_id: user.uid, media_urls: mediaUrls, - risk_level: riskLevel, + risk_level: classification.riskLevel, + risk_reason: classification.reason, + classified_at: new Date().toISOString(), created_at: serverTimestamp(), }); - toast.success('Your story has been shared successfully'); + if (classification.riskLevel === 'HIGH') { + setShowEmergencyModal(true); + } else { + toast.success('Your story has been shared successfully'); + } // Fetch the updated stories list fetchMyStories(); @@ -889,22 +897,37 @@ export default function ShareStory() { )}
+ { + setShowEmergencyModal(false); + toast.success('Your story has been shared successfully'); + }} + />
); } -async function classifyPostRisk(title: string, content: string): Promise { +interface CrisisClassification { + riskLevel: string; + reason: string; +} + +async function classifyPostRisk(title: string, content: string): Promise { try { const response = await fetch('/.netlify/functions/classify-crisis', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title, content }), }); - if (!response.ok) return 'LOW'; + if (!response.ok) return { riskLevel: 'LOW', reason: '' }; const data = await response.json(); - return data.riskLevel || 'LOW'; + return { + riskLevel: data.riskLevel || 'LOW', + reason: data.reason || '' + }; } catch { - return 'LOW'; + return { riskLevel: 'LOW', reason: '' }; } } diff --git a/src/utils/constants.ts b/src/utils/constants.ts new file mode 100644 index 0000000..8061914 --- /dev/null +++ b/src/utils/constants.ts @@ -0,0 +1,52 @@ +import { Phone, AlertTriangle, Heart, Globe, Shield } from "lucide-react"; + +export const QUICK_HELPLINES = [ + { + icon: Phone, + title: "Women's Helpline", + number: "1091 / 181", + description: "24/7 support for women in distress", + }, + { + icon: AlertTriangle, + title: "Emergency Response", + number: "112", + description: "National emergency assistance", + }, + { + icon: Heart, + title: "Child Helpline", + number: "1098", + description: "Support for children in need", + }, + { + icon: Globe, + title: "Cyber Crime Helpline", + number: "1930", + description: "Report cyber fraud, online harassment, and digital abuse", + }, + { + icon: Heart, + title: "Ambulance Services", + number: "108", + description: "Emergency medical assistance", + }, + { + icon: Shield, + title: "Police", + number: "100", + description: "Immediate law enforcement assistance", + }, + { + icon: AlertTriangle, + title: "Fire Services", + number: "101", + description: "Fire and rescue emergency services", + }, + { + icon: Heart, + title: "Kiran Mental Health Helpline", + number: "1800-599-0019", + description: "24/7 mental health and emotional support", + }, +];