diff --git a/backend/database/schema.sql b/backend/database/schema.sql index 5265007..8d29ef0 100644 --- a/backend/database/schema.sql +++ b/backend/database/schema.sql @@ -32,6 +32,7 @@ CREATE TABLE IF NOT EXISTS messages ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), room_id UUID NOT NULL REFERENCES rooms(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + nickname VARCHAR(50), -- Denormalized for chat history content TEXT NOT NULL, risk_level VARCHAR(20) DEFAULT 'none' CHECK (risk_level IN ('none', 'low', 'medium', 'high', 'critical')), created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() diff --git a/backend/src/routes/journal.ts b/backend/src/routes/journal.ts index 119bd64..4fb8eca 100644 --- a/backend/src/routes/journal.ts +++ b/backend/src/routes/journal.ts @@ -83,14 +83,14 @@ router.put('/:id', authenticate, async (req: AuthRequest, res) => { .single(); if (error) { + // Check for "no rows found" error code from PostgREST + if (error.code === 'PGRST116') { + return res.status(404).json({ error: 'Entry not found' }); + } console.error('Error updating journal entry:', error); return res.status(500).json({ error: 'Failed to update entry' }); } - if (!data) { - return res.status(404).json({ error: 'Entry not found' }); - } - res.json(data); } catch (error) { console.error('Error:', error); @@ -104,17 +104,22 @@ router.delete('/:id', authenticate, async (req: AuthRequest, res) => { const userId = req.user!.id; const { id } = req.params; - const { error } = await supabase + const { data, error } = await supabase .from('journal_entries') .delete() .eq('id', id) - .eq('user_id', userId); + .eq('user_id', userId) + .select(); if (error) { console.error('Error deleting journal entry:', error); return res.status(500).json({ error: 'Failed to delete entry' }); } + if (!data || data.length === 0) { + return res.status(404).json({ error: 'Entry not found' }); + } + res.status(204).send(); } catch (error) { console.error('Error:', error); diff --git a/backend/src/services/chatServer.ts b/backend/src/services/chatServer.ts index 58127df..9b52e0a 100644 --- a/backend/src/services/chatServer.ts +++ b/backend/src/services/chatServer.ts @@ -1,4 +1,4 @@ -import WebSocket from 'ws'; +import { WebSocket, WebSocketServer } from 'ws'; import { supabase } from '../lib/supabase'; import { detectCrisis, getCrisisResourcesMessage } from './crisisDetection'; @@ -19,11 +19,12 @@ interface RoomMember { } export class ChatServer { - private wss: WebSocket.Server; - private rooms: Map>; + private wss: WebSocketServer; + // RoomId -> UserId -> RoomMember + private rooms: Map>; constructor(server: any) { - this.wss = new WebSocket.Server({ server }); + this.wss = new WebSocketServer({ server }); this.rooms = new Map(); this.wss.on('connection', this.handleConnection.bind(this)); @@ -95,18 +96,19 @@ export class ChatServer { try { // Add user to room if (!this.rooms.has(roomId)) { - this.rooms.set(roomId, new Set()); + this.rooms.set(roomId, new Map()); } const room = this.rooms.get(roomId)!; - room.add({ ws, userId, nickname }); + // Use Map to ensure unique user per room (replace old connection if same user joins) + room.set(userId, { ws, userId, nickname }); // Store connection metadata (ws as any).roomId = roomId; (ws as any).userId = userId; (ws as any).nickname = nickname; - // Fetch recent messages from database (without profile join - nicknames come from messages) + // Fetch recent messages from database const { data: messages, error } = await supabase .from('messages') .select('*') @@ -157,21 +159,19 @@ export class ChatServer { const room = this.rooms.get(roomId); if (room) { // Remove user from room - room.forEach((member) => { - if (member.userId === userId) { - room.delete(member); - } - }); - - // Broadcast leave event - this.broadcastToRoom(roomId, { - type: 'leave', - userId, - nickname, - timestamp: new Date().toISOString(), - }); - - console.log(`${nickname} left room ${roomId}`); + if (room.has(userId)) { + room.delete(userId); + + // Broadcast leave event + this.broadcastToRoom(roomId, { + type: 'leave', + userId, + nickname, + timestamp: new Date().toISOString(), + }); + + console.log(`${nickname} left room ${roomId}`); + } } } } @@ -190,12 +190,13 @@ export class ChatServer { // Detect crisis in message const crisisResult = await detectCrisis(content); - // Save message to database with risk level + // Save message to database with risk level AND nickname const { data: savedMessage, error } = await supabase .from('messages') .insert({ room_id: roomId, user_id: userId, + nickname: nickname, // Added nickname content, risk_level: crisisResult.riskLevel, }) @@ -208,11 +209,11 @@ export class ChatServer { return; } - // Broadcast message to room with nickname from WebSocket metadata + // Broadcast message to room this.broadcastToRoom(roomId, { type: 'chat', userId: savedMessage.user_id, - nickname: nickname, // Use nickname from WebSocket connection + nickname: savedMessage.nickname || nickname, content: savedMessage.content, timestamp: savedMessage.created_at, riskLevel: savedMessage.risk_level, @@ -245,11 +246,13 @@ export class ChatServer { if (roomId && userId) { const room = this.rooms.get(roomId); if (room) { - room.forEach((member) => { - if (member.userId === userId) { - room.delete(member); - } - }); + if (room.has(userId)) { + // Only remove if the socket matches (in case user reconnected quickly on another socket but same userId) + const member = room.get(userId); + if (member && member.ws === ws) { + room.delete(userId); + } + } } } diff --git a/backend/src/services/crisisDetection.ts b/backend/src/services/crisisDetection.ts index 796bab2..2c73620 100644 --- a/backend/src/services/crisisDetection.ts +++ b/backend/src/services/crisisDetection.ts @@ -66,6 +66,8 @@ const CRISIS_KEYWORDS = { const HIGH_RISK_EMOTIONS = ['sadness', 'fear', 'anger']; const MEDIUM_RISK_EMOTIONS = ['disgust', 'surprise']; +const RISK_LEVELS = ['none', 'low', 'medium', 'high', 'critical'] as const; + /** * Analyze message using HuggingFace emotion detection model */ @@ -97,7 +99,7 @@ async function analyzeWithHuggingFace( return null; } - const result: HuggingFaceResponse[][] = await response.json(); + const result = await response.json() as HuggingFaceResponse[][]; return result[0] || null; } catch (error) { console.error('Error calling HuggingFace API:', error); @@ -111,46 +113,25 @@ async function analyzeWithHuggingFace( function analyzeWithKeywords(message: string): CrisisDetectionResult { const lowerMessage = message.toLowerCase(); const triggeredKeywords: string[] = []; - let highestRiskLevel: 'none' | 'low' | 'medium' | 'high' | 'critical' = 'none'; + let highestRiskLevel: typeof RISK_LEVELS[number] = 'none'; - // Check critical keywords - for (const keyword of CRISIS_KEYWORDS.critical) { - if (lowerMessage.includes(keyword)) { - triggeredKeywords.push(keyword); - highestRiskLevel = 'critical'; - } - } + // Check all keywords across all levels + // Use type assertion to iterate through keys safely + const levels = ['critical', 'high', 'medium', 'low'] as const; - // Check high-risk keywords - if (highestRiskLevel !== 'critical') { - for (const keyword of CRISIS_KEYWORDS.high) { + for (const level of levels) { + const keywords = CRISIS_KEYWORDS[level]; + for (const keyword of keywords) { if (lowerMessage.includes(keyword)) { triggeredKeywords.push(keyword); - if (highestRiskLevel !== 'high') { - highestRiskLevel = 'high'; - } - } - } - } - // Check medium-risk keywords - if (highestRiskLevel === 'none' || highestRiskLevel === 'low') { - for (const keyword of CRISIS_KEYWORDS.medium) { - if (lowerMessage.includes(keyword)) { - triggeredKeywords.push(keyword); - if (highestRiskLevel !== 'medium' && highestRiskLevel !== 'high') { - highestRiskLevel = 'medium'; - } - } - } - } + // Update risk level if this level is higher than current highest + const currentLevelIndex = RISK_LEVELS.indexOf(highestRiskLevel); + const newLevelIndex = RISK_LEVELS.indexOf(level); - // Check low-risk keywords - if (highestRiskLevel === 'none') { - for (const keyword of CRISIS_KEYWORDS.low) { - if (lowerMessage.includes(keyword)) { - triggeredKeywords.push(keyword); - highestRiskLevel = 'low'; + if (newLevelIndex > currentLevelIndex) { + highestRiskLevel = level; + } } } } @@ -175,7 +156,7 @@ export async function detectCrisis( if (emotions && emotions.length > 0) { // Analyze emotion scores const detectedEmotions = emotions.map((e) => e.label); - let riskLevel: 'none' | 'low' | 'medium' | 'high' | 'critical' = 'none'; + let riskLevel: typeof RISK_LEVELS[number] = 'none'; let maxScore = 0; for (const emotion of emotions) { @@ -184,7 +165,10 @@ export async function detectCrisis( } if (HIGH_RISK_EMOTIONS.includes(emotion.label) && emotion.score > 0.5) { - riskLevel = emotion.score > 0.7 ? 'high' : 'medium'; + const potentialRisk = emotion.score > 0.7 ? 'high' : 'medium'; + if (RISK_LEVELS.indexOf(potentialRisk) > RISK_LEVELS.indexOf(riskLevel)) { + riskLevel = potentialRisk; + } } else if ( MEDIUM_RISK_EMOTIONS.includes(emotion.label) && emotion.score > 0.6 @@ -197,9 +181,8 @@ export async function detectCrisis( // Also run keyword analysis and take the higher risk level const keywordResult = analyzeWithKeywords(message); - const riskLevels = ['none', 'low', 'medium', 'high', 'critical']; - const aiRiskIndex = riskLevels.indexOf(riskLevel); - const keywordRiskIndex = riskLevels.indexOf(keywordResult.riskLevel); + const aiRiskIndex = RISK_LEVELS.indexOf(riskLevel); + const keywordRiskIndex = RISK_LEVELS.indexOf(keywordResult.riskLevel); if (keywordRiskIndex > aiRiskIndex) { riskLevel = keywordResult.riskLevel; diff --git a/frontend/src/components/ChatRoom.tsx b/frontend/src/components/ChatRoom.tsx index 07a1652..ee8a755 100644 --- a/frontend/src/components/ChatRoom.tsx +++ b/frontend/src/components/ChatRoom.tsx @@ -40,7 +40,7 @@ export default function ChatRoom({ room, currentUser, onClose }: ChatRoomProps) ...prev, { userId: message.userId!, - nickname: message.nickname!, + nickname: message.nickname || 'Anonymous', content: message.content!, timestamp: message.timestamp!, riskLevel: message.riskLevel, @@ -210,7 +210,7 @@ export default function ChatRoom({ room, currentUser, onClose }: ChatRoomProps) : 'text-gray-600' }`} > - {msg.userId === currentUser.id ? 'You' : msg.nickname} + {msg.userId === currentUser.id ? 'You' : (msg.nickname || 'Anonymous')}