diff --git a/.gitignore b/.gitignore index 7d57d90..e25f3b5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,8 @@ /frontend/node_modules +/frontend/build/ +/frontend/dist/ /backend/node_modules +/backend/build/ +/backend/dist/ /.context .env \ No newline at end of file diff --git a/backend/.env b/backend/.env index 9a06f44..6a3f431 100644 --- a/backend/.env +++ b/backend/.env @@ -1,3 +1,4 @@ GEMINI_API_KEY="AIzaSyDke9H2NhiG6rBwxT0qrdYgnNoNZm_0j58" ENCRYPTION_KEY=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2 -CORS_ORIGIN=http://localhost:3000 \ No newline at end of file +CORS_ORIGIN=http://localhost:3000 +SESSION_TIMEOUT_MINUTES=1 \ No newline at end of file diff --git a/backend/dist/index.js b/backend/dist/index.js index d1b638e..6867092 100644 --- a/backend/dist/index.js +++ b/backend/dist/index.js @@ -49,6 +49,7 @@ app.post('/sessions', (req, res) => { responses: new Map(), clients: new Map(), finalResult: null, + lastActivity: Date.now(), }); console.log(`New session created: ${sessionId}`); res.status(201).json({ sessionId }); diff --git a/backend/dist/routes/sessions.js b/backend/dist/routes/sessions.js index e5fda32..0fb4f6c 100644 --- a/backend/dist/routes/sessions.js +++ b/backend/dist/routes/sessions.js @@ -13,30 +13,89 @@ var __importDefault = (this && this.__importDefault) || function (mod) { }; Object.defineProperty(exports, "__esModule", { value: true }); const express_1 = __importDefault(require("express")); -const ws_1 = require("../ws"); // Import sessions, SessionState, broadcastToSession, and handleWebSocketMessage from ws/index.ts +const uuid_1 = require("uuid"); +const ws_1 = require("../ws"); // Import sessions, SessionState, broadcastToSession from ws/index.ts +const LLMService_1 = require("../services/LLMService"); +const EncryptionService_1 = require("../services/EncryptionService"); const router = express_1.default.Router(); +// Initialize LLM Service (API key from environment) +const llmService = new LLMService_1.LLMService(process.env.GEMINI_API_KEY || ''); +// Initialize Encryption Service +const encryptionService = new EncryptionService_1.EncryptionService(process.env.ENCRYPTION_KEY || ''); +router.post('/sessions', (req, res) => { + const sessionId = (0, uuid_1.v4)(); + ws_1.sessions.set(sessionId, { + state: ws_1.SessionState.SETUP, + topic: null, + description: null, + expectedResponses: 0, + submittedCount: 0, + responses: new Map(), + clients: new Map(), + finalResult: null, + lastActivity: Date.now(), + }); + res.status(201).json({ sessionId }); +}); router.post('/sessions/:sessionId/responses', (req, res) => __awaiter(void 0, void 0, void 0, function* () { const { sessionId } = req.params; - const { userId, wants, accepts, afraidToAsk } = req.body; + const { clientId, wants, accepts, noGoes, afraidToAsk } = req.body; // Use clientId instead of userId if (!ws_1.sessions.has(sessionId)) { return res.status(404).json({ message: 'Session not found.' }); } - // Create a dummy WebSocket object for the handleWebSocketMessage function. - // This is a workaround to reuse the WebSocket message handling logic. - // In a real application, consider a more robust event-driven architecture. - const dummyWs = { - send: (message) => console.log('Dummy WS send:', message), - readyState: 1, // OPEN - }; - const message = { - type: 'SUBMIT_RESPONSE', - clientId: userId, - payload: { - response: { wants, accepts, afraidToAsk }, - }, - }; + const sessionData = ws_1.sessions.get(sessionId); + if (sessionData.state !== ws_1.SessionState.GATHERING) { + return res.status(400).json({ message: `Session is not in GATHERING state. Current state: ${sessionData.state}` }); + } + if (sessionData.responses.has(clientId)) { + return res.status(400).json({ message: 'You have already submitted a response for this session.' }); + } + if ([...wants, ...accepts, ...noGoes].some((desire) => desire.length > 500) || afraidToAsk.length > 500) { + return res.status(400).json({ message: 'One of your desires or afraidToAsk exceeds the 500 character limit.' }); + } try { - yield (0, ws_1.handleWebSocketMessage)(dummyWs, sessionId, message); + const hasContradictionsGist = yield llmService.checkForInnerContradictions({ wants, accepts, noGoes, afraidToAsk }); + if (hasContradictionsGist) { + return res.status(400).json({ message: `Your submission contains inner contradictions: ${hasContradictionsGist} Please resolve them and submit again.` }); + } + const encryptedWants = wants.map((d) => encryptionService.encrypt(d)); + const encryptedAccepts = accepts.map((d) => encryptionService.encrypt(d)); + const encryptedNoGoes = noGoes.map((d) => encryptionService.encrypt(d)); + const encryptedAfraidToAsk = encryptionService.encrypt(afraidToAsk); + sessionData.responses.set(clientId, { wants: encryptedWants, accepts: encryptedAccepts, noGoes: encryptedNoGoes, afraidToAsk: encryptedAfraidToAsk }); + sessionData.submittedCount++; + console.log(`Client ${clientId} submitted response via HTTP. Submitted count: ${sessionData.submittedCount}/${sessionData.expectedResponses}`); + if (sessionData.submittedCount === sessionData.expectedResponses) { + sessionData.state = ws_1.SessionState.HARMONIZING; + (0, ws_1.broadcastToSession)(sessionId, { type: 'STATE_UPDATE', payload: {} }); + console.log(`Session ${sessionId} moved to HARMONIZING. Triggering LLM analysis.`); + // Perform LLM analysis asynchronously + (() => __awaiter(void 0, void 0, void 0, function* () { + try { + const allDecryptedDesires = Array.from(sessionData.responses.values()).map(encryptedResponse => { + const decryptedWants = encryptedResponse.wants.map((d) => encryptionService.decrypt(d)); + const decryptedAccepts = encryptedResponse.accepts.map((d) => encryptionService.decrypt(d)); + const decryptedNoGoes = encryptedResponse.noGoes.map((d) => encryptionService.decrypt(d)); + const decryptedAfraidToAsk = encryptionService.decrypt(encryptedResponse.afraidToAsk); + return { wants: decryptedWants, accepts: decryptedAccepts, noGoes: decryptedNoGoes, afraidToAsk: decryptedAfraidToAsk }; + }); + const decision = yield llmService.analyzeDesires(allDecryptedDesires); + sessionData.finalResult = decision; + sessionData.state = ws_1.SessionState.FINAL; + (0, ws_1.broadcastToSession)(sessionId, { type: 'STATE_UPDATE', payload: {} }); + console.log(`Analysis complete for session ${sessionId}. Result:`, decision); + } + catch (error) { + console.error(`Error during analysis for session ${sessionId}:`, error.message); + sessionData.state = ws_1.SessionState.ERROR; + (0, ws_1.broadcastToSession)(sessionId, { type: 'STATE_UPDATE', payload: {} }); + } + }))(); + } + else { + // Only broadcast the latest count if the session is not yet harmonizing + (0, ws_1.broadcastToSession)(sessionId, { type: 'STATE_UPDATE', payload: {} }); + } res.status(202).json({ message: 'Response submission acknowledged and processed.' }); } catch (error) { diff --git a/backend/dist/ws/index.js b/backend/dist/ws/index.js index 6aae1b5..af36565 100644 --- a/backend/dist/ws/index.js +++ b/backend/dist/ws/index.js @@ -69,8 +69,23 @@ const broadcastToSession = (sessionId, message, excludeClientId = null) => { } }; exports.broadcastToSession = broadcastToSession; +const SESSION_TIMEOUT_MINUTES = parseInt(process.env.SESSION_TIMEOUT_MINUTES || '1440', 10); +const SESSION_TIMEOUT_MS = SESSION_TIMEOUT_MINUTES * 60 * 1000; // Convert minutes to milliseconds +// Function to clean up inactive sessions +const cleanupInactiveSessions = () => { + const now = Date.now(); + for (const [sessionId, sessionData] of exports.sessions.entries()) { + if (sessionData.clients.size === 0 && (now - sessionData.lastActivity > SESSION_TIMEOUT_MS)) { + exports.sessions.delete(sessionId); + logEvent('session_purged_inactive', sessionId); + console.log(`Inactive session ${sessionId} purged.`); + } + } +}; const createWebSocketServer = (server) => { const wss = new ws_1.WebSocketServer({ server }); + // Schedule periodic cleanup of inactive sessions + setInterval(cleanupInactiveSessions, 60 * 60 * 1000); // Run every hour wss.on('connection', (ws, req) => { const url = new URL(req.url || '', `http://${req.headers.host}`); const sessionId = url.pathname.split('/').pop(); @@ -78,20 +93,7 @@ const createWebSocketServer = (server) => { ws.close(1008, 'Invalid session ID'); return; } - if (!exports.sessions.has(sessionId)) { - exports.sessions.set(sessionId, { - state: SessionState.SETUP, - topic: null, - description: null, - expectedResponses: 0, - submittedCount: 0, - responses: new Map(), - clients: new Map(), - finalResult: null, - }); - } - const sessionData = exports.sessions.get(sessionId); - console.log(`Client connecting to session: ${sessionId}`); + let sessionData = null; // Set up a ping interval to keep the connection alive const pingInterval = setInterval(() => { if (ws.readyState === ws_1.WebSocket.OPEN) { @@ -100,40 +102,45 @@ const createWebSocketServer = (server) => { }, 30000); // Send ping every 30 seconds ws.on('message', (message) => __awaiter(void 0, void 0, void 0, function* () { const parsedMessage = JSON.parse(message.toString()); - const { type, clientId, payload } = parsedMessage; - if (!clientId) { - console.error(`Received message without clientId in session ${sessionId}. Type: ${type}`); - ws.send(JSON.stringify({ type: 'ERROR', payload: { message: 'clientId is required' } })); - return; + const updatedSessionData = yield (0, exports.handleWebSocketMessage)(ws, sessionId, parsedMessage); + if (updatedSessionData) { + sessionData = updatedSessionData; } - if (!sessionData.clients.has(clientId)) { - sessionData.clients.set(clientId, ws); - console.log(`Client ${clientId} registered for session: ${sessionId}. Total clients: ${sessionData.clients.size}`); - ws.send(JSON.stringify({ type: 'STATE_UPDATE', payload: { session: getSerializableSession(sessionData, clientId) } })); - } - console.log(`Received message from ${clientId} in session ${sessionId}:`, type); - yield (0, exports.handleWebSocketMessage)(ws, sessionId, parsedMessage); })); ws.on('close', () => { clearInterval(pingInterval); // Clear the interval when the connection closes let disconnectedClientId = null; - for (const [clientId, clientWs] of sessionData.clients.entries()) { - if (clientWs === ws) { - disconnectedClientId = clientId; - break; + const currentSessionData = exports.sessions.get(sessionId); // Retrieve the latest sessionData + if (currentSessionData) { // Check if sessionData is not null + for (const [clientId, clientWs] of currentSessionData.clients.entries()) { + if (clientWs === ws) { + disconnectedClientId = clientId; + break; + } + } + if (disconnectedClientId) { + currentSessionData.clients.delete(disconnectedClientId); + console.log(`Client ${disconnectedClientId} disconnected from session: ${sessionId}. Remaining clients: ${currentSessionData.clients.size}`); + } + else { + console.log(`An unregistered client disconnected from session: ${sessionId}.`); + } + if (currentSessionData.clients.size === 0) { + // Only purge session if it's in SETUP, FINAL, or ERROR state + if (currentSessionData.state === SessionState.SETUP || + currentSessionData.state === SessionState.FINAL || + currentSessionData.state === SessionState.ERROR) { + exports.sessions.delete(sessionId); + logEvent('session_purged', sessionId); + console.log(`Session ${sessionId} closed and state cleared.`); + } + else { + console.log(`Session ${sessionId} is in ${currentSessionData.state} state. Not purging despite no active clients.`); + } } } - if (disconnectedClientId) { - sessionData.clients.delete(disconnectedClientId); - console.log(`Client ${disconnectedClientId} disconnected from session: ${sessionId}. Remaining clients: ${sessionData.clients.size}`); - } else { - console.log(`An unregistered client disconnected from session: ${sessionId}.`); - } - if (sessionData.clients.size === 0) { - exports.sessions.delete(sessionId); - logEvent('session_purged', sessionId); - console.log(`Session ${sessionId} closed and state cleared.`); + console.log(`Client disconnected from session: ${sessionId}. Session data was null.`); } }); ws.on('error', (error) => { @@ -148,31 +155,67 @@ const handleWebSocketMessage = (ws, sessionId, parsedMessage) => __awaiter(void if (!clientId) { console.error(`Received message without clientId in session ${sessionId}. Type: ${type}`); ws.send(JSON.stringify({ type: 'ERROR', payload: { message: 'clientId is required' } })); - return; + return exports.sessions.get(sessionId) || null; // Return current session state if available } - const sessionData = exports.sessions.get(sessionId); - if (!sessionData.clients.has(clientId)) { - sessionData.clients.set(clientId, ws); - console.log(`Client ${clientId} registered for session: ${sessionId}. Total clients: ${sessionData.clients.size}`); - ws.send(JSON.stringify({ type: 'STATE_UPDATE', payload: { session: getSerializableSession(sessionData, clientId) } })); + let sessionData = exports.sessions.get(sessionId); + // Update lastActivity timestamp on any message + if (sessionData) { + sessionData.lastActivity = Date.now(); + console.log(`Session ${sessionId}: lastActivity updated to ${sessionData.lastActivity}`); + } + // If sessionData is null here, it means a JOIN_SESSION message hasn't been processed yet for a new session. + // The JOIN_SESSION case will handle its creation. + if (!sessionData && type !== 'JOIN_SESSION') { + ws.send(JSON.stringify({ type: 'ERROR', payload: { message: 'Session not found and message is not JOIN_SESSION' } })); + return null; // Session not found, return null } - console.log(`Received message from ${clientId} in session ${sessionId}:`, type); switch (type) { - case 'REGISTER_CLIENT': - console.log(`Client ${clientId} registered successfully for session ${sessionId}.`); - break; + case 'JOIN_SESSION': + if (!sessionData) { + // Create a new session if it doesn't exist + const newSessionData = { + state: SessionState.SETUP, + topic: null, + description: null, + expectedResponses: 0, + submittedCount: 0, + responses: new Map(), + clients: new Map(), + finalResult: null, + lastActivity: Date.now(), // Initialize lastActivity + }; + exports.sessions.set(sessionId, newSessionData); // Explicitly set in global map + sessionData = newSessionData; // Update local reference to the newly created session + console.log(`New session ${sessionId} created upon client ${clientId} joining. lastActivity: ${sessionData.lastActivity}`); + } + // Register the client to the session's clients map + if (!sessionData.clients.has(clientId)) { + sessionData.clients.set(clientId, ws); + console.log(`Client ${clientId} joined session: ${sessionId}. Total clients: ${sessionData.clients.size}`); + } + ws.send(JSON.stringify({ type: 'STATE_UPDATE', payload: { session: getSerializableSession(sessionData, clientId) } })); + console.log(`Client ${clientId} received STATE_UPDATE for session ${sessionId} upon joining.`); + return sessionData; // Return the updated sessionData case 'PING': + if (!sessionData) { + ws.send(JSON.stringify({ type: 'ERROR', payload: { message: 'Session data not available for PING' } })); + return null; + } // Respond to client pings with a pong if (ws.readyState === ws_1.WebSocket.OPEN) { ws.pong(); } - break; + return sessionData || null; // Return current sessionData or null if undefined case 'SETUP_SESSION': + if (!sessionData) { + ws.send(JSON.stringify({ type: 'ERROR', payload: { message: 'Session data not available for SETUP_SESSION' } })); + return null; + } if (sessionData.state === SessionState.SETUP) { const { expectedResponses, topic, description } = payload; if (typeof expectedResponses !== 'number' || expectedResponses <= 0) { ws.send(JSON.stringify({ type: 'ERROR', payload: { message: 'Invalid expectedResponses' } })); - return; + return sessionData || null; // Return current sessionData on error } sessionData.expectedResponses = expectedResponses; sessionData.topic = topic || 'Untitled Session'; @@ -184,22 +227,26 @@ const handleWebSocketMessage = (ws, sessionId, parsedMessage) => __awaiter(void else { ws.send(JSON.stringify({ type: 'ERROR', payload: { message: `Session is not in SETUP state. Current state: ${sessionData.state}` } })); } - break; + return sessionData || null; // Return current sessionData case 'SUBMIT_RESPONSE': + if (!sessionData) { + ws.send(JSON.stringify({ type: 'ERROR', payload: { message: 'Session data not available for SUBMIT_RESPONSE' } })); + return null; + } if (sessionData.state === SessionState.GATHERING) { if (sessionData.responses.has(clientId)) { ws.send(JSON.stringify({ type: 'ERROR', payload: { message: 'You have already submitted a response for this session.' } })); - return; + return sessionData || null; // Return current sessionData on error } const { wants, accepts, noGoes, afraidToAsk } = payload.response; - if ([...wants, ...accepts, ...noGoes].some(desire => desire.length > 500) || afraidToAsk.length > 500) { + if ([...wants, ...accepts, ...noGoes].some((desire) => desire.length > 500) || afraidToAsk.length > 500) { ws.send(JSON.stringify({ type: 'ERROR', payload: { message: 'One of your desires or afraidToAsk exceeds the 500 character limit.' } })); - return; + return sessionData || null; // Return current sessionData on error } const hasContradictionsGist = yield llmService.checkForInnerContradictions(payload.response); if (hasContradictionsGist) { ws.send(JSON.stringify({ type: 'ERROR', payload: { message: `Your submission contains inner contradictions: ${hasContradictionsGist} Please resolve them and submit again.` } })); - return; + return sessionData || null; // Return current sessionData on error } const encryptedWants = wants.map((d) => encryptionService.encrypt(d)); const encryptedAccepts = accepts.map((d) => encryptionService.encrypt(d)); @@ -207,18 +254,15 @@ const handleWebSocketMessage = (ws, sessionId, parsedMessage) => __awaiter(void const encryptedAfraidToAsk = encryptionService.encrypt(afraidToAsk); sessionData.responses.set(clientId, { wants: encryptedWants, accepts: encryptedAccepts, noGoes: encryptedNoGoes, afraidToAsk: encryptedAfraidToAsk }); sessionData.submittedCount++; - logEvent('response_submitted', sessionId, { clientId, submittedCount: sessionData.submittedCount }); console.log(`Client ${clientId} submitted response. Submitted count: ${sessionData.submittedCount}/${sessionData.expectedResponses}`); if (sessionData.submittedCount === sessionData.expectedResponses) { sessionData.state = SessionState.HARMONIZING; (0, exports.broadcastToSession)(sessionId, { type: 'STATE_UPDATE', payload: {} }); - logEvent('session_harmonizing', sessionId, { expectedResponses: sessionData.expectedResponses }); console.log(`Session ${sessionId} moved to HARMONIZING. Triggering LLM analysis.`); // Perform LLM analysis asynchronously (() => __awaiter(void 0, void 0, void 0, function* () { - let durationMs = 0; // Declare here try { - logEvent('llm_analysis_started', sessionId); + console.log('llm_analysis_started', sessionId); const startTime = process.hrtime.bigint(); const allDecryptedDesires = Array.from(sessionData.responses.values()).map(encryptedResponse => { const decryptedWants = encryptedResponse.wants.map((d) => encryptionService.decrypt(d)); @@ -231,17 +275,17 @@ const handleWebSocketMessage = (ws, sessionId, parsedMessage) => __awaiter(void sessionData.finalResult = decision; sessionData.state = SessionState.FINAL; (0, exports.broadcastToSession)(sessionId, { type: 'STATE_UPDATE', payload: {} }); - logEvent('llm_analysis_completed', sessionId, { result: decision }); - recordMetric('llm_analysis_duration', durationMs, sessionId, { status: 'success' }); - recordMetric('llm_analysis_availability', 'available', sessionId); + console.log('llm_analysis_completed', sessionId, { result: decision }); + console.log('llm_analysis_duration', 0, sessionId, { status: 'success' }); + console.log('llm_analysis_availability', 'available', sessionId); console.log(`Analysis complete for session ${sessionId}. Result:`, decision); } catch (error) { console.error(`Error during analysis for session ${sessionId}:`, error.message); sessionData.state = SessionState.ERROR; (0, exports.broadcastToSession)(sessionId, { type: 'STATE_UPDATE', payload: {} }); - logEvent('llm_analysis_error', sessionId, { error: error.message }); - recordMetric('llm_analysis_availability', 'unavailable', sessionId, { error: error.message }); + console.log('llm_analysis_error', sessionId, { error: error.message }); + console.log('llm_analysis_availability', 'unavailable', sessionId, { error: error.message }); } }))(); } @@ -253,11 +297,23 @@ const handleWebSocketMessage = (ws, sessionId, parsedMessage) => __awaiter(void else { ws.send(JSON.stringify({ type: 'ERROR', payload: { message: `Session is not in GATHERING state. Current state: ${sessionData.state}` } })); } - break; + return sessionData || null; // Return current sessionData default: console.warn(`Unknown message type: ${type} from client ${clientId} in session ${sessionId}`); ws.send(JSON.stringify({ type: 'ERROR', payload: { message: `Unknown message type: ${type}` } })); - break; + return sessionData || null; // Return current sessionData } }); exports.handleWebSocketMessage = handleWebSocketMessage; +const cleanupInactiveSessions = () => { + console.log('Running cleanupInactiveSessions...'); + const now = Date.now(); + for (const [sessionId, sessionData] of exports.sessions.entries()) { + console.log(`Session ${sessionId}: clients.size=${sessionData.clients.size}, lastActivity=${sessionData.lastActivity}, timeSinceLastActivity=${now - sessionData.lastActivity}, SESSION_TIMEOUT_MS=${SESSION_TIMEOUT_MS}`); + if (sessionData.clients.size === 0 && (now - sessionData.lastActivity > SESSION_TIMEOUT_MS)) { + exports.sessions.delete(sessionId); + logEvent('session_purged_inactive', sessionId); + console.log(`Inactive session ${sessionId} purged.`); + } + } +}; diff --git a/backend/src/index.ts b/backend/src/index.ts index 7a856e6..46b16c5 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -51,6 +51,7 @@ app.post('/sessions', (req, res) => { responses: new Map(), clients: new Map(), finalResult: null, + lastActivity: Date.now(), }); console.log(`New session created: ${sessionId}`); res.status(201).json({ sessionId }); diff --git a/backend/src/routes/sessions.ts b/backend/src/routes/sessions.ts index d6b1287..779ad85 100644 --- a/backend/src/routes/sessions.ts +++ b/backend/src/routes/sessions.ts @@ -1,9 +1,18 @@ + + import express from 'express'; import { v4 as uuidv4 } from 'uuid'; -import { sessions, SessionState, broadcastToSession, handleWebSocketMessage } from '../ws'; // Import sessions, SessionState, broadcastToSession, and handleWebSocketMessage from ws/index.ts +import { sessions, SessionState, broadcastToSession } from '../ws'; // Import sessions, SessionState, broadcastToSession from ws/index.ts +import { LLMService } from '../services/LLMService'; +import { EncryptionService } from '../services/EncryptionService'; const router = express.Router(); +// Initialize LLM Service (API key from environment) +const llmService = new LLMService(process.env.GEMINI_API_KEY || ''); +// Initialize Encryption Service +const encryptionService = new EncryptionService(process.env.ENCRYPTION_KEY || ''); + router.post('/sessions', (req, res) => { const sessionId = uuidv4(); sessions.set(sessionId, { @@ -15,36 +24,81 @@ router.post('/sessions', (req, res) => { responses: new Map(), clients: new Map(), finalResult: null, + lastActivity: Date.now(), }); res.status(201).json({ sessionId }); }); router.post('/sessions/:sessionId/responses', async (req, res) => { const { sessionId } = req.params; - const { userId, wants, accepts, afraidToAsk } = req.body; + const { clientId, wants, accepts, noGoes, afraidToAsk } = req.body; // Use clientId instead of userId if (!sessions.has(sessionId)) { return res.status(404).json({ message: 'Session not found.' }); } - // Create a dummy WebSocket object for the handleWebSocketMessage function. - // This is a workaround to reuse the WebSocket message handling logic. - // In a real application, consider a more robust event-driven architecture. - const dummyWs: any = { - send: (message: string) => console.log('Dummy WS send:', message), - readyState: 1, // OPEN - }; + const sessionData = sessions.get(sessionId)!; - const message = { - type: 'SUBMIT_RESPONSE', - clientId: userId, // Using userId as clientId for simplicity in this context - payload: { - response: { wants, accepts, afraidToAsk }, - }, - }; + if (sessionData.state !== SessionState.GATHERING) { + return res.status(400).json({ message: `Session is not in GATHERING state. Current state: ${sessionData.state}` }); + } + + if (sessionData.responses.has(clientId)) { + return res.status(400).json({ message: 'You have already submitted a response for this session.' }); + } + + if ([...wants, ...accepts, ...noGoes].some((desire: string) => desire.length > 500) || afraidToAsk.length > 500) { + return res.status(400).json({ message: 'One of your desires or afraidToAsk exceeds the 500 character limit.' }); + } try { - await handleWebSocketMessage(dummyWs, sessionId, message); + const hasContradictionsGist = await llmService.checkForInnerContradictions({ wants, accepts, noGoes, afraidToAsk }); + if (hasContradictionsGist) { + return res.status(400).json({ message: `Your submission contains inner contradictions: ${hasContradictionsGist} Please resolve them and submit again.` }); + } + + const encryptedWants = wants.map((d: string) => encryptionService.encrypt(d)); + const encryptedAccepts = accepts.map((d: string) => encryptionService.encrypt(d)); + const encryptedNoGoes = noGoes.map((d: string) => encryptionService.encrypt(d)); + const encryptedAfraidToAsk = encryptionService.encrypt(afraidToAsk); + + sessionData.responses.set(clientId, { wants: encryptedWants, accepts: encryptedAccepts, noGoes: encryptedNoGoes, afraidToAsk: encryptedAfraidToAsk }); + sessionData.submittedCount++; + + console.log(`Client ${clientId} submitted response via HTTP. Submitted count: ${sessionData.submittedCount}/${sessionData.expectedResponses}`); + + if (sessionData.submittedCount === sessionData.expectedResponses) { + sessionData.state = SessionState.HARMONIZING; + broadcastToSession(sessionId, { type: 'STATE_UPDATE', payload: {} }); + console.log(`Session ${sessionId} moved to HARMONIZING. Triggering LLM analysis.`); + + // Perform LLM analysis asynchronously + (async () => { + try { + const allDecryptedDesires = Array.from(sessionData.responses.values()).map(encryptedResponse => { + const decryptedWants = encryptedResponse.wants.map((d: string) => encryptionService.decrypt(d)); + const decryptedAccepts = encryptedResponse.accepts.map((d: string) => encryptionService.decrypt(d)); + const decryptedNoGoes = encryptedResponse.noGoes.map((d: string) => encryptionService.decrypt(d)); + const decryptedAfraidToAsk = encryptionService.decrypt(encryptedResponse.afraidToAsk); + return { wants: decryptedWants, accepts: decryptedAccepts, noGoes: decryptedNoGoes, afraidToAsk: decryptedAfraidToAsk }; + }); + const decision = await llmService.analyzeDesires(allDecryptedDesires); + + sessionData.finalResult = decision; + sessionData.state = SessionState.FINAL; + broadcastToSession(sessionId, { type: 'STATE_UPDATE', payload: {} }); + console.log(`Analysis complete for session ${sessionId}. Result:`, decision); + } catch (error: any) { + console.error(`Error during analysis for session ${sessionId}:`, error.message); + sessionData.state = SessionState.ERROR; + broadcastToSession(sessionId, { type: 'STATE_UPDATE', payload: {} }); + } + })(); + } else { + // Only broadcast the latest count if the session is not yet harmonizing + broadcastToSession(sessionId, { type: 'STATE_UPDATE', payload: {} }); + } + res.status(202).json({ message: 'Response submission acknowledged and processed.' }); } catch (error: any) { console.error('Error processing response via HTTP route:', error); diff --git a/backend/src/ws/index.ts b/backend/src/ws/index.ts index 5c85cf4..30843d4 100644 --- a/backend/src/ws/index.ts +++ b/backend/src/ws/index.ts @@ -41,6 +41,7 @@ interface SessionData { responses: Map; // Stores the submitted desire objects. Map clients: Map; // Maps the persistent Client ID to their active WebSocket connection object. finalResult: any | null; // The result returned by the LLM. + lastActivity: number; // Timestamp of the last activity (e.g., message, client join/leave) } export const sessions = new Map(); @@ -102,9 +103,27 @@ export const broadcastToSession = (sessionId: string, message: any, excludeClien } }; +const SESSION_TIMEOUT_MINUTES = parseInt(process.env.SESSION_TIMEOUT_MINUTES || '1440', 10); +const SESSION_TIMEOUT_MS = SESSION_TIMEOUT_MINUTES * 60 * 1000; // Convert minutes to milliseconds + +// Function to clean up inactive sessions +const cleanupInactiveSessions = () => { + const now = Date.now(); + for (const [sessionId, sessionData] of sessions.entries()) { + if (sessionData.clients.size === 0 && (now - sessionData.lastActivity > SESSION_TIMEOUT_MS)) { + sessions.delete(sessionId); + logEvent('session_purged_inactive', sessionId); + console.log(`Inactive session ${sessionId} purged.`); + } + } +}; + export const createWebSocketServer = (server: any) => { const wss = new WebSocketServer({ server }); + // Schedule periodic cleanup of inactive sessions + setInterval(cleanupInactiveSessions, 60 * 60 * 1000); // Run every hour + wss.on('connection', (ws, req) => { const url = new URL(req.url || '', `http://${req.headers.host}`); const sessionId = url.pathname.split('/').pop(); @@ -114,21 +133,9 @@ export const createWebSocketServer = (server: any) => { return; } - if (!sessions.has(sessionId)) { - sessions.set(sessionId, { - state: SessionState.SETUP, - topic: null, - description: null, - expectedResponses: 0, - submittedCount: 0, - responses: new Map(), - clients: new Map(), - finalResult: null, - }); - } - const sessionData = sessions.get(sessionId)!; - console.log(`Client connecting to session: ${sessionId}`); + + let sessionData: SessionData | null = null; // Set up a ping interval to keep the connection alive const pingInterval = setInterval(() => { @@ -137,47 +144,45 @@ export const createWebSocketServer = (server: any) => { } }, 30000); // Send ping every 30 seconds - ws.on('message', async (message) => { - const parsedMessage = JSON.parse(message.toString()); - const { type, clientId, payload } = parsedMessage; - - if (!clientId) { - console.error(`Received message without clientId in session ${sessionId}. Type: ${type}`); - ws.send(JSON.stringify({ type: 'ERROR', payload: { message: 'clientId is required' } })); - return; - } - - if (!sessionData.clients.has(clientId)) { - sessionData.clients.set(clientId, ws); - console.log(`Client ${clientId} registered for session: ${sessionId}. Total clients: ${sessionData.clients.size}`); - ws.send(JSON.stringify({ type: 'STATE_UPDATE', payload: { session: getSerializableSession(sessionData, clientId) } })); - } - - console.log(`Received message from ${clientId} in session ${sessionId}:`, type); - await handleWebSocketMessage(ws, sessionId, parsedMessage); - }); - - ws.on('close', () => { + ws.on('message', async (message) => { + const parsedMessage = JSON.parse(message.toString()); + const updatedSessionData = await handleWebSocketMessage(ws, sessionId, parsedMessage); + if (updatedSessionData) { + sessionData = updatedSessionData; + } + }); ws.on('close', () => { clearInterval(pingInterval); // Clear the interval when the connection closes let disconnectedClientId: string | null = null; - for (const [clientId, clientWs] of sessionData.clients.entries()) { - if (clientWs === ws) { - disconnectedClientId = clientId; - break; + const currentSessionData = sessions.get(sessionId); // Retrieve the latest sessionData + if (currentSessionData) { // Check if sessionData is not null + for (const [clientId, clientWs] of currentSessionData.clients.entries()) { + if (clientWs === ws) { + disconnectedClientId = clientId; + break; + } } - } - if (disconnectedClientId) { - sessionData.clients.delete(disconnectedClientId); - console.log(`Client ${disconnectedClientId} disconnected from session: ${sessionId}. Remaining clients: ${sessionData.clients.size}`); + if (disconnectedClientId) { + currentSessionData.clients.delete(disconnectedClientId); + console.log(`Client ${disconnectedClientId} disconnected from session: ${sessionId}. Remaining clients: ${currentSessionData.clients.size}`); + } else { + console.log(`An unregistered client disconnected from session: ${sessionId}.`); + } + + if (currentSessionData.clients.size === 0) { + // Only purge session if it's in SETUP, FINAL, or ERROR state + if (currentSessionData.state === SessionState.SETUP || + currentSessionData.state === SessionState.FINAL || + currentSessionData.state === SessionState.ERROR) { + sessions.delete(sessionId); + logEvent('session_purged', sessionId); + console.log(`Session ${sessionId} closed and state cleared.`); + } else { + console.log(`Session ${sessionId} is in ${currentSessionData.state} state. Not purging despite no active clients.`); + } + } } else { - console.log(`An unregistered client disconnected from session: ${sessionId}.`); - } - - if (sessionData.clients.size === 0) { - sessions.delete(sessionId); - logEvent('session_purged', sessionId); - console.log(`Session ${sessionId} closed and state cleared.`); + console.log(`Client disconnected from session: ${sessionId}. Session data was null.`); } }); @@ -189,131 +194,337 @@ export const createWebSocketServer = (server: any) => { return wss; }; -export const handleWebSocketMessage = async (ws: WebSocket, sessionId: string, parsedMessage: any) => { +export const handleWebSocketMessage = async (ws: WebSocket, sessionId: string, parsedMessage: any): Promise => { const { type, clientId, payload } = parsedMessage; if (!clientId) { console.error(`Received message without clientId in session ${sessionId}. Type: ${type}`); ws.send(JSON.stringify({ type: 'ERROR', payload: { message: 'clientId is required' } })); - return; + return sessions.get(sessionId) || null; // Return current session state if available } - const sessionData = sessions.get(sessionId)!; + let sessionData = sessions.get(sessionId); - if (!sessionData.clients.has(clientId)) { - sessionData.clients.set(clientId, ws); - console.log(`Client ${clientId} registered for session: ${sessionId}. Total clients: ${sessionData.clients.size}`); - ws.send(JSON.stringify({ type: 'STATE_UPDATE', payload: { session: getSerializableSession(sessionData, clientId) } })); - } + - console.log(`Received message from ${clientId} in session ${sessionId}:`, type); + // Update lastActivity timestamp on any message - switch (type) { - case 'REGISTER_CLIENT': - console.log(`Client ${clientId} registered successfully for session ${sessionId}.`); - break; + if (sessionData) { - case 'PING': - // Respond to client pings with a pong - if (ws.readyState === WebSocket.OPEN) { - ws.pong(); - } - break; + sessionData.lastActivity = Date.now(); - case 'SETUP_SESSION': - if (sessionData.state === SessionState.SETUP) { - const { expectedResponses, topic, description } = payload; - if (typeof expectedResponses !== 'number' || expectedResponses <= 0) { - ws.send(JSON.stringify({ type: 'ERROR', payload: { message: 'Invalid expectedResponses' } })); - return; - } - sessionData.expectedResponses = expectedResponses; - sessionData.topic = topic || 'Untitled Session'; - sessionData.description = description || null; - sessionData.state = SessionState.GATHERING; - broadcastToSession(sessionId, { type: 'STATE_UPDATE', payload: {} }); - console.log(`Session ${sessionId} moved to GATHERING with topic "${sessionData.topic}" and ${expectedResponses} expected responses.`); - } else { - ws.send(JSON.stringify({ type: 'ERROR', payload: { message: `Session is not in SETUP state. Current state: ${sessionData.state}` } })); - } - break; + console.log(`Session ${sessionId}: lastActivity updated to ${sessionData.lastActivity}`); + + } + + + + // If sessionData is null here, it means a JOIN_SESSION message hasn't been processed yet for a new session. + + // The JOIN_SESSION case will handle its creation. + + if (!sessionData && type !== 'JOIN_SESSION') { + + ws.send(JSON.stringify({ type: 'ERROR', payload: { message: 'Session not found and message is not JOIN_SESSION' } })); + + return null; // Session not found, return null + + } + + + + switch (type) { + + case 'JOIN_SESSION': + + if (!sessionData) { + + // Create a new session if it doesn't exist + + const newSessionData: SessionData = { + + state: SessionState.SETUP, + + topic: null, + + description: null, + + expectedResponses: 0, + + submittedCount: 0, + + responses: new Map(), + + clients: new Map(), + + finalResult: null, + + lastActivity: Date.now(), // Initialize lastActivity + + }; + + sessions.set(sessionId, newSessionData); // Explicitly set in global map + + sessionData = newSessionData; // Update local reference to the newly created session + + console.log(`New session ${sessionId} created upon client ${clientId} joining. lastActivity: ${sessionData.lastActivity}`); - case 'SUBMIT_RESPONSE': - if (sessionData.state === SessionState.GATHERING) { - if (sessionData.responses.has(clientId)) { - ws.send(JSON.stringify({ type: 'ERROR', payload: { message: 'You have already submitted a response for this session.' } })); - return; } - const { wants, accepts, noGoes, afraidToAsk } = payload.response; - if ([...wants, ...accepts, ...noGoes].some(desire => desire.length > 500) || afraidToAsk.length > 500) { - ws.send(JSON.stringify({ type: 'ERROR', payload: { message: 'One of your desires or afraidToAsk exceeds the 500 character limit.' } })); - return; + + + // Register the client to the session's clients map + + if (!sessionData.clients.has(clientId)) { + + sessionData.clients.set(clientId, ws); + + console.log(`Client ${clientId} joined session: ${sessionId}. Total clients: ${sessionData.clients.size}`); + } - const hasContradictionsGist = await llmService.checkForInnerContradictions(payload.response); - if (hasContradictionsGist) { - ws.send(JSON.stringify({ type: 'ERROR', payload: { message: `Your submission contains inner contradictions: ${hasContradictionsGist} Please resolve them and submit again.` } })); - return; + ws.send(JSON.stringify({ type: 'STATE_UPDATE', payload: { session: getSerializableSession(sessionData, clientId) } })); + + console.log(`Client ${clientId} received STATE_UPDATE for session ${sessionId} upon joining.`); + + return sessionData; // Return the updated sessionData + + + + case 'PING': + + if (!sessionData) { ws.send(JSON.stringify({ type: 'ERROR', payload: { message: 'Session data not available for PING' } })); return null; } + + // Respond to client pings with a pong + + if (ws.readyState === WebSocket.OPEN) { + + ws.pong(); + } - const encryptedWants = wants.map((d: string) => encryptionService.encrypt(d)); - const encryptedAccepts = accepts.map((d: string) => encryptionService.encrypt(d)); - const encryptedNoGoes = noGoes.map((d: string) => encryptionService.encrypt(d)); - const encryptedAfraidToAsk = encryptionService.encrypt(afraidToAsk); + return sessionData || null; // Return current sessionData or null if undefined - sessionData.responses.set(clientId, { wants: encryptedWants, accepts: encryptedAccepts, noGoes: encryptedNoGoes, afraidToAsk: encryptedAfraidToAsk }); - sessionData.submittedCount++; - logEvent('response_submitted', sessionId, { clientId, submittedCount: sessionData.submittedCount }); - console.log(`Client ${clientId} submitted response. Submitted count: ${sessionData.submittedCount}/${sessionData.expectedResponses}`); + case 'SETUP_SESSION': + + if (!sessionData) { ws.send(JSON.stringify({ type: 'ERROR', payload: { message: 'Session data not available for SETUP_SESSION' } })); return null; } + + if (sessionData.state === SessionState.SETUP) { + + const { expectedResponses, topic, description } = payload; + + if (typeof expectedResponses !== 'number' || expectedResponses <= 0) { + + ws.send(JSON.stringify({ type: 'ERROR', payload: { message: 'Invalid expectedResponses' } })); + + return sessionData || null; // Return current sessionData on error + + } + + sessionData.expectedResponses = expectedResponses; + + sessionData.topic = topic || 'Untitled Session'; + + sessionData.description = description || null; + + sessionData.state = SessionState.GATHERING; - if (sessionData.submittedCount === sessionData.expectedResponses) { - sessionData.state = SessionState.HARMONIZING; broadcastToSession(sessionId, { type: 'STATE_UPDATE', payload: {} }); - logEvent('session_harmonizing', sessionId, { expectedResponses: sessionData.expectedResponses }); - console.log(`Session ${sessionId} moved to HARMONIZING. Triggering LLM analysis.`); - // Perform LLM analysis asynchronously - (async () => { - let durationMs: number = 0; // Declare here - try { - logEvent('llm_analysis_started', sessionId); - const startTime = process.hrtime.bigint(); const allDecryptedDesires = Array.from(sessionData.responses.values()).map(encryptedResponse => { - const decryptedWants = encryptedResponse.wants.map((d: string) => encryptionService.decrypt(d)); - const decryptedAccepts = encryptedResponse.accepts.map((d: string) => encryptionService.decrypt(d)); - const decryptedNoGoes = encryptedResponse.noGoes.map((d: string) => encryptionService.decrypt(d)); - const decryptedAfraidToAsk = encryptionService.decrypt(encryptedResponse.afraidToAsk); - return { wants: decryptedWants, accepts: decryptedAccepts, noGoes: decryptedNoGoes, afraidToAsk: decryptedAfraidToAsk }; - }); - const decision = await llmService.analyzeDesires(allDecryptedDesires); + console.log(`Session ${sessionId} moved to GATHERING with topic "${sessionData.topic}" and ${expectedResponses} expected responses.`); - sessionData.finalResult = decision; - sessionData.state = SessionState.FINAL; - broadcastToSession(sessionId, { type: 'STATE_UPDATE', payload: {} }); - logEvent('llm_analysis_completed', sessionId, { result: decision }); - recordMetric('llm_analysis_duration', durationMs, sessionId, { status: 'success' }); recordMetric('llm_analysis_availability', 'available', sessionId); - console.log(`Analysis complete for session ${sessionId}. Result:`, decision); + } + + else { + + ws.send(JSON.stringify({ type: 'ERROR', payload: { message: `Session is not in SETUP state. Current state: ${sessionData.state}` } })); + + } + + return sessionData || null; // Return current sessionData + + + + case 'SUBMIT_RESPONSE': + + if (!sessionData) { ws.send(JSON.stringify({ type: 'ERROR', payload: { message: 'Session data not available for SUBMIT_RESPONSE' } })); return null; } + + if (sessionData.state === SessionState.GATHERING) { + + if (sessionData.responses.has(clientId)) { + + ws.send(JSON.stringify({ type: 'ERROR', payload: { message: 'You have already submitted a response for this session.' } })); + + return sessionData || null; // Return current sessionData on error + + } + + + + const { wants, accepts, noGoes, afraidToAsk } = payload.response; + + if ([...wants, ...accepts, ...noGoes].some((desire: string) => desire.length > 500) || afraidToAsk.length > 500) { + + ws.send(JSON.stringify({ type: 'ERROR', payload: { message: 'One of your desires or afraidToAsk exceeds the 500 character limit.' } })); + + return sessionData || null; // Return current sessionData on error + + } + + + + const hasContradictionsGist = await llmService.checkForInnerContradictions(payload.response); + + if (hasContradictionsGist) { + + ws.send(JSON.stringify({ type: 'ERROR', payload: { message: `Your submission contains inner contradictions: ${hasContradictionsGist} Please resolve them and submit again.` } })); + + return sessionData || null; // Return current sessionData on error + + } + + + + const encryptedWants = wants.map((d: string) => encryptionService.encrypt(d)); + + const encryptedAccepts = accepts.map((d: string) => encryptionService.encrypt(d)); + + const encryptedNoGoes = noGoes.map((d: string) => encryptionService.encrypt(d)); + + const encryptedAfraidToAsk = encryptionService.encrypt(afraidToAsk); + + + + sessionData.responses.set(clientId, { wants: encryptedWants, accepts: encryptedAccepts, noGoes: encryptedNoGoes, afraidToAsk: encryptedAfraidToAsk }); + + sessionData.submittedCount++; + + + + console.log(`Client ${clientId} submitted response. Submitted count: ${sessionData.submittedCount}/${sessionData.expectedResponses}`); + + + + if (sessionData.submittedCount === sessionData.expectedResponses) { + + sessionData.state = SessionState.HARMONIZING; + + broadcastToSession(sessionId, { type: 'STATE_UPDATE', payload: {} }); + + console.log(`Session ${sessionId} moved to HARMONIZING. Triggering LLM analysis.`); + + + + // Perform LLM analysis asynchronously + + (async () => { + + try { + + console.log('llm_analysis_started', sessionId); + + const startTime = process.hrtime.bigint(); const allDecryptedDesires = Array.from(sessionData.responses.values()).map(encryptedResponse => { + + const decryptedWants = encryptedResponse.wants.map((d: string) => encryptionService.decrypt(d)); + + const decryptedAccepts = encryptedResponse.accepts.map((d: string) => encryptionService.decrypt(d)); + + const decryptedNoGoes = encryptedResponse.noGoes.map((d: string) => encryptionService.decrypt(d)); + + const decryptedAfraidToAsk = encryptionService.decrypt(encryptedResponse.afraidToAsk); + + return { wants: decryptedWants, accepts: decryptedAccepts, noGoes: decryptedNoGoes, afraidToAsk: decryptedAfraidToAsk }; + + }); + + const decision = await llmService.analyzeDesires(allDecryptedDesires); + + + + sessionData.finalResult = decision; + + sessionData.state = SessionState.FINAL; + + broadcastToSession(sessionId, { type: 'STATE_UPDATE', payload: {} }); + + console.log('llm_analysis_completed', sessionId, { result: decision }); + + console.log('llm_analysis_duration', 0, sessionId, { status: 'success' }); console.log('llm_analysis_availability', 'available', sessionId); + + console.log(`Analysis complete for session ${sessionId}. Result:`, decision); + + + + } catch (error: any) { + + console.error(`Error during analysis for session ${sessionId}:`, error.message); + + sessionData.state = SessionState.ERROR; + + broadcastToSession(sessionId, { type: 'STATE_UPDATE', payload: {} }); + + console.log('llm_analysis_error', sessionId, { error: error.message }); + + console.log('llm_analysis_availability', 'unavailable', sessionId, { error: error.message }); + + } + + })(); + + } else { + + // Only broadcast the latest count if the session is not yet harmonizing + + broadcastToSession(sessionId, { type: 'STATE_UPDATE', payload: {} }); + + } - } catch (error: any) { - console.error(`Error during analysis for session ${sessionId}:`, error.message); - sessionData.state = SessionState.ERROR; - broadcastToSession(sessionId, { type: 'STATE_UPDATE', payload: {} }); - logEvent('llm_analysis_error', sessionId, { error: error.message }); - recordMetric('llm_analysis_availability', 'unavailable', sessionId, { error: error.message }); - } - })(); } else { - // Only broadcast the latest count if the session is not yet harmonizing - broadcastToSession(sessionId, { type: 'STATE_UPDATE', payload: {} }); - } - } else { - ws.send(JSON.stringify({ type: 'ERROR', payload: { message: `Session is not in GATHERING state. Current state: ${sessionData.state}` } })); - } - break; - default: - console.warn(`Unknown message type: ${type} from client ${clientId} in session ${sessionId}`); - ws.send(JSON.stringify({ type: 'ERROR', payload: { message: `Unknown message type: ${type}` } })); - break; - } -}; + ws.send(JSON.stringify({ type: 'ERROR', payload: { message: `Session is not in GATHERING state. Current state: ${sessionData.state}` } })); + + } + + return sessionData || null; // Return current sessionData + + + + default: + + console.warn(`Unknown message type: ${type} from client ${clientId} in session ${sessionId}`); + + ws.send(JSON.stringify({ type: 'ERROR', payload: { message: `Unknown message type: ${type}` } })); + + return sessionData || null; // Return current sessionData + + } + + }; + + + + const cleanupInactiveSessions = () => { + + console.log('Running cleanupInactiveSessions...'); + + const now = Date.now(); + + for (const [sessionId, sessionData] of sessions.entries()) { + + console.log(`Session ${sessionId}: clients.size=${sessionData.clients.size}, lastActivity=${sessionData.lastActivity}, timeSinceLastActivity=${now - sessionData.lastActivity}, SESSION_TIMEOUT_MS=${SESSION_TIMEOUT_MS}`); + + if (sessionData.clients.size === 0 && (now - sessionData.lastActivity > SESSION_TIMEOUT_MS)) { + + sessions.delete(sessionId); + + logEvent('session_purged_inactive', sessionId); + + console.log(`Inactive session ${sessionId} purged.`); + + } + + } + + }; + + diff --git a/frontend/build/asset-manifest.json b/frontend/build/asset-manifest.json index 8cb2898..c2177a5 100644 --- a/frontend/build/asset-manifest.json +++ b/frontend/build/asset-manifest.json @@ -1,10 +1,10 @@ { "files": { - "main.js": "/static/js/main.d2d83152.js", + "main.js": "/static/js/main.53017931.js", "index.html": "/index.html", - "main.d2d83152.js.map": "/static/js/main.d2d83152.js.map" + "main.53017931.js.map": "/static/js/main.53017931.js.map" }, "entrypoints": [ - "static/js/main.d2d83152.js" + "static/js/main.53017931.js" ] } \ No newline at end of file diff --git a/frontend/build/index.html b/frontend/build/index.html index 7b74ad6..f9c99ce 100644 --- a/frontend/build/index.html +++ b/frontend/build/index.html @@ -1 +1 @@ -Unisono
\ No newline at end of file +Unisono
\ No newline at end of file diff --git a/frontend/build/static/js/main.d2d83152.js b/frontend/build/static/js/main.d2d83152.js deleted file mode 100644 index 1087397..0000000 --- a/frontend/build/static/js/main.d2d83152.js +++ /dev/null @@ -1,3 +0,0 @@ -/*! For license information please see main.d2d83152.js.LICENSE.txt */ -(()=>{var e={39:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"default",{enumerable:!0,get:function(){return r.createSvgIcon}});var r=n(1100)},219:(e,t,n)=>{"use strict";var r=n(3763),o={childContextTypes:!0,contextType:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromError:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},a={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},i={$$typeof:!0,compare:!0,defaultProps:!0,displayName:!0,propTypes:!0,type:!0},l={};function s(e){return r.isMemo(e)?i:l[e.$$typeof]||o}l[r.ForwardRef]={$$typeof:!0,render:!0,defaultProps:!0,displayName:!0,propTypes:!0},l[r.Memo]=i;var u=Object.defineProperty,c=Object.getOwnPropertyNames,d=Object.getOwnPropertySymbols,f=Object.getOwnPropertyDescriptor,p=Object.getPrototypeOf,h=Object.prototype;e.exports=function e(t,n,r){if("string"!==typeof n){if(h){var o=p(n);o&&o!==h&&e(t,o,r)}var i=c(n);d&&(i=i.concat(d(n)));for(var l=s(t),m=s(n),g=0;g{"use strict";n.r(t),n.d(t,{default:()=>r.A});var r=n(7868)},528:(e,t)=>{"use strict";var n=Symbol.for("react.transitional.element"),r=Symbol.for("react.portal"),o=Symbol.for("react.fragment"),a=Symbol.for("react.strict_mode"),i=Symbol.for("react.profiler"),l=Symbol.for("react.consumer"),s=Symbol.for("react.context"),u=Symbol.for("react.forward_ref"),c=Symbol.for("react.suspense"),d=Symbol.for("react.suspense_list"),f=Symbol.for("react.memo"),p=Symbol.for("react.lazy"),h=Symbol.for("react.view_transition"),m=Symbol.for("react.client.reference");function g(e){if("object"===typeof e&&null!==e){var t=e.$$typeof;switch(t){case n:switch(e=e.type){case o:case i:case a:case c:case d:case h:return e;default:switch(e=e&&e.$$typeof){case s:case u:case p:case f:case l:return e;default:return t}}case r:return t}}}t.vM=u,t.lD=f},579:(e,t,n)=>{"use strict";e.exports=n(1153)},869:(e,t,n)=>{"use strict";n.d(t,{A:()=>a});n(5043);var r=n(3290),o=n(579);function a(e){const{styles:t,defaultTheme:n={}}=e,a="function"===typeof t?e=>{return t(void 0===(r=e)||null===r||0===Object.keys(r).length?n:e);var r}:t;return(0,o.jsx)(r.mL,{styles:a})}},918:(e,t,n)=>{"use strict";function r(e){var t=Object.create(null);return function(n){return void 0===t[n]&&(t[n]=e(n)),t[n]}}n.d(t,{A:()=>r})},950:(e,t,n)=>{"use strict";n.d(t,{A:()=>r});const r=n(3468).A},1100:(e,t,n)=>{"use strict";n.r(t),n.d(t,{capitalize:()=>o.A,createChainedFunction:()=>a,createSvgIcon:()=>i.A,debounce:()=>l.A,deprecatedPropType:()=>s,isMuiElement:()=>u.A,ownerDocument:()=>c.A,ownerWindow:()=>d.A,requirePropFactory:()=>f,setRef:()=>p,unstable_ClassNameGenerator:()=>w,unstable_useEnhancedEffect:()=>h.A,unstable_useId:()=>m,unsupportedProp:()=>g,useControlled:()=>v.A,useEventCallback:()=>y.A,useForkRef:()=>b.A,useIsFocusVisible:()=>x.A});var r=n(9386),o=n(6803);const a=n(2456).A;var i=n(9662),l=n(950);const s=function(e,t){return()=>null};var u=n(7328),c=n(2427),d=n(6078);const f=function(e,t){return()=>null};const p=n(6564).A;var h=n(5013);const m=n(5844).A;const g=function(e,t,n,r,o){return null};var v=n(5420),y=n(3319),b=n(5849),x=n(3574);const w={configure:e=>{r.A.configure(e)}}},1153:(e,t,n)=>{"use strict";var r=n(5043),o=Symbol.for("react.element"),a=Symbol.for("react.fragment"),i=Object.prototype.hasOwnProperty,l=r.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,s={key:!0,ref:!0,__self:!0,__source:!0};function u(e,t,n){var r,a={},u=null,c=null;for(r in void 0!==n&&(u=""+n),void 0!==t.key&&(u=""+t.key),void 0!==t.ref&&(c=t.ref),t)i.call(t,r)&&!s.hasOwnProperty(r)&&(a[r]=t[r]);if(e&&e.defaultProps)for(r in t=e.defaultProps)void 0===a[r]&&(a[r]=t[r]);return{$$typeof:o,type:e,key:u,ref:c,props:a,_owner:l.current}}t.Fragment=a,t.jsx=u,t.jsxs=u},1337:(e,t,n)=>{"use strict";var r=n(4994);t.A=void 0;var o=r(n(39)),a=n(579);t.A=(0,o.default)((0,a.jsx)("path",{d:"M16.59 8.59 12 13.17 7.41 8.59 6 10l6 6 6-6z"}),"ExpandMore")},1338:(e,t,n)=>{"use strict";var r=n(4994);t.A=void 0;var o=r(n(39)),a=n(579);t.A=(0,o.default)((0,a.jsx)("path",{d:"M16.59 7.58 10 14.17l-3.59-3.58L5 12l5 5 8-8zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2m0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8"}),"CheckCircleOutline")},1475:(e,t,n)=>{"use strict";n.d(t,{A:()=>o});var r=n(7123);const o=e=>(0,r.A)(e)&&"classes"!==e},1668:(e,t,n)=>{"use strict";function r(e){return e&&e.ownerDocument||document}n.d(t,{A:()=>r})},1722:(e,t,n)=>{"use strict";n.d(t,{Rk:()=>r,SF:()=>o,sk:()=>a});function r(e,t,n){var r="";return n.split(" ").forEach(function(n){void 0!==e[n]?t.push(e[n]+";"):n&&(r+=n+" ")}),r}var o=function(e,t,n){var r=e.key+"-"+t.name;!1===n&&void 0===e.registered[r]&&(e.registered[r]=t.styles)},a=function(e,t,n){o(e,t,n);var r=e.key+"-"+t.name;if(void 0===e.inserted[t.name]){var a=t;do{e.insert(t===a?"."+r:"",a,e.sheet,!0),a=a.next}while(void 0!==a)}}},1782:(e,t,n)=>{"use strict";n.d(t,{A:()=>a});var r=n(5043),o=n(4440);const a=function(e){const t=r.useRef(e);return(0,o.A)(()=>{t.current=e}),r.useRef(function(){return(0,t.current)(...arguments)}).current}},2372:(e,t,n)=>{"use strict";n.d(t,{Ay:()=>a});var r=n(9386);const o={active:"active",checked:"checked",completed:"completed",disabled:"disabled",error:"error",expanded:"expanded",focused:"focused",focusVisible:"focusVisible",open:"open",readOnly:"readOnly",required:"required",selected:"selected"};function a(e,t){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"Mui";const a=o[t];return a?"".concat(n,"-").concat(a):"".concat(r.A.generate(e),"-").concat(t)}},2427:(e,t,n)=>{"use strict";n.d(t,{A:()=>r});const r=n(1668).A},2456:(e,t,n)=>{"use strict";function r(){for(var e=arguments.length,t=new Array(e),n=0;nnull==t?e:function(){for(var n=arguments.length,r=new Array(n),o=0;o{})}n.d(t,{A:()=>r})},2532:(e,t,n)=>{"use strict";n.d(t,{A:()=>o});var r=n(2372);function o(e,t){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"Mui";const o={};return t.forEach(t=>{o[t]=(0,r.Ay)(e,t,n)}),o}},2730:(e,t,n)=>{"use strict";var r=n(5043),o=n(8853);function a(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n