From 691a2fa422246a32387e0971eb37bdbe2df223b9 Mon Sep 17 00:00:00 2001 From: aodulov Date: Mon, 13 Oct 2025 13:50:49 +0300 Subject: [PATCH] Session Details implemented --- backend/src/routes/sessions.ts | 1 + backend/src/services/LLMService.ts | 10 ++-- backend/src/ws/index.ts | 5 +- frontend/src/components/ResultsDisplay.tsx | 2 +- frontend/src/hooks/useSession.ts | 1 + frontend/src/pages/SessionPage.tsx | 53 +++++++++++++++++++--- 6 files changed, 59 insertions(+), 13 deletions(-) diff --git a/backend/src/routes/sessions.ts b/backend/src/routes/sessions.ts index f55dbf9..0f4cef3 100644 --- a/backend/src/routes/sessions.ts +++ b/backend/src/routes/sessions.ts @@ -9,6 +9,7 @@ router.post('/sessions', (req, res) => { sessions.set(sessionId, { state: SessionState.SETUP, topic: null, + description: null, expectedResponses: 0, submittedCount: 0, responses: new Map(), diff --git a/backend/src/services/LLMService.ts b/backend/src/services/LLMService.ts index a3bcd9d..d963a29 100644 --- a/backend/src/services/LLMService.ts +++ b/backend/src/services/LLMService.ts @@ -34,15 +34,15 @@ export class LLMService { Each participant's desire set includes 'wants', 'accepts', 'noGoes', and an 'afraidToAsk' field. The 'afraidToAsk' field contains a sensitive idea that the participant is hesitant to express publicly. Here are the rules for categorization and synthesis, with special handling for 'afraidToAsk' ideas: - - "goTo": Synthesize a text describing what ALL participants want without contradictions. This should include 'afraidToAsk' ideas that semantically match all other participant's 'wants' or 'afraidToAsk'. If an 'afraidToAsk' idea matches, it should be treated as a 'want' for the submitting participant. - - "alsoGood": Synthesize a text describing what at least one participant wants (including matched 'afraidToAsk' ideas), not everyone wants but all other participants at least accept, and is not a "noGoes" for anyone. This should reflect a generally agreeable outcome. - - "considerable": Synthesize a text describing what is wanted or accepted by some, but not all, participants (including matched 'afraidToAsk' ideas), and is not a "noGoes" for anyone. This should highlight areas of partial agreement or options that could be explored. - - "noGoes": Synthesize a text describing what at least ONE participant does not want. This should clearly state the collective exclusions. + - "goTo": Synthesize a text describing what ALL participants want without contradictions. This should include 'afraidToAsk' ideas that semantically match all other participant's 'wants' or 'afraidToAsk'. If an 'afraidToAsk' idea matches, it should be treated as a 'want' for the submitting participant. Use the more specific opinions and leave all the specific options if they do not contradict each other drastically. + - "alsoGood": Synthesize a text describing what at least one participant wants (including matched 'afraidToAsk' ideas), not everyone wants but all other participants at least accept, and is not a "noGoes" for anyone. This should reflect a generally agreeable outcome. Use the more specific opinions and leave all the specific options if they do not contradict each other drastically. + - "considerable": Synthesize a text describing what is wanted or accepted by some, but not all, participants (including matched 'afraidToAsk' ideas), and is not a "noGoes" for anyone. This should highlight areas of partial agreement or options that could be explored. Use the more specific opinions and leave all the specific options if they do not contradict each other drastically. + - "noGoes": Synthesize a text describing what at least ONE participant does not want. This should clearly state the collective exclusions. Use the more broad opinions summarizing all the specific options if they do not contradict each other drastically. - "needsDiscussion": Synthesize a text describing where there is a direct conflict (e.g., one participant wants it, another does not want it). This should highlight areas requiring further negotiation. Do not include 'afraidToAsk' in this category. 'AfraidToAsk' ideas that do NOT semantically match any other participant's 'wants' or 'accepts' should remain private and NOT be included in any of the synthesized categories. - Prioritize more specific desires over more broad ones for positive categories ("goTo", "alsoGood", "considerable"). For negative categories ("noGoes", "needsDiscussion"), prioritize more broad ideas over more specific ones. Formulate common ideas from the point of 'us', e.g. "We are going to...", or "We want to...", or "We think..." + Formulate common ideas from the point of 'us', e.g. "We are going to...", or "We want to...", or "We think...", or "We do not...". The input will be a JSON object containing a list of desire sets. Each desire set has a participantId (implicitly handled by the array index) and four arrays/strings: "wants", "accepts", "noGoes", and "afraidToAsk". diff --git a/backend/src/ws/index.ts b/backend/src/ws/index.ts index 4bcdb47..2139677 100644 --- a/backend/src/ws/index.ts +++ b/backend/src/ws/index.ts @@ -35,6 +35,7 @@ interface EncryptedResponseData { interface SessionData { state: SessionState; // Current phase of the session topic: string | null; // The topic of the session + description: string | null; // The description of the session expectedResponses: number; // The number set by the first user in State A. submittedCount: number; // The current count of submitted responses. responses: Map; // Stores the submitted desire objects. Map @@ -117,6 +118,7 @@ export const createWebSocketServer = (server: any) => { sessions.set(sessionId, { state: SessionState.SETUP, topic: null, + description: null, expectedResponses: 0, submittedCount: 0, responses: new Map(), @@ -204,13 +206,14 @@ export const handleWebSocketMessage = async (ws: WebSocket, sessionId: string, p case 'SETUP_SESSION': if (sessionData.state === SessionState.SETUP) { - const { expectedResponses, topic } = payload; + 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.`); diff --git a/frontend/src/components/ResultsDisplay.tsx b/frontend/src/components/ResultsDisplay.tsx index ab06d25..5f034e3 100644 --- a/frontend/src/components/ResultsDisplay.tsx +++ b/frontend/src/components/ResultsDisplay.tsx @@ -48,7 +48,7 @@ const ResultsDisplay: React.FC = ({ decision }) => { - + diff --git a/frontend/src/hooks/useSession.ts b/frontend/src/hooks/useSession.ts index ce07ca3..8475ae6 100644 --- a/frontend/src/hooks/useSession.ts +++ b/frontend/src/hooks/useSession.ts @@ -43,6 +43,7 @@ export interface Session { responses: { [clientId: string]: DesireSet }; // Map of clientId to their submitted DesireSet finalResult: Decision | null; topic?: string; // This might be part of the initial setup payload + description?: string; // Add description field } // Utility to generate a persistent client ID diff --git a/frontend/src/pages/SessionPage.tsx b/frontend/src/pages/SessionPage.tsx index 228b84c..2fc6d74 100644 --- a/frontend/src/pages/SessionPage.tsx +++ b/frontend/src/pages/SessionPage.tsx @@ -10,10 +10,21 @@ const SessionPage = () => { const { sessionId } = useParams<{ sessionId: string }>(); const [session, setSession, sendMessage, clientId, wsError] = useSession(sessionId || ''); const [expectedResponses, setExpectedResponses] = useState(2); + const [expectedResponsesError, setExpectedResponsesError] = useState(false); const [topic, setTopic] = useState(''); + const [topicError, setTopicError] = useState(false); + const [description, setDescription] = useState(''); const handleSetupSession = () => { - sendMessage({ type: 'SETUP_SESSION', payload: { expectedResponses, topic } }); + if (!topic.trim()) { + setTopicError(true); + return; + } + if (expectedResponses < 2 || expectedResponses > 12) { + setExpectedResponsesError(true); + return; + } + sendMessage({ type: 'SETUP_SESSION', payload: { expectedResponses, topic, description } }); }; const handleSubmitDesires = (desires: { wants: string[], accepts: string[], noGoes: string[], afraidToAsk: string }) => { @@ -48,21 +59,45 @@ const SessionPage = () => { {session.topic || session.sessionId} + {session.description && ( + + {session.description} + + )} {session.state === SessionState.SETUP && ( - Set Up Session + Set Up Desires Harmonization setTopic(e.target.value)} + onChange={(e) => { + setTopic(e.target.value); + setTopicError(!e.target.value.trim()); + }} + error={topicError} + helperText={topicError ? 'Topic is required' : ''} + /> + setDescription(e.target.value)} + inputProps={{ maxLength: 500 }} + helperText={`Provide more details about what should be addressed in this session (max 500 characters). ${description.length}/500`} /> { type="number" id="expectedResponses" value={expectedResponses} - onChange={(e) => setExpectedResponses(parseInt(e.target.value, 10))} + onChange={(e) => { + const value = parseInt(e.target.value, 10); + setExpectedResponses(value); + setExpectedResponsesError(value < 2 || value > 12); + }} + error={expectedResponsesError} + helperText={expectedResponsesError ? 'Must be an integer between 2 and 12' : 'Enter the number of participants expected (min 2, max 12)'} InputProps={{ - inputProps: { min: 1 } + inputProps: { min: 1, max: 12 } }} />