-
-
Notifications
You must be signed in to change notification settings - Fork 21
feat: add typing indicator for better UI experience issue #8 #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 5 commits
f47028f
b4b4a8d
b43ac7c
cd1cd9a
c2483dd
87cb3d7
aa771c1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,21 +1,15 @@ | ||
| # Backend Environment Variables | ||
| # Copy this to .env and fill in your actual values | ||
|
|
||
| # Supabase Configuration (get from https://app.supabase.com → Project Settings → API) | ||
| # Supabase Configuration | ||
| SUPABASE_URL=https://your-project-id.supabase.co | ||
| SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... | ||
| SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... | ||
| SUPABASE_ANON_KEY=your-anon-key | ||
| SUPABASE_SERVICE_ROLE_KEY=your-service-role-key | ||
|
|
||
| # HuggingFace API Token (optional - get from https://huggingface.co/settings/tokens) | ||
| # If not provided, crisis detection will use keyword-based fallback | ||
| # HuggingFace API Token (Optional) | ||
| HUGGINGFACE_API_TOKEN=hf_YourTokenHere | ||
|
|
||
| # Frontend URL (for CORS whitelist) | ||
| FRONTEND_URL=http://localhost:3000 | ||
|
|
||
| # Server Configuration | ||
| PORT=3001 | ||
| FRONTEND_URL=http://localhost:3000 | ||
|
|
||
| # Rate Limiting (optional - defaults shown) | ||
| # Rate Limiting | ||
| RATE_LIMIT_WINDOW_MS=900000 | ||
| RATE_LIMIT_MAX_REQUESTS=100 |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import { z } from 'zod'; | ||
| import dotenv from 'dotenv'; | ||
|
|
||
| dotenv.config(); | ||
|
|
||
| const envSchema = z.object({ | ||
| // Supabase Configuration | ||
| SUPABASE_URL: z.string().url(), | ||
| SUPABASE_ANON_KEY: z.string().min(1), | ||
| SUPABASE_SERVICE_ROLE_KEY: z.string().min(1), | ||
|
|
||
| // HuggingFace API Token (Optional) | ||
| HUGGINGFACE_API_TOKEN: z.string().optional(), | ||
|
|
||
| // Server Configuration | ||
| FRONTEND_URL: z.string().url().default('http://localhost:3000'), | ||
| PORT: z.coerce.number().default(3001), | ||
|
|
||
| // Rate Limiting | ||
| RATE_LIMIT_WINDOW_MS: z.coerce.number().default(900000), | ||
| RATE_LIMIT_MAX_REQUESTS: z.coerce.number().default(100), | ||
| }); | ||
|
|
||
| const _env = envSchema.safeParse(process.env); | ||
|
|
||
| if (!_env.success) { | ||
| console.error('Invalid environment variables:'); | ||
| console.error(JSON.stringify(_env.error.format(), null, 4)); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| export const env = _env.data; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,60 +1,22 @@ | ||
| import dotenv from 'dotenv'; | ||
| import { env } from './env'; | ||
|
|
||
| dotenv.config(); | ||
|
|
||
| interface Config { | ||
| const config = { | ||
| supabase: { | ||
| url: string; | ||
| anonKey: string; | ||
| serviceRoleKey: string; | ||
| }; | ||
| huggingface: { | ||
| apiToken?: string; | ||
| }; | ||
| server: { | ||
| port: number; | ||
| frontendUrl: string; | ||
| }; | ||
| rateLimit: { | ||
| windowMs: number; | ||
| maxRequests: number; | ||
| }; | ||
| } | ||
|
|
||
| const config: Config = { | ||
| supabase: { | ||
| url: process.env.SUPABASE_URL || '', | ||
| anonKey: process.env.SUPABASE_ANON_KEY || '', | ||
| serviceRoleKey: process.env.SUPABASE_SERVICE_ROLE_KEY || '', | ||
| url: env.SUPABASE_URL, | ||
| anonKey: env.SUPABASE_ANON_KEY, | ||
| serviceRoleKey: env.SUPABASE_SERVICE_ROLE_KEY, | ||
| }, | ||
| huggingface: { | ||
| apiToken: process.env.HUGGINGFACE_API_TOKEN, | ||
| apiToken: env.HUGGINGFACE_API_TOKEN, | ||
| }, | ||
| server: { | ||
| port: parseInt(process.env.PORT || '3001', 10), | ||
| frontendUrl: process.env.FRONTEND_URL || 'http://localhost:3000', | ||
| port: env.PORT, | ||
| frontendUrl: env.FRONTEND_URL, | ||
| }, | ||
| rateLimit: { | ||
| windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '900000', 10), | ||
| maxRequests: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '100', 10), | ||
| windowMs: env.RATE_LIMIT_WINDOW_MS, | ||
| maxRequests: env.RATE_LIMIT_MAX_REQUESTS, | ||
| }, | ||
| }; | ||
|
|
||
| // Validation | ||
| const requiredEnvVars = [ | ||
| 'SUPABASE_URL', | ||
| 'SUPABASE_ANON_KEY', | ||
| 'SUPABASE_SERVICE_ROLE_KEY', | ||
| 'FRONTEND_URL', | ||
| ]; | ||
|
|
||
| const missingVars = requiredEnvVars.filter((varName) => !process.env[varName]); | ||
|
|
||
| if (missingVars.length > 0) { | ||
| throw new Error( | ||
| `Missing required environment variables: ${missingVars.join(', ')}\n` + | ||
| 'Please check your .env file and ensure all required variables are set.' | ||
| ); | ||
| } | ||
|
|
||
| export default config; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,18 +2,26 @@ import WebSocket from 'ws'; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { supabase } from '../lib/supabase'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { detectCrisis, getCrisisResourcesMessage } from './crisisDetection'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export interface AuthenticatedWebSocket extends WebSocket { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isAlive?: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| roomId?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userId?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| nickname?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface ChatMessage { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: 'join' | 'leave' | 'chat' | 'crisis_alert'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: 'join' | 'leave' | 'chat' | 'crisis_alert' | 'typing'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| roomId?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userId?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| nickname?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| content?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| riskLevel?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| timestamp?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isTyping?: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface RoomMember { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ws: WebSocket; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ws: AuthenticatedWebSocket; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userId: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| nickname: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -30,7 +38,8 @@ export class ChatServer { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Heartbeat to detect dead connections | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setInterval(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.wss.clients.forEach((ws: any) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.wss.clients.forEach((client) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ws = client as AuthenticatedWebSocket; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (ws.isAlive === false) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ws.terminate(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -40,12 +49,13 @@ export class ChatServer { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, 30000); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private handleConnection(ws: WebSocket) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private handleConnection(client: WebSocket) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ws = client as AuthenticatedWebSocket; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('New WebSocket connection'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (ws as any).isAlive = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ws.isAlive = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ws.on('pong', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (ws as any).isAlive = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ws.isAlive = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ws.on('message', async (data: string) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -68,7 +78,7 @@ export class ChatServer { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private async handleMessage(ws: WebSocket, message: ChatMessage) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private async handleMessage(ws: AuthenticatedWebSocket, message: ChatMessage) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| switch (message.type) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case 'join': | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await this.handleJoin(ws, message); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -79,12 +89,29 @@ export class ChatServer { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case 'chat': | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await this.handleChatMessage(ws, message); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case 'typing': | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.handleTyping(ws, message); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ws.send(JSON.stringify({ type: 'error', message: 'Unknown message type' })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private async handleJoin(ws: WebSocket, message: ChatMessage) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private handleTyping(ws: AuthenticatedWebSocket, message: ChatMessage) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { roomId, userId, nickname } = ws; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (roomId && userId) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Broadcast to other users in the room | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.broadcastToRoom(roomId, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: 'typing', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| nickname, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isTyping: message.isTyping, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, userId); // Exclude sender | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+103
to
+112
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (roomId && userId) { | |
| const isTyping = typeof message.isTyping === 'boolean' ? message.isTyping : false; | |
| // Broadcast to other users in the room | |
| this.broadcastToRoom(roomId, { | |
| type: 'typing', | |
| userId, | |
| nickname, | |
| isTyping, | |
| }, userId); // Exclude sender | |
| } | |
| // Ensure the socket is still a member of the room before broadcasting | |
| if (!roomId || !userId) { | |
| return; | |
| } | |
| const roomMembers = this.rooms.get(roomId); | |
| if (!roomMembers) { | |
| return; | |
| } | |
| const isMember = Array.from(roomMembers).some((member) => member.ws === ws); | |
| if (!isMember) { | |
| return; | |
| } | |
| const isTyping = typeof message.isTyping === 'boolean' ? message.isTyping : false; | |
| // Broadcast to other users in the room | |
| this.broadcastToRoom( | |
| roomId, | |
| { | |
| type: 'typing', | |
| userId, | |
| nickname, | |
| isTyping, | |
| }, | |
| userId, // Exclude sender | |
| ); |
Uh oh!
There was an error while loading. Please reload this page.