Skip to content

Commit

Permalink
Fix multi-user reaction updates
Browse files Browse the repository at this point in the history
  • Loading branch information
sofvanh committed Dec 4, 2024
1 parent 3148e88 commit e7415b3
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 74 deletions.
26 changes: 7 additions & 19 deletions backend/src/analysis/argumentScoreHandler.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
import { Score } from "../.shared/types";
import { ReactionForGraph, getReactionsForGraph } from "../db/operations/reactionOperations";
import { cosineSimilarityMatrix } from "../utils/math";


interface ArgumentScore {
argumentId: string;
consensusScore: number;
fragmentationScore: number;
clarityScore: number;
}

export async function getArgumentScores(graphId: string): Promise<ArgumentScore[]> {
export async function getArgumentScores(graphId: string): Promise<Map<string, Score>> {
const reactionArray: ReactionForGraph[] = await getReactionsForGraph(graphId);
const minimumVotesUser = 3;
const minimumVotesArgument = 2;
Expand Down Expand Up @@ -80,12 +74,7 @@ export async function getArgumentScores(graphId: string): Promise<ArgumentScore[
const userSimilarityMatrix: number[][] = cosineSimilarityMatrix(votingMatrix);

// Calculate the argument scores for each argument
const argumentScores: {
argumentId: string,
consensusScore: number,
fragmentationScore: number
clarityScore: number
}[] = [];
const argumentScores: Map<string, Score> = new Map();

argumentIndexMap.forEach((argumentIndex, argumentId) => {
//Identify users who voted on this argument
Expand Down Expand Up @@ -187,11 +176,10 @@ export async function getArgumentScores(graphId: string): Promise<ArgumentScore[
}
const argumentClarityScore = 1 - (unclearSum / uniquenessSum);

argumentScores.push({
argumentId,
consensusScore: argumentConsensusScore,
fragmentationScore: argumentFragmentationScore,
clarityScore: argumentClarityScore
argumentScores.set(argumentId, {
consensus: argumentConsensusScore,
fragmentation: argumentFragmentationScore,
clarity: argumentClarityScore
});
}
});
Expand Down
23 changes: 23 additions & 0 deletions backend/src/db/operations/argumentOperations.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Argument } from "../../.shared/types";
import { query } from "../db";
import { generateArgumentId } from "../idGenerator";
import { getReactionCountsForArgument } from "./reactionOperations";

export async function addArgument(
graphId: string,
Expand All @@ -13,4 +15,25 @@ export async function addArgument(
[id, graphId, statement, embedding, authorId]
);
return id;
}

export async function getArgument(
argumentId: string
): Promise<Argument | null> {
const result = await query('SELECT * FROM arguments WHERE id = $1', [argumentId]);
if (result.rows.length === 0) {
return null;
}

const reactionCounts = await getReactionCountsForArgument(argumentId);

const row = result.rows[0];
return {
id: row.id,
graphId: row.graph_id,
statement: row.statement,
embedding: row.embedding,
authorId: row.author_id,
reactionCounts: reactionCounts
};
}
34 changes: 4 additions & 30 deletions backend/src/db/operations/graphOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { query } from '../db';
import { generateGraphId } from '../idGenerator';
import { Argument, Edge, Graph } from '../../.shared/types';
import { getArgumentScores } from '../../analysis/argumentScoreHandler';
import { getReactionCounts } from './reactionOperations';

export async function createGraph(name: string, authorId: string): Promise<string> {
const id = generateGraphId();
Expand All @@ -25,35 +26,8 @@ export async function getGraphData(graphId: string): Promise<Graph> {

const argumentsResult = await query('SELECT * FROM arguments WHERE graph_id = $1', [graphId]);
const edgesResult = await query('SELECT * FROM edges WHERE graph_id = $1', [graphId]);

const reactionCountsResult = await query(
`SELECT argument_id, type, COUNT(*) as count
FROM reactions
WHERE argument_id IN (SELECT id FROM arguments WHERE graph_id = $1)
GROUP BY argument_id, type`,
[graphId]
);

const reactionCountsMap = new Map();
reactionCountsResult.rows.forEach((row: any) => {
if (!reactionCountsMap.has(row.argument_id)) {
reactionCountsMap.set(row.argument_id, { agree: 0, disagree: 0, unclear: 0 });
}
reactionCountsMap.get(row.argument_id)[row.type] = parseInt(row.count);
});

// TODO Just change getArgumentScores so that it already returns a map... We don't even need this ArgumentScore type (or we can change it so that Argument uses it directly)
const reactionCounts = await getReactionCounts(graphId);
const argumentScores = await getArgumentScores(graphId);
const scoresMap = new Map(
argumentScores.map(score => [
score.argumentId,
{
consensus: score.consensusScore,
fragmentation: score.fragmentationScore,
clarity: score.clarityScore
}
])
)

// TODO This is terrible, create types for db results already...
const args: Argument[] = argumentsResult.rows.map((row: { id: string; graph_id: string; statement: string; embedding: number[], author_id: string }) => ({
Expand All @@ -62,8 +36,8 @@ export async function getGraphData(graphId: string): Promise<Graph> {
statement: row.statement,
embedding: row.embedding,
authorId: row.author_id,
reactionCounts: reactionCountsMap.get(row.id) || { agree: 0, disagree: 0, unclear: 0 },
score: scoresMap.get(row.id)
reactionCounts: reactionCounts.get(row.id) || { agree: 0, disagree: 0, unclear: 0 },
score: argumentScores.get(row.id)
}));

const links: Edge[] = edgesResult.rows.map((row: { id: string; graph_id: string; source_id: string; target_id: string }) => ({
Expand Down
56 changes: 56 additions & 0 deletions backend/src/db/operations/reactionOperations.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { query } from '../db';
import { generateReactionId } from '../idGenerator';
import { ReactionCounts, UserReaction } from '../../.shared/types';

export async function addReaction(
userId: string,
Expand Down Expand Up @@ -61,4 +62,59 @@ export async function getReactionsForGraph(
argumentId: row.argument_id,
type: row.type
}));
}

export async function getReactionCounts(
graphId: string
): Promise<Map<string, ReactionCounts>> {
const reactionCountsResult = await query(
`SELECT argument_id, type, COUNT(*) as count
FROM reactions
WHERE argument_id IN (SELECT id FROM arguments WHERE graph_id = $1)
GROUP BY argument_id, type`,
[graphId]
);

const reactionCountsMap = new Map();
reactionCountsResult.rows.forEach((row: any) => {
if (!reactionCountsMap.has(row.argument_id)) {
reactionCountsMap.set(row.argument_id, { agree: 0, disagree: 0, unclear: 0 });
}
reactionCountsMap.get(row.argument_id)[row.type] = parseInt(row.count);
});

return reactionCountsMap;
}

export async function getReactionCountsForArgument(argumentId: string): Promise<ReactionCounts> {
const reactionCountsResult = await query(
`SELECT type, COUNT(*) as count
FROM reactions
WHERE argument_id = $1
GROUP BY type`,
[argumentId]
);

const reactionCounts: ReactionCounts = { agree: 0, disagree: 0, unclear: 0 };
reactionCountsResult.rows.forEach((row: { type: keyof ReactionCounts; count: string }) => {
reactionCounts[row.type] = parseInt(row.count);
});

return reactionCounts;
}

export async function getUserReactionForArgument(userId: string, argumentId: string): Promise<UserReaction> {
const userReactionResult = await query(
`SELECT type
FROM reactions
WHERE user_id = $1 AND argument_id = $2`,
[userId, argumentId]
);

const userReaction: UserReaction = {};
userReactionResult.rows.forEach((row: { type: keyof UserReaction }) => {
userReaction[row.type] = true;
});

return userReaction;
}
15 changes: 6 additions & 9 deletions backend/src/websocket/reactionHandler.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
import { Socket } from 'socket.io';
import { addReaction, removeReaction } from '../db/operations/reactionOperations';
import { getGraphDataWithUserReactions } from '../db/operations/graphOperations';
import { getGraphData, getGraphDataWithUserReactions } from '../db/operations/graphOperations';
import { query } from '../db/db';
import { sendReactionUpdate } from './updateHandler';


// TODO When scores are updated, send only the changed scores

export const handleAddReaction = async (
socket: Socket,
io: any,
{ argumentId, type }: { argumentId: string; type: 'agree' | 'disagree' | 'unclear' },
callback?: Function
) => {
if (!socket.data.user) {
console.log(`Failed to add reaction: No user data on socket. Argument ID: ${argumentId}, Type: ${type}, Socket ID: ${socket.id}`);
console.error(`Failed to add reaction: No user data on socket. Argument ID: ${argumentId}, Type: ${type}, Socket ID: ${socket.id}`);
callback?.({ success: false, error: 'Authentication required' });
return;
}

try {
const id = await addReaction(socket.data.user.id, argumentId, type);
const graphId = (await query('SELECT graph_id FROM arguments WHERE id = $1', [argumentId])).rows[0].graph_id;
const updatedGraph = await getGraphDataWithUserReactions(graphId, socket.data.user.id);
io.to(graphId).emit('graph update', updatedGraph); // TODO This breaks, because we're sending the current user's score to all users in the graph!
sendReactionUpdate(socket, io, graphId, argumentId);
callback?.({ success: true, id });
} catch (error) {
console.error('Error adding reaction:', error);
Expand All @@ -37,16 +35,15 @@ export const handleRemoveReaction = async (
callback?: Function
) => {
if (!socket.data.user) {
console.log(`Failed to remove reaction: No user data on socket. Argument ID: ${argumentId}, Type: ${type}, Socket ID: ${socket.id}`);
console.error(`Failed to remove reaction: No user data on socket. Argument ID: ${argumentId}, Type: ${type}, Socket ID: ${socket.id}`);
callback?.({ success: false, error: 'Authentication required' });
return;
}

try {
await removeReaction(socket.data.user.id, argumentId, type);
const graphId = (await query('SELECT graph_id FROM arguments WHERE id = $1', [argumentId])).rows[0].graph_id;
const updatedGraph = await getGraphDataWithUserReactions(graphId, socket.data.user.id);
io.to(graphId).emit('graph update', updatedGraph);
sendReactionUpdate(socket, io, graphId, argumentId);
callback?.({ success: true });
} catch (error) {
console.error('Error removing reaction:', error);
Expand Down
24 changes: 24 additions & 0 deletions backend/src/websocket/updateHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Socket } from "socket.io";
import { getArgumentScores } from "../analysis/argumentScoreHandler";
import { getReactionCountsForArgument, getUserReactionForArgument } from "../db/operations/reactionOperations";


export const sendReactionUpdate = async (
socket: Socket,
io: any,
graphId: string,
argumentId: string
) => {
// Send new UserReaction state to the user who performed the action
const userReaction = await getUserReactionForArgument(socket.data.user.id, argumentId);
socket.emit('user reaction update', { argumentId, userReaction })
// Send new ReactionCounts states to all users currently in the graph
const reactionCounts = await getReactionCountsForArgument(argumentId);
io.to(graphId).emit('argument reactions update', { argumentId, reactionCounts });
// Send the new argument scores to all users currently in the graph
// TODO Only send scores that changed
const newScores = await getArgumentScores(graphId);
// Convert Map to plain object before sending
const scoresObject = Object.fromEntries(newScores);
io.to(graphId).emit('graph scores update', scoresObject);
}
33 changes: 32 additions & 1 deletion frontend/src/hooks/useGraph.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { Graph, ForceGraphData, NodeData, LinkData } from '../shared/types';
import { Graph, ForceGraphData, NodeData, LinkData, Score, UserReaction, ReactionCounts } from '../shared/types';
import { useWebSocket } from '../contexts/WebSocketContext';
import { useAuth } from '../contexts/AuthContext';

Expand All @@ -19,10 +19,41 @@ export function useGraph(graphId: string) {
socket?.emit('join graph', graphId);
socket?.on('graph data', setGraph);
socket?.on('graph update', setGraph);
socket?.on('user reaction update', ({ argumentId, userReaction }: { argumentId: string, userReaction: UserReaction }) => {
setGraph(prevGraph => {
if (!prevGraph) return prevGraph;
const updatedArguments = prevGraph.arguments.map(arg =>
arg.id === argumentId ? { ...arg, userReaction } : arg
);
return { ...prevGraph, arguments: updatedArguments };
});
});
socket?.on('argument reactions update', ({ argumentId, reactionCounts }: { argumentId: string, reactionCounts: ReactionCounts }) => {
setGraph(prevGraph => {
if (!prevGraph) return prevGraph;
const updatedArguments = prevGraph.arguments.map(arg =>
arg.id === argumentId ? { ...arg, reactionCounts } : arg
);
return { ...prevGraph, arguments: updatedArguments };
});
});
socket?.on('graph scores update', (newScores: { [key: string]: Score }) => {
setGraph(prevGraph => {
if (!prevGraph) return prevGraph;
const updatedArguments = prevGraph.arguments.map(arg => ({
...arg,
score: newScores[arg.id] || arg.score
}));
return { ...prevGraph, arguments: updatedArguments };
});
});
return () => {
socket?.emit('leave graph', graphId);
socket?.off('graph data');
socket?.off('graph update');
socket?.off('user reaction update');
socket?.off('argument reactions update');
socket?.off('graph scores update');
}
}, [socket, graphId, user]);

Expand Down
36 changes: 21 additions & 15 deletions frontend/src/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,27 @@ export interface Argument {
statement: string;
embedding: number[];
authorId?: string;
reactionCounts?: {
agree: number;
disagree: number;
unclear: number;
};
userReaction?: {
agree?: boolean;
disagree?: boolean;
unclear?: boolean;
};
score?: {
consensus: number;
fragmentation: number;
clarity: number;
};
reactionCounts?: ReactionCounts;
userReaction?: UserReaction;
score?: Score;
}

export interface ReactionCounts {
agree: number;
disagree: number;
unclear: number;
}

export interface UserReaction {
agree?: boolean;
disagree?: boolean;
unclear?: boolean;
}

export interface Score {
consensus: number;
fragmentation: number;
clarity: number;
}

export interface Edge {
Expand Down

0 comments on commit e7415b3

Please sign in to comment.