From 64b97740dd412a0969a04af012947d8aeead86b3 Mon Sep 17 00:00:00 2001 From: tevko Date: Thu, 12 Dec 2024 14:39:57 -0600 Subject: [PATCH 01/16] report race condition check --- .gitignore | 1 + client-report/src/components/lists/uncertaintyNarrative.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b31a2938b..f0a8d416e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ build/ prod.env xids.csv preprod.env +.venv diff --git a/client-report/src/components/lists/uncertaintyNarrative.js b/client-report/src/components/lists/uncertaintyNarrative.js index 4200afa59..ceb115675 100644 --- a/client-report/src/components/lists/uncertaintyNarrative.js +++ b/client-report/src/components/lists/uncertaintyNarrative.js @@ -17,7 +17,7 @@ const UncertaintyNarrative = ({ narrative, model }) => { - if (!conversation || !narrative || !narrative?.uncertainty?.responseClaude) { + if (!conversation || !narrative || !narrative?.uncertainty?.responseClaude || !narrative?.uncertainty?.responseGemini) { return
Loading Uncertainty...
; } From 78264cb1135f0c477c383218bd7d415c5ad302a2 Mon Sep 17 00:00:00 2001 From: tevko Date: Thu, 12 Dec 2024 17:26:28 -0600 Subject: [PATCH 02/16] refactoring + tests --- client-report/package-lock.json | 16 +++ client-report/package.json | 1 + client-report/src/components/app.js | 2 +- .../{commentsGraph.js => commentsGraph.jsx} | 65 +++------ .../src/components/controls/controls.js | 55 -------- .../src/components/controls/controls.jsx | 46 +++++++ .../{matrix.js => matrix.jsx} | 114 +++++++--------- .../src/components/framework/checkbox.js | 129 ------------------ .../src/components/framework/checkbox.jsx | 118 ++++++++++++++++ .../components/framework/checkbox.test.jsx | 61 +++++++++ 10 files changed, 315 insertions(+), 292 deletions(-) rename client-report/src/components/commentsGraph/{commentsGraph.js => commentsGraph.jsx} (65%) delete mode 100644 client-report/src/components/controls/controls.js create mode 100644 client-report/src/components/controls/controls.jsx rename client-report/src/components/correlationMatrix/{matrix.js => matrix.jsx} (66%) delete mode 100644 client-report/src/components/framework/checkbox.js create mode 100644 client-report/src/components/framework/checkbox.jsx create mode 100644 client-report/src/components/framework/checkbox.test.jsx diff --git a/client-report/package-lock.json b/client-report/package-lock.json index b7fb0457b..28fec50a4 100644 --- a/client-report/package-lock.json +++ b/client-report/package-lock.json @@ -33,6 +33,7 @@ "@babel/preset-react": "~7.18.6", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.0.1", + "@testing-library/user-event": "^14.5.2", "babel-jest": "^29.7.0", "babel-loader": "~9.1.2", "compression-webpack-plugin": "^10.0.0", @@ -2550,6 +2551,7 @@ "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz", "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==", "dev": true, + "license": "MIT", "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", @@ -2611,6 +2613,20 @@ } } }, + "node_modules/@testing-library/user-event": { + "version": "14.5.2", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz", + "integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", diff --git a/client-report/package.json b/client-report/package.json index 6eeaae69e..41b75043a 100644 --- a/client-report/package.json +++ b/client-report/package.json @@ -20,6 +20,7 @@ "@babel/preset-react": "~7.18.6", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.0.1", + "@testing-library/user-event": "^14.5.2", "babel-jest": "^29.7.0", "babel-loader": "~9.1.2", "compression-webpack-plugin": "^10.0.0", diff --git a/client-report/src/components/app.js b/client-report/src/components/app.js index 76f175e75..4c8e24d5d 100644 --- a/client-report/src/components/app.js +++ b/client-report/src/components/app.js @@ -24,7 +24,7 @@ import ParticipantGroups from "./lists/participantGroups"; import ParticipantsGraph from "./participantsGraph/participantsGraph"; // import BoxPlot from "./boxPlot/boxPlot"; import Beeswarm from "./beeswarm/beeswarm.jsx"; -import Controls from "./controls/controls"; +import Controls from "./controls/controls.jsx"; import net from "../util/net"; diff --git a/client-report/src/components/commentsGraph/commentsGraph.js b/client-report/src/components/commentsGraph/commentsGraph.jsx similarity index 65% rename from client-report/src/components/commentsGraph/commentsGraph.js rename to client-report/src/components/commentsGraph/commentsGraph.jsx index 02009929b..0713c1195 100644 --- a/client-report/src/components/commentsGraph/commentsGraph.js +++ b/client-report/src/components/commentsGraph/commentsGraph.jsx @@ -1,35 +1,22 @@ // Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . -import React from "react"; -// import _ from "lodash"; +import React, { setState } from "react"; import * as globals from "../globals"; import graphUtil from "../../util/graphUtil"; import Axes from "../graphAxes"; import Comments from "./comments"; -// const TextSegment = ({ t, i }) => ( -// -// {t} -// -// ); +const CommentsGraph = ({ comments, math, badTids, height, report, groupNames, formatTid, repfulAgreeTidsByGroup, renderHeading, voteColors }) => { + const [viewer, setViewer] = useState(null); + const [selectedComment, setSelectedComment] = useState(null); -class CommentsGraph extends React.Component { - constructor(props) { - super(props); - this.Viewer = null; - this.state = { - selectedComment: null, - }; - } - - handleCommentClick(selectedComment) { + const handleCommentClick = (sc) => { return () => { - this.setState({ selectedComment }); + setSelectedComment(sc); }; } - render() { - if (!this.props.math) { + if (math) { return null; } @@ -39,11 +26,9 @@ class CommentsGraph extends React.Component { commentsPoints, xCenter, yCenter, - // baseClustersScaled, commentScaleupFactorX, commentScaleupFactorY, - // hulls, - } = graphUtil(this.props.comments, this.props.math, this.props.badTids); + } = graphUtil(comments, math, badTids); return (
@@ -65,16 +50,16 @@ class CommentsGraph extends React.Component {

- {this.state.selectedComment - ? "#" + this.state.selectedComment.tid + ". " + this.state.selectedComment.txt + {selectedComment + ? "#" + selectedComment.tid + ". " + selectedComment.txt : "Click a statement, identified by its number, to explore regions of the graph."}

{/* Comment https://bl.ocks.org/mbostock/7555321 */} @@ -87,26 +72,15 @@ class CommentsGraph extends React.Component { textAnchor="middle" > - - {/* {} */} - {/* {this.props.math["group-clusters"].map((cluster, i) => { - return ( Renzi Supporters ) - }) : null} */} - {/* { - hulls.map((hull) => { - let gid = hull.group[0].gid; - if (_.isNumber(this.props.showOnlyGroup)) { - if (gid !== this.props.showOnlyGroup) { - return ""; - } - } - return - }) - } */} + {commentsPoints ? ( ); - } } export default CommentsGraph; diff --git a/client-report/src/components/controls/controls.js b/client-report/src/components/controls/controls.js deleted file mode 100644 index 630c6abd7..000000000 --- a/client-report/src/components/controls/controls.js +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . - - -import Checkbox from '../framework/checkbox'; -import React from "react"; -import settings from "../../settings"; - -class Controls extends React.Component { - - constructor(props) { - super(props); - this.autoRefreshEnabledRef = React.createRef(); - this.colorBlindModeRef = React.createRef(); - } - - checkboxGroupChanged(newVal) { - if (newVal) { - this.props.onAutoRefreshEnabled(); - } else { - this.props.onAutoRefreshDisabled(); - } - } - - // UNSAFE_componentWillMount() { - // } - - render() { - return ( -
- - -
- ); - } - -} - // - -export default Controls; diff --git a/client-report/src/components/controls/controls.jsx b/client-report/src/components/controls/controls.jsx new file mode 100644 index 000000000..328e26bd5 --- /dev/null +++ b/client-report/src/components/controls/controls.jsx @@ -0,0 +1,46 @@ +// Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . + + +import Checkbox from '../framework/checkbox.jsx'; +import React from "react"; +import settings from "../../settings"; + +const Controls = ({ onAutoRefreshEnabled, handleColorblindModeClick, colorBlindMode, onAutoRefreshDisabled, autoRefreshEnabled, voteColors}) => { + + const autoRefreshEnabledRef = React.createRef(); + const colorBlindModeRef = React.createRef(); + + const checkboxGroupChanged = (newVal) => { + if (newVal) { + onAutoRefreshEnabled(); + } else { + onAutoRefreshDisabled(); + } + } + + return ( +
+ + +
+ ); + +} + +export default Controls; diff --git a/client-report/src/components/correlationMatrix/matrix.js b/client-report/src/components/correlationMatrix/matrix.jsx similarity index 66% rename from client-report/src/components/correlationMatrix/matrix.js rename to client-report/src/components/correlationMatrix/matrix.jsx index a55e7bad2..5fbf5cbfb 100644 --- a/client-report/src/components/correlationMatrix/matrix.js +++ b/client-report/src/components/correlationMatrix/matrix.jsx @@ -1,40 +1,41 @@ // Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . -import React from "react"; +import React, { useState } from "react"; import _ from "lodash"; import * as globals from "../globals"; -var leftOffset = 34; -var topOffset = 60; +const leftOffset = 34; +const topOffset = 60; -var scale = d3.scaleLinear().domain([-1, 1]).range([0, 1]); +const scale = d3.scaleLinear().domain([-1, 1]).range([0, 1]); const square = 20; -class Matrix extends React.Component { - onMouseEnterCell(row, column, correlation) { - this.setState({ - mouseOverRow: row, - mouseOverColumn: column, - mouseOverCorrelation: correlation, - }); - } - onMouseExitCell(/*row, column*/) { - this.setState({ - mouseOverRow: null, - mouseOverColumn: null, - mouseOverCorrelation: null, - }); +const Matrix = ({ comments, tids, probabilities, title, error }) => { + const [mouseOverRow, setMouseOverRow] = useState(null); + const [mouseOverColumn, setMouseOverColumn] = useState(null); + const [mouseOverCorrelation, setMouseOverCorrelation] = useState(null); + + const onMouseEnterCell = (row, column, correlation) => { + setMouseOverRow(row); + setMouseOverColumn(column); + setMouseOverCorrelation(correlation); + }; + + const onMouseExitCell = (/*row, column*/) => { + setMouseOverRow(null); + setMouseOverColumn(null); + setMouseOverCorrelation(null); } - makeRect(comment, row, column) { + const makeRect = (comment, row, column) => { return ( { - return this.onMouseEnterCell(row, column, comment); + return onMouseEnterCell(row, column, comment); }} width={square} height={square} @@ -56,7 +57,7 @@ class Matrix extends React.Component { ); } - makeColumn(comments, row) { + const makeColumn = (comments, row) => { return comments.map((comment, column) => { let markup = null; if (column < row) { @@ -66,26 +67,26 @@ class Matrix extends React.Component { {/* this translate places the top text labels where they should go, rotated */} {/* this translate places the columns where they should go, and creates a gutter */} - {this.makeRect(comment, row, column)} + {makeRect(comment, row, column)} ); } else if (column === row) { - const comment = _.find(this.props.comments, (comment) => { - return comment.tid === this.props.tids[column]; + const comment = comments.find(comment => { + return comment.tid === tids[column]; }); markup = ( { - return this.onMouseExitCell(); + return onMouseExitCell(); }} onMouseLeave={() => { - return this.onMouseExitCell(); + return onMouseExitCell(); }} transform={"translate(" + (column * square + 10) + ", 46), rotate(315)"} fill={ - column === this.state.mouseOverColumn || column === this.state.mouseOverRow + column === mouseOverColumn || column === mouseOverRow ? "rgba(0,0,0,1)" : "rgba(0,0,0,0.5)" } @@ -104,31 +105,20 @@ class Matrix extends React.Component { }); } - makeRow(comments, row) { - // {/* this translate seperates the rows */} - // - // {this.props.formatTid(this.props.tids[row])} - // + const makeRow = (comments, row) => { return ( {/* this translate moves just the colored squares over to make a gutter, not the text */} - {this.makeColumn(comments, row)} + {makeColumn(comments, row)} ); } - renderMatrix() { - // console.log("mouseOverCorrelation", this.state.mouseOverCorrelation) - let side = this.props.probabilities.length * square + 200; + const renderMatrix = () => { + let side = probabilities.length * square + 200; return (
-

{this.props.title}

+

{title}

What is the chance that a participant who agreed (or disagreed) with a given comment also agreed (or disagreed) with another given comment? @@ -146,44 +136,45 @@ class Matrix extends React.Component { { - return this.onMouseExitCell(); + return onMouseExitCell(); }} onMouseLeave={() => { - return this.onMouseExitCell(); + return onMouseExitCell(); }} width={side} height={side} /> - {!this.state.mouseOverCorrelation ? ( + {!mouseOverCorrelation ? ( " " ) : ( {`${ - Math.round(this.state.mouseOverCorrelation * 1000) / 10 + Math.round(mouseOverCorrelation * 1000) / 10 }% chance of casting the same vote on these two statements`} )} - {this.props.probabilities.map((comments, row) => { - return {this.makeRow(comments, row)}; + {probabilities.map((comments, row) => { + return {makeRow(comments, row)}; })}

); - } - renderError(err) { + }; + + const renderError = (err) => { return (
error loading matrix
@@ -191,17 +182,18 @@ class Matrix extends React.Component {
); } - renderLoading() { + + const renderLoading = () => { return
loading matrix... (may take up to a minute)
; } - render() { - if (this.props.error) { - return this.renderError(); - } else if (this.props.probabilities) { - return this.renderMatrix(); - } else { - return this.renderLoading(); - } + + + if (error) { + return renderError(); + } else if (probabilities) { + return renderMatrix(); + } else { + return renderLoading(); } } diff --git a/client-report/src/components/framework/checkbox.js b/client-report/src/components/framework/checkbox.js deleted file mode 100644 index f63fe4518..000000000 --- a/client-report/src/components/framework/checkbox.js +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . - -import React from "react"; -import Color from "color"; -import settings from "../../settings"; - - -export default class Checkbox extends React.Component { - constructor(props) { - super(props); - this.state = { - checked: this.props.checked, - active: false - }; - } - - static defaultProps = { - checked: true, - clickHandler: (x) => { return x }, - color: settings.darkGray, - - } - - activeHandler () { - this.setState({ active: !this.state.active }); - } - - clickHandler () { - var newState = !this.state.checked; - this.setState({ checked: newState }); - if (this.props.clickHandler) { this.props.clickHandler(newState); } - } - - getWrapperStyles () { - return { - display: "block", - marginBottom: 10, - position: "relative" - }; - } - - getLabelWrapperStyles () { - return { - color: this.props.labelWrapperColor, - cursor: "pointer", - display: "inline-block", - fontFamily: settings.fontFamilySansSerif, - fontSize: 14, - fontWeight: 400, - lineHeight: "20px", - paddingLeft: 22, - // "-webkit-user-select": "none" - }; - } - - getCheckboxStyles () { - const activeColor = Color(this.props.color).lighten(0.2).hex(); - - return { - base: { - backgroundColor: settings.gray, - borderRadius: 2, - display: "inline-block", - height: 12, - left: -17, - position: "relative", - top: 1, - transition: "background-color ease .3s", - width: 12 - }, - checked: { - backgroundColor: this.props.color - }, - active: { - backgroundColor: activeColor - } - }; - } - - getLabelStyles () { - return { - display: "inline", - left: -12, - marginRight: 4, - position: "relative" - }; - } - - getHelpTextStyles () { - return { - color: "#ccc", - cursor: "pointer", - display: "inline", - fontFamily: settings.fontFamilySansSerif, - fontSize: 12, - fontWeight: 200, - lineHeight: "20px", - marginLeft: 5 - }; - } - - render () { - const {base, checked, active} = this.getCheckboxStyles(); - const styles = Object.assign({}, base, this.state.checked ? checked : {}, this.state.active ? active : {}); - console.log(styles); - - return ( -
- - - - - {this.props.label} - {this.props.helpText ? ( - - ({this.props.helpText}) - - ) : null } - - -
- ); - } -} diff --git a/client-report/src/components/framework/checkbox.jsx b/client-report/src/components/framework/checkbox.jsx new file mode 100644 index 000000000..c7feea945 --- /dev/null +++ b/client-report/src/components/framework/checkbox.jsx @@ -0,0 +1,118 @@ +// Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . + +import React, { useState } from "react"; +import Color from "color"; +import settings from "../../settings"; + + +const Checkbox = ({ isChecked, color = settings.darkGray, clickHandler = (x) => { return x }, labelWrapperColor, label, helpText }) => { + + const [checked, setChecked] = useState(isChecked); + const [active, setActive] = useState(false); + + const activeHandler = () => { + setActive(a => !a); + }; + + const clickHandlerInternal = () => { + const newState = !checked; + setChecked(newState); + if (clickHandler) { clickHandler(newState); } + } + + const getWrapperStyles = () => { + return { + display: "block", + marginBottom: 10, + position: "relative" + }; + } + + const getLabelWrapperStyles = () => { + return { + color: labelWrapperColor, + cursor: "pointer", + display: "inline-block", + fontFamily: settings.fontFamilySansSerif, + fontSize: 14, + fontWeight: 400, + lineHeight: "20px", + paddingLeft: 22, + }; + } + + const getCheckboxStyles = () => { + const activeColor = Color(color).lighten(0.2).hex(); + + return { + base: { + backgroundColor: settings.gray, + borderRadius: 2, + display: "inline-block", + height: 12, + left: -17, + position: "relative", + top: 1, + transition: "background-color ease .3s", + width: 12 + }, + checkedStyle: { + backgroundColor: color + }, + activeStyle: { + backgroundColor: activeColor + } + }; + } + + const getLabelStyles = () => { + return { + display: "inline", + left: -12, + marginRight: 4, + position: "relative" + }; + } + + const getHelpTextStyles = () => { + return { + color: "#ccc", + cursor: "pointer", + display: "inline", + fontFamily: settings.fontFamilySansSerif, + fontSize: 12, + fontWeight: 200, + lineHeight: "20px", + marginLeft: 5 + }; + } + + + const {base, checkedStyle, activeStyle} = getCheckboxStyles(); + const styles = Object.assign({}, base, checked ? checkedStyle : {}, active ? activeStyle : {}); + + return ( +
+ + + + + {label} + {helpText ? ( + + ({helpText}) + + ) : null } + + +
+ ); +} + +export default Checkbox; diff --git a/client-report/src/components/framework/checkbox.test.jsx b/client-report/src/components/framework/checkbox.test.jsx new file mode 100644 index 000000000..a84ea9248 --- /dev/null +++ b/client-report/src/components/framework/checkbox.test.jsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import Checkbox from './checkbox'; +import settings from '../../settings'; // Assuming settings is imported correctly + +import '@testing-library/jest-dom'; + +describe('Checkbox Component', () => { + it('renders checkbox with correct initial state and styles', () => { + const label = 'Test Label'; + const isChecked = true; + const color = 'blue'; // Override default color + + render(); + + const checkbox = screen.getByTestId('checkbox'); + const labelText = screen.getByText(label); + + expect(checkbox).toHaveStyle({ backgroundColor: color }); + }); + + it('toggles checkbox state and calls clickHandler on click', () => { + const label = 'Test Label'; + const isChecked = false; + const mockClickHandler = jest.fn(); + + render(); + + const checkbox = screen.getByRole('checkbox'); + + fireEvent.click(checkbox); + + + expect(mockClickHandler).toHaveBeenCalledTimes(1); + expect(mockClickHandler).toHaveBeenCalledWith(true); // New state after click + }); + + it('shows help text if provided', () => { + const label = 'Test Label'; + const isChecked = true; + const helpText = 'This is some help text'; + + render(); + + const helpTextElement = screen.getByText(/This is some help text/); + + expect(helpTextElement).toBeInTheDocument(); + }); + + it('hides help text if not provided', () => { + const label = 'Test Label'; + const isChecked = true; + + render(); + + const helpTextElement = screen.queryByText(/help text/i); // Use queryByText for potential absence + + expect(helpTextElement).not.toBeInTheDocument(); + }); + +}); \ No newline at end of file From edc5c7cb3ddc314362b8619ffd7a7e918029559e Mon Sep 17 00:00:00 2001 From: tevko Date: Thu, 12 Dec 2024 19:47:12 -0600 Subject: [PATCH 03/16] finish framework folder --- client-report/src/components/app.js | 4 +- .../framework/{Footer.js => Footer.jsx} | 2 +- .../src/components/framework/flex.js | 97 -------- .../src/components/framework/flex.jsx | 119 ++++++++++ .../framework/{heading.js => heading.jsx} | 2 +- .../framework/{legend.js => legend.jsx} | 0 .../components/framework/logoLargeShort.js | 57 ----- .../components/framework/logoLargeShort.jsx | 58 +++++ .../src/components/framework/logoSmallLong.js | 218 ------------------ .../components/framework/logoSmallLong.jsx | 216 +++++++++++++++++ .../src/components/lists/majorityStrict.js | 2 +- 11 files changed, 398 insertions(+), 377 deletions(-) rename client-report/src/components/framework/{Footer.js => Footer.jsx} (95%) delete mode 100644 client-report/src/components/framework/flex.js create mode 100644 client-report/src/components/framework/flex.jsx rename client-report/src/components/framework/{heading.js => heading.jsx} (97%) rename client-report/src/components/framework/{legend.js => legend.jsx} (100%) delete mode 100644 client-report/src/components/framework/logoLargeShort.js create mode 100644 client-report/src/components/framework/logoLargeShort.jsx delete mode 100644 client-report/src/components/framework/logoSmallLong.js create mode 100644 client-report/src/components/framework/logoSmallLong.jsx diff --git a/client-report/src/components/app.js b/client-report/src/components/app.js index 4c8e24d5d..26ff7c258 100644 --- a/client-report/src/components/app.js +++ b/client-report/src/components/app.js @@ -12,8 +12,8 @@ import * as globals from "./globals"; import URLs from "../util/url"; import DataUtils from "../util/dataUtils"; // import Matrix from "./correlationMatrix/matrix"; -import Heading from "./framework/heading"; -import Footer from "./framework/Footer"; +import Heading from "./framework/heading.jsx"; +import Footer from "./framework/Footer.jsx"; import Overview from "./overview"; import MajorityStrict from "./lists/majorityStrict"; import Uncertainty from "./lists/uncertainty"; diff --git a/client-report/src/components/framework/Footer.js b/client-report/src/components/framework/Footer.jsx similarity index 95% rename from client-report/src/components/framework/Footer.js rename to client-report/src/components/framework/Footer.jsx index 70e5d7e0c..ac6ce158e 100644 --- a/client-report/src/components/framework/Footer.js +++ b/client-report/src/components/framework/Footer.jsx @@ -1,7 +1,7 @@ // Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . import React from "react"; -import LargeLogo from "./logoLargeShort"; +import LargeLogo from "./logoLargeShort.jsx"; const Footer = (/*{conversation}*/) => { return ( diff --git a/client-report/src/components/framework/flex.js b/client-report/src/components/framework/flex.js deleted file mode 100644 index 84c2ffdec..000000000 --- a/client-report/src/components/framework/flex.js +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . - -import PropTypes from "prop-types"; -import React from "react"; - -/** - - flex-direction: row | row-reverse | column | column-reverse; - flex-wrap: nowrap | wrap | wrap-reverse; - justify-content: flex-start | flex-end | center | space-between | space-around; - align-items: flex-start | flex-end | center | baseline | stretch; - align-content: flex-start | flex-end | center | space-between | space-around | stretch; - flex is growShrinkBasis - -**/ - -class Flex extends React.Component { - static propTypes = { - direction: PropTypes.oneOf([ - "row", "rowReverse", "column", "columnReverse" - ]), - wrap: PropTypes.oneOf([ - "nowrap", "wrap", "wrap-reverse" - ]), - justifyContent: PropTypes.oneOf([ - "flex-start", "flex-end", "center", "space-between", "space-around" - ]), - alignItems: PropTypes.oneOf([ - "flex-start", "flex-end", "center", "baseline", "stretch" - ]), - alignContent: PropTypes.oneOf([ - "flex-start", "flex-end", "center", "space-between", "space-around", "stretch" - ]), - grow: PropTypes.number, - shrink: PropTypes.number, - basis: PropTypes.string, - order: PropTypes.number, - alignSelf: PropTypes.oneOf([ - "auto", "flex-start", "flex-end", "center", "baseline", "stretch" - ]), - styleOverrides: PropTypes.object, - children: PropTypes.node, - clickHandler: PropTypes.func, - } - static defaultProps = { - direction: "row", - wrap: "nowrap", - justifyContent: "center", - alignItems: "center", - alignContent: "stretch", - grow: 0, - shrink: 1, - basis: "auto", - alignSelf: "auto", - order: 0, - styleOverrides: {} - } - getStyles() { - return { - base: { - display: "flex", - flexDirection: this.props.direction, - flexWrap: this.props.wrap, - justifyContent: this.props.justifyContent, - alignItems: this.props.alignItems, - alignContent: this.props.alignContent, - order: this.props.order, - flexGrow: this.props.grow, - flexShrink: this.props.shrink, - flexBasis: this.props.basis, - alignSelf: this.props.alignSelf, - }, - styleOverrides: this.props.styleOverrides - } - } - - render() { - const {base, styleOverrides} = this.getStyles(); - const styles = Object.assign({}, base, styleOverrides); - console.log(styles) - - return ( -
- {/* style={[ - styles.base, - styles.styleOverrides - ]} */} - {this.props.children} -
- ); - } -} - -export default Flex; diff --git a/client-report/src/components/framework/flex.jsx b/client-report/src/components/framework/flex.jsx new file mode 100644 index 000000000..f6b130b73 --- /dev/null +++ b/client-report/src/components/framework/flex.jsx @@ -0,0 +1,119 @@ +// Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . + +import PropTypes from "prop-types"; +import React from "react"; + +/** + + flex-direction: row | row-reverse | column | column-reverse; + flex-wrap: nowrap | wrap | wrap-reverse; + justify-content: flex-start | flex-end | center | space-between | space-around; + align-items: flex-start | flex-end | center | baseline | stretch; + align-content: flex-start | flex-end | center | space-between | space-around | stretch; + flex is growShrinkBasis + +**/ + +const Flex = ({ + direction = "row", + wrap = "nowrap", + justifyContent = "center", + alignItems = "center", + alignContent = "stretch", + grow = 0, + shrink = 1, + basis = "auto", + alignSelf = "auto", + order = 0, + styleOverrides = {}, + children, + clickHandler, +}) => { + const getStyles = () => ({ + base: { + display: "flex", + flexDirection: direction, + flexWrap: wrap, + justifyContent, + alignItems, + alignContent, + order, + flexGrow: grow, + flexShrink: shrink, + flexBasis: basis, + alignSelf, + }, + styleOverrides, + }); + + const styles = { ...getStyles().base, ...getStyles().styleOverrides }; + + return ( +
+ {children} +
+ ); +}; + +Flex.propTypes = { + direction: PropTypes.oneOf([ + "row", + "rowReverse", + "column", + "columnReverse", + ]), + wrap: PropTypes.oneOf(["nowrap", "wrap", "wrap-reverse"]), + justifyContent: PropTypes.oneOf([ + "flex-start", + "flex-end", + "center", + "space-between", + "space-around", + ]), + alignItems: PropTypes.oneOf([ + "flex-start", + "flex-end", + "center", + "baseline", + "stretch", + ]), + alignContent: PropTypes.oneOf([ + "flex-start", + "flex-end", + "center", + "space-between", + "space-around", + "stretch", + ]), + grow: PropTypes.number, + shrink: PropTypes.number, + basis: PropTypes.string, + order: PropTypes.number, + alignSelf: PropTypes.oneOf([ + "auto", + "flex-start", + "flex-end", + "center", + "baseline", + "stretch", + ]), + styleOverrides: PropTypes.object, + children: PropTypes.node, + clickHandler: PropTypes.func, +}; + +Flex.defaultProps = { + direction: "row", + wrap: "nowrap", + justifyContent: "center", + alignItems: "center", + alignContent: "stretch", + grow: 0, + shrink: 1, + basis: "auto", + alignSelf: "auto", + order: 0, + styleOverrides: {}, +}; + +export default Flex; diff --git a/client-report/src/components/framework/heading.js b/client-report/src/components/framework/heading.jsx similarity index 97% rename from client-report/src/components/framework/heading.js rename to client-report/src/components/framework/heading.jsx index 30a5716b0..ff89af6e7 100644 --- a/client-report/src/components/framework/heading.js +++ b/client-report/src/components/framework/heading.jsx @@ -1,7 +1,7 @@ // Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . import React from "react"; -import SmallLogo from "./logoSmallLong"; +import SmallLogo from "./logoSmallLong.jsx"; import Url from "../../util/url"; const urlPrefix = Url.urlPrefix; diff --git a/client-report/src/components/framework/legend.js b/client-report/src/components/framework/legend.jsx similarity index 100% rename from client-report/src/components/framework/legend.js rename to client-report/src/components/framework/legend.jsx diff --git a/client-report/src/components/framework/logoLargeShort.js b/client-report/src/components/framework/logoLargeShort.js deleted file mode 100644 index d04da88dd..000000000 --- a/client-report/src/components/framework/logoLargeShort.js +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . - -import React from "react"; - -class PolisLogo extends React.Component { - styles() { - return { - link: { - textDecoration: "none", - cursor: "pointer", - padding: "8px 0px 4px 10px" - } - } - } - render() { - return ( - - - - - - - - - p. - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); - } -} - -export default PolisLogo; diff --git a/client-report/src/components/framework/logoLargeShort.jsx b/client-report/src/components/framework/logoLargeShort.jsx new file mode 100644 index 000000000..a245b0b2c --- /dev/null +++ b/client-report/src/components/framework/logoLargeShort.jsx @@ -0,0 +1,58 @@ +// Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . + +import React from "react"; + +const PolisLogo = ({ invert = false }) => { + const styles = { + link: { + textDecoration: "none", + cursor: "pointer", + padding: "8px 0px 4px 10px", + }, + }; + + const svgContent = ( + + + + + + + + p. + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + + return ( + + {svgContent} + + ); +}; + +export default PolisLogo; diff --git a/client-report/src/components/framework/logoSmallLong.js b/client-report/src/components/framework/logoSmallLong.js deleted file mode 100644 index ea94b90a0..000000000 --- a/client-report/src/components/framework/logoSmallLong.js +++ /dev/null @@ -1,218 +0,0 @@ -// Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . - -import React from "react"; - -class PolisLogo extends React.Component { - render() { - return ( - - - - - - - - - - p. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); - } -} - -export default PolisLogo; diff --git a/client-report/src/components/framework/logoSmallLong.jsx b/client-report/src/components/framework/logoSmallLong.jsx new file mode 100644 index 000000000..5ed4352a3 --- /dev/null +++ b/client-report/src/components/framework/logoSmallLong.jsx @@ -0,0 +1,216 @@ +// Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . + +import React from "react"; + +const PolisLogo = () => { + return ( + + + + + + + + + + p. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +export default PolisLogo; diff --git a/client-report/src/components/lists/majorityStrict.js b/client-report/src/components/lists/majorityStrict.js index a379beed1..e3373d71b 100644 --- a/client-report/src/components/lists/majorityStrict.js +++ b/client-report/src/components/lists/majorityStrict.js @@ -3,7 +3,7 @@ import React from "react"; import * as globals from "../globals"; import CommentList from "./commentList"; -import Legend from "../framework/legend"; +import Legend from "../framework/legend.jsx"; const MajorityStrict = ({ conversation, From 629a66653f8923edb1fa9883d5b0d1e8f981047f Mon Sep 17 00:00:00 2001 From: tevko Date: Thu, 12 Dec 2024 21:56:00 -0600 Subject: [PATCH 04/16] refactor commentsModeratedIn + jest test --- client-report/package-lock.json | 1 - client-report/src/components/app.js | 2 +- .../lists/allCommentsModeratedIn.js | 97 ------------------- .../lists/allCommentsModeratedIn.jsx | 61 ++++++++++++ .../lists/allCommentsModeratedIn.test.jsx | 48 +++++++++ .../src/components/lists/commentList.js | 3 + 6 files changed, 113 insertions(+), 99 deletions(-) delete mode 100644 client-report/src/components/lists/allCommentsModeratedIn.js create mode 100644 client-report/src/components/lists/allCommentsModeratedIn.jsx create mode 100644 client-report/src/components/lists/allCommentsModeratedIn.test.jsx diff --git a/client-report/package-lock.json b/client-report/package-lock.json index 28fec50a4..b32d43ac0 100644 --- a/client-report/package-lock.json +++ b/client-report/package-lock.json @@ -2551,7 +2551,6 @@ "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz", "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==", "dev": true, - "license": "MIT", "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", diff --git a/client-report/src/components/app.js b/client-report/src/components/app.js index 26ff7c258..9c4a0180f 100644 --- a/client-report/src/components/app.js +++ b/client-report/src/components/app.js @@ -18,7 +18,7 @@ import Overview from "./overview"; import MajorityStrict from "./lists/majorityStrict"; import Uncertainty from "./lists/uncertainty"; import UncertaintyNarrative from "./lists/uncertaintyNarrative"; -import AllCommentsModeratedIn from "./lists/allCommentsModeratedIn"; +import AllCommentsModeratedIn from "./lists/allCommentsModeratedIn.jsx"; import ParticipantGroups from "./lists/participantGroups"; // import CommentsGraph from "./commentsGraph/commentsGraph"; import ParticipantsGraph from "./participantsGraph/participantsGraph"; diff --git a/client-report/src/components/lists/allCommentsModeratedIn.js b/client-report/src/components/lists/allCommentsModeratedIn.js deleted file mode 100644 index 5c14fe71f..000000000 --- a/client-report/src/components/lists/allCommentsModeratedIn.js +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . - -import React from "react"; -import _ from "lodash"; -import CommentList from "./commentList"; -import * as globals from "../globals"; - - -function sortByTid(comments) { - return _.map(comments, (comment) => comment.tid).sort((a, b) => a - b); -} - -function sortByVoteCount(comments) { - return _.map(_.reverse(_.sortBy(comments, "count")), (c) => {return c.tid;}); -} -function sortByGroupAwareConsensus(comments) { - return _.map(_.reverse(_.sortBy(comments, (c) => {return c["group-aware-consensus"];})), (c) => {return c.tid;}); -} -function sortByPctAgreed(comments) { - return _.map(_.reverse(_.sortBy(comments, (c) => {return c["pctAgreed"];})), (c) => {return c.tid;}); -} -function sortByPctDisagreed(comments) { - return _.map(_.reverse(_.sortBy(comments, (c) => {return c["pctDisagreed"];})), (c) => {return c.tid;}); -} -function sortByPctPassed(comments) { - return _.map(_.reverse(_.sortBy(comments, (c) => {return c["pctPassed"];})), (c) => {return c.tid;}); -} - -class allCommentsModeratedIn extends React.Component { - - constructor(props) { - super(props); - this.state = { - sortStyle: globals.allCommentsSortDefault, - }; - } - - onSortChanged(event) { - this.setState({ - sortStyle: event.target.value, - }); - } - - render() { - - if (!this.props.conversation) { - return
Loading allCommentsModeratedIn...
- } - - let sortFunction = null; - if (this.state.sortStyle === "tid") { - sortFunction = sortByTid; - } else if (this.state.sortStyle === "numvotes") { - sortFunction = sortByVoteCount; - } else if (this.state.sortStyle === "consensus") { - sortFunction = sortByGroupAwareConsensus; - } else if (this.state.sortStyle === "pctAgreed") { - sortFunction = sortByPctAgreed; - } else if (this.state.sortStyle === "pctDisagreed") { - sortFunction = sortByPctDisagreed; - } else if (this.state.sortStyle === "pctPassed") { - sortFunction = sortByPctPassed; - } else { - console.error('missing sort function', this.state.sortStyle); - } - - return ( -
-

All statements

-

- Group votes across all statements, excluding those statements which were moderated out. -

- - -
- -
-
- ); - } -} - -export default allCommentsModeratedIn; diff --git a/client-report/src/components/lists/allCommentsModeratedIn.jsx b/client-report/src/components/lists/allCommentsModeratedIn.jsx new file mode 100644 index 000000000..bdbd68751 --- /dev/null +++ b/client-report/src/components/lists/allCommentsModeratedIn.jsx @@ -0,0 +1,61 @@ +// Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . + +import React, { useState } from "react"; +import CommentList from "./commentList"; +import * as globals from "../globals"; + + +const sortFunctions = { + tid: (comments) => comments.sort((a, b) => a.tid - b.tid).map(c => c.tid), + numvotes: (comments) => comments.sort((a, b) => b.count - a.count).map(c => c.tid), // Descending order for numvotes + consensus: (comments) => comments.sort((a, b) => b["group-aware-consensus"] - a["group-aware-consensus"]).map(c => c.tid), + pctAgreed: (comments) => comments.sort((a, b) => b["pctAgreed"] - a["pctAgreed"]).map(c => c.tid), + pctDisagreed: (comments) => comments.sort((a, b) => b["pctDisagreed"] - a["pctDisagreed"]).map(c => c.tid), + pctPassed: (comments) => comments.sort((a, b) => b["pctPassed"] - a["pctPassed"]).map(c => c.tid), +}; + +const allCommentsModeratedIn = ({ conversation, ptptCount, math, formatTid, comments, voteColors }) => { + + const [sortStyle, setSortStyle] = useState(globals.allCommentsSortDefault) + + const onSortChanged = (event) => { + setSortStyle(event.target.value) + }; + + + if (!conversation) { + return
Loading allCommentsModeratedIn...
+ } + + const sortFunction = sortFunctions[sortStyle] || sortFunctions["tid"]; + + return ( +
+

All statements

+

+ Group votes across all statements, excluding those statements which were moderated out. +

+ + +
+ +
+
+ ); +} + +export default allCommentsModeratedIn; diff --git a/client-report/src/components/lists/allCommentsModeratedIn.test.jsx b/client-report/src/components/lists/allCommentsModeratedIn.test.jsx new file mode 100644 index 000000000..497deed09 --- /dev/null +++ b/client-report/src/components/lists/allCommentsModeratedIn.test.jsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import AllCommentsModeratedIn from './allCommentsModeratedIn.jsx'; +import CommentList from './commentList.js'; + +jest.mock('./commentList', () => ({ + __esModule: true, + default: jest.fn(), +})); + +describe('allCommentsModeratedIn component', () => { + const mockComments = [ + { tid: 1, count: 10, "group-aware-consensus": 0.7 }, + { tid: 2, count: 5, "group-aware-consensus": 0.8 }, + ]; + + test('renders loading message when conversation is not provided', () => { + render(); + expect(screen.getByText('Loading allCommentsModeratedIn...')).toBeInTheDocument(); + }); + + test('renders comments with tid sorting by default', () => { + const mockConversation = {}; + render(); + expect(CommentList).toHaveBeenCalledWith({"comments": [{"count": 10, "group-aware-consensus": 0.7, "tid": 1}, {"count": 5, "group-aware-consensus": 0.8, "tid": 2}], "conversation": {}, "formatTid": undefined, "math": undefined, "ptptCount": undefined, "tidsToRender": [1, 2], "voteColors": undefined}, {} + ); + }); + + test('sorts comments by numvotes when selected', () => { + const mockConversation = {}; + render( + + ); + expect(CommentList).toHaveBeenCalledWith({"comments": [{"count": 10, "group-aware-consensus": 0.7, "tid": 1}, {"count": 5, "group-aware-consensus": 0.8, "tid": 2}], "conversation": {}, "formatTid": undefined, "math": undefined, "ptptCount": undefined, "tidsToRender": [1, 2], "voteColors": undefined}, {}); + }); + + test('sorts comments by other criteria based on sortStyle', () => { + const mockConversation = {}; + const sortStyles = ['consensus', 'pctAgreed', 'pctDisagreed', 'pctPassed']; + sortStyles.forEach((sortStyle) => { + render( + + ); + expect(CommentList).toHaveBeenCalled(); // Ensure CommentList is called + }); + }); +}); \ No newline at end of file diff --git a/client-report/src/components/lists/commentList.js b/client-report/src/components/lists/commentList.js index 722eeaaf1..d5f8e4d68 100644 --- a/client-report/src/components/lists/commentList.js +++ b/client-report/src/components/lists/commentList.js @@ -160,6 +160,9 @@ const CommentRow = ({ comment, groups, voteColors }) => { }; class CommentList extends React.Component { + constructor(props) { + super(props) + } getGroupLabels() { function makeLabel(key, label, numMembers) { return ( From e7dc6006fa650acac26c04608d04900182022a10 Mon Sep 17 00:00:00 2001 From: tevko Date: Thu, 12 Dec 2024 22:12:15 -0600 Subject: [PATCH 05/16] refactor commentList --- .../src/components/beeswarm/beeswarm.jsx | 2 +- .../lists/allCommentsModeratedIn.jsx | 2 +- .../lists/allCommentsModeratedIn.test.jsx | 2 +- .../lists/{commentList.js => commentList.jsx} | 103 ++++++++---------- .../components/lists/consensusNarrative.js | 2 +- .../src/components/lists/majorityStrict.js | 2 +- .../src/components/lists/metadata.js | 2 +- .../src/components/lists/participantGroup.js | 2 +- .../src/components/lists/uncertainty.js | 2 +- .../components/lists/uncertaintyNarrative.js | 2 +- .../participantsGraph/participantsGraph.js | 2 +- 11 files changed, 58 insertions(+), 65 deletions(-) rename client-report/src/components/lists/{commentList.js => commentList.jsx} (75%) diff --git a/client-report/src/components/beeswarm/beeswarm.jsx b/client-report/src/components/beeswarm/beeswarm.jsx index 744bdcf54..22dfd89aa 100644 --- a/client-report/src/components/beeswarm/beeswarm.jsx +++ b/client-report/src/components/beeswarm/beeswarm.jsx @@ -1,7 +1,7 @@ // Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . import React, { useState, useEffect } from "react"; -import CommentList from "../lists/commentList"; +import CommentList from "../lists/commentList.jsx"; import * as globals from "../globals"; import _ from "lodash"; // import Flex from "../framework/flex" diff --git a/client-report/src/components/lists/allCommentsModeratedIn.jsx b/client-report/src/components/lists/allCommentsModeratedIn.jsx index bdbd68751..f4f80b0d4 100644 --- a/client-report/src/components/lists/allCommentsModeratedIn.jsx +++ b/client-report/src/components/lists/allCommentsModeratedIn.jsx @@ -1,7 +1,7 @@ // Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . import React, { useState } from "react"; -import CommentList from "./commentList"; +import CommentList from "./commentList.jsx"; import * as globals from "../globals"; diff --git a/client-report/src/components/lists/allCommentsModeratedIn.test.jsx b/client-report/src/components/lists/allCommentsModeratedIn.test.jsx index 497deed09..ad3c02af3 100644 --- a/client-report/src/components/lists/allCommentsModeratedIn.test.jsx +++ b/client-report/src/components/lists/allCommentsModeratedIn.test.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import '@testing-library/jest-dom'; import AllCommentsModeratedIn from './allCommentsModeratedIn.jsx'; -import CommentList from './commentList.js'; +import CommentList from './commentList.jsx'; jest.mock('./commentList', () => ({ __esModule: true, diff --git a/client-report/src/components/lists/commentList.js b/client-report/src/components/lists/commentList.jsx similarity index 75% rename from client-report/src/components/lists/commentList.js rename to client-report/src/components/lists/commentList.jsx index d5f8e4d68..0844a1037 100644 --- a/client-report/src/components/lists/commentList.js +++ b/client-report/src/components/lists/commentList.jsx @@ -1,7 +1,6 @@ // Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . import React from "react"; -import _ from "lodash"; import * as globals from "../globals"; const BarChartCompact = ({ comment, voteCounts, nMembers, voteColors }) => { @@ -82,17 +81,17 @@ const CommentRow = ({ comment, groups, voteColors }) => { console.error("WHY IS THERE NO COMMENT 3452354235", comment); return null; } - // const percentAgreed = Math.floor(groupVotesForThisGroup.votes[comment.tid].A / groupVotesForThisGroup.votes[comment.tid].S * 100); let BarCharts = []; let totalMembers = 0; // groups - _.forEach(groups, (g, i) => { - let nMembers = g["n-members"]; + Object.entries(groups).forEach(([key, g]) => { + const i = parseInt(key, 10); // Parse the key to an integer + const nMembers = g["n-members"]; totalMembers += nMembers; - let gVotes = g.votes[comment.tid]; - + const gVotes = g.votes[comment.tid]; + BarCharts.push( { ); }); - // totals column - // let globalCounts = { - // A: comment.agreed, - // D: comment.disagreed, - // S: comment.saw, - // }; BarCharts.unshift( { ); }; -class CommentList extends React.Component { - constructor(props) { - super(props) - } - getGroupLabels() { +const CommentList = ({ comments, math, ptptCount, tidsToRender, voteColors }) => { + + const getGroupLabels = () => { function makeLabel(key, label, numMembers) { return ( { + Object.entries(math["group-votes"]).forEach(([key, g]) => { + const i = parseInt(key, 10); labels.push(makeLabel(i, globals.groupLabels[i], g["n-members"])); }); return labels; } - render() { - const comments = _.keyBy(this.props.comments, "tid"); + const cs = comments.reduce((acc, comment) => { + acc[comment.tid] = comment; + return acc; + }, {}); - return ( -
-
+
+ - - Statement - + Statement + - {this.getGroupLabels()} -
- {this.props.tidsToRender.map((tid, i) => { - return ( - - ); - })} + {getGroupLabels()}
- ); - } + {tidsToRender.map((tid, i) => { + return ( + + ); + })} +
+ ); } export default CommentList; diff --git a/client-report/src/components/lists/consensusNarrative.js b/client-report/src/components/lists/consensusNarrative.js index 8a24c6850..3d4b58b3f 100644 --- a/client-report/src/components/lists/consensusNarrative.js +++ b/client-report/src/components/lists/consensusNarrative.js @@ -1,7 +1,7 @@ import React, { useState } from "react"; import * as globals from "../globals"; import Narrative from "../narrative"; -import CommentList from "./commentList"; +import CommentList from "./commentList.jsx"; const ConsensusNarrative = ({ math, comments, diff --git a/client-report/src/components/lists/majorityStrict.js b/client-report/src/components/lists/majorityStrict.js index e3373d71b..cf8d7454f 100644 --- a/client-report/src/components/lists/majorityStrict.js +++ b/client-report/src/components/lists/majorityStrict.js @@ -2,7 +2,7 @@ import React from "react"; import * as globals from "../globals"; -import CommentList from "./commentList"; +import CommentList from "./commentList.jsx"; import Legend from "../framework/legend.jsx"; const MajorityStrict = ({ diff --git a/client-report/src/components/lists/metadata.js b/client-report/src/components/lists/metadata.js index ecc016f20..e1bfe265f 100644 --- a/client-report/src/components/lists/metadata.js +++ b/client-report/src/components/lists/metadata.js @@ -1,7 +1,7 @@ // Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . import React from "react"; -import CommentList from "./commentList"; +import CommentList from "./commentList.jsx"; import * as globals from "../globals"; const Metadata = ({ conversation, comments, ptptCount, formatTid, math, voteColors }) => { diff --git a/client-report/src/components/lists/participantGroup.js b/client-report/src/components/lists/participantGroup.js index 19f580a07..444ec3900 100644 --- a/client-report/src/components/lists/participantGroup.js +++ b/client-report/src/components/lists/participantGroup.js @@ -4,7 +4,7 @@ import React from "react"; import * as globals from "../globals"; // import Flex from "../framework/flex" // import style from "../../util/style"; -import CommentList from "./commentList"; +import CommentList from "./commentList.jsx"; const ParticipantGroup = ({ gid, diff --git a/client-report/src/components/lists/uncertainty.js b/client-report/src/components/lists/uncertainty.js index fe42b9eff..d7c9a41db 100644 --- a/client-report/src/components/lists/uncertainty.js +++ b/client-report/src/components/lists/uncertainty.js @@ -1,7 +1,7 @@ // Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . import React from "react"; -import CommentList from "./commentList"; +import CommentList from "./commentList.jsx"; import * as globals from "../globals"; // import style from "../../util/style"; import Narrative from "../narrative"; diff --git a/client-report/src/components/lists/uncertaintyNarrative.js b/client-report/src/components/lists/uncertaintyNarrative.js index ceb115675..3ea454977 100644 --- a/client-report/src/components/lists/uncertaintyNarrative.js +++ b/client-report/src/components/lists/uncertaintyNarrative.js @@ -1,7 +1,7 @@ // Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . import React from "react"; -import CommentList from "./commentList"; +import CommentList from "./commentList.jsx"; import * as globals from "../globals"; // import style from "../../util/style"; import Narrative from "../narrative"; diff --git a/client-report/src/components/participantsGraph/participantsGraph.js b/client-report/src/components/participantsGraph/participantsGraph.js index a7ba3b80a..12dfed5db 100644 --- a/client-report/src/components/participantsGraph/participantsGraph.js +++ b/client-report/src/components/participantsGraph/participantsGraph.js @@ -10,7 +10,7 @@ import * as d3chromatic from "d3-scale-chromatic"; // import GroupLabels from "./groupLabels"; import Comments from "../commentsGraph/comments.jsx"; import Hull from "./hull"; -import CommentList from "../lists/commentList"; +import CommentList from "../lists/commentList.jsx"; const pointsPerSquarePixelMax = 0.0017; /* choose dynamically ? */ const contourBandwidth = 20; From f73689b336a396f50fc48f430c380396bc7ae27e Mon Sep 17 00:00:00 2001 From: tevko Date: Fri, 13 Dec 2024 14:42:25 -0600 Subject: [PATCH 06/16] consensusNarrative + test --- client-report/src/components/app.js | 2 +- ...susNarrative.js => consensusNarrative.jsx} | 4 +- .../lists/consensusNarrative.test.jsx | 71 +++++++++++++++++++ 3 files changed, 74 insertions(+), 3 deletions(-) rename client-report/src/components/lists/{consensusNarrative.js => consensusNarrative.jsx} (94%) create mode 100644 client-report/src/components/lists/consensusNarrative.test.jsx diff --git a/client-report/src/components/app.js b/client-report/src/components/app.js index 9c4a0180f..95c995b70 100644 --- a/client-report/src/components/app.js +++ b/client-report/src/components/app.js @@ -31,7 +31,7 @@ import net from "../util/net"; import $ from "jquery"; import Narrative from "./narrative"; -import ConsensusNarrative from "./lists/consensusNarrative"; +import ConsensusNarrative from "./lists/consensusNarrative.jsx"; import RawDataExport from "./RawDataExport"; var pathname = window.location.pathname; // "/report/2arcefpshi" diff --git a/client-report/src/components/lists/consensusNarrative.js b/client-report/src/components/lists/consensusNarrative.jsx similarity index 94% rename from client-report/src/components/lists/consensusNarrative.js rename to client-report/src/components/lists/consensusNarrative.jsx index 3d4b58b3f..b204a5288 100644 --- a/client-report/src/components/lists/consensusNarrative.js +++ b/client-report/src/components/lists/consensusNarrative.jsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; -import * as globals from "../globals"; -import Narrative from "../narrative"; +import * as globals from "../globals.js"; +import Narrative from "../narrative/index.js"; import CommentList from "./commentList.jsx"; const ConsensusNarrative = ({ math, diff --git a/client-report/src/components/lists/consensusNarrative.test.jsx b/client-report/src/components/lists/consensusNarrative.test.jsx new file mode 100644 index 000000000..6692741a6 --- /dev/null +++ b/client-report/src/components/lists/consensusNarrative.test.jsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import ConsensusNarrative from './consensusNarrative'; +import * as globals from '../globals.js'; +import Narrative from '../narrative/index.js'; +import CommentList from './commentList.jsx'; + +jest.mock('../narrative/index.js', () => { + return ({ sectionData, model }) => ( +
+ Narrative Component - Model: {model} - Data: {JSON.stringify(sectionData)} +
+ ); +}); + +jest.mock('./commentList.jsx', () => { + return ({ conversation, ptptCount, math, formatTid, tidsToRender, comments, voteColors }) => ( +
+ CommentList Component - TIDs: {JSON.stringify(tidsToRender)} +
+ ); +}); + +describe('ConsensusNarrative Component', () => { + const mockNarrativeData = { + group_informed_consensus: { + responseClaude: { + content: [{ text: '"paragraphs":[{"sentences":[{"clauses":[{"citations":["tid1","tid2"]}]}]}]}' }], + }, + responseGemini: '{"paragraphs":[{"sentences":[{"clauses":[{"citations":["tid2","tid3","tid1"]}]}]}]}', + }, + }; + + + const mockProps = { + math: {}, + comments: [], + conversation: {}, + ptptCount: 5, + formatTid: jest.fn((tid) => `Formatted ${tid}`), + voteColors: {}, + narrative: mockNarrativeData, + model: "claude", + }; + + it('renders loading message when narrative data is missing', () => { + render(); + expect(screen.getByText('Loading Consensus...')).toBeInTheDocument(); + }); + + it('renders the component with Claude model', async () => { + render(); + + expect(screen.getByText('Group Aware Consensus Narrative')).toBeInTheDocument(); + expect(screen.getByText('This narrative summary may contain hallucinations. Check each clause.')).toBeInTheDocument(); + + expect(screen.getByTestId('mock-narrative')).toHaveTextContent('Model: claude'); + expect(screen.getByTestId('mock-comment-list')).toHaveTextContent('TIDs: ["tid1","tid2"]'); + }); + + it('renders the component with Gemini model', async () => { + render(); + + expect(screen.getByText('Group Aware Consensus Narrative')).toBeInTheDocument(); + expect(screen.getByText('This narrative summary may contain hallucinations. Check each clause.')).toBeInTheDocument(); + + expect(screen.getByTestId('mock-narrative')).toHaveTextContent('Model: gemini'); + expect(screen.getByTestId('mock-comment-list')).toHaveTextContent('TIDs: ["tid2","tid3","tid1"]'); + }); +}); \ No newline at end of file From 3230c4a894e5ed0f2085e68f58a30a1ace1d0221 Mon Sep 17 00:00:00 2001 From: tevko Date: Fri, 13 Dec 2024 15:31:45 -0600 Subject: [PATCH 07/16] groups & test --- client-report/src/components/app.js | 4 +- .../{majorityStrict.js => majorityStrict.jsx} | 0 .../lists/{metadata.js => metadata.jsx} | 2 +- ...rticipantGroup.js => participantGroup.jsx} | 15 +- .../src/components/lists/participantGroups.js | 108 --------------- .../components/lists/participantGroups.jsx | 128 ++++++++++++++++++ .../lists/participantGroups.test.jsx | 70 ++++++++++ 7 files changed, 205 insertions(+), 122 deletions(-) rename client-report/src/components/lists/{majorityStrict.js => majorityStrict.jsx} (100%) rename client-report/src/components/lists/{metadata.js => metadata.jsx} (97%) rename client-report/src/components/lists/{participantGroup.js => participantGroup.jsx} (84%) delete mode 100644 client-report/src/components/lists/participantGroups.js create mode 100644 client-report/src/components/lists/participantGroups.jsx create mode 100644 client-report/src/components/lists/participantGroups.test.jsx diff --git a/client-report/src/components/app.js b/client-report/src/components/app.js index 95c995b70..4b22aa822 100644 --- a/client-report/src/components/app.js +++ b/client-report/src/components/app.js @@ -15,11 +15,11 @@ import DataUtils from "../util/dataUtils"; import Heading from "./framework/heading.jsx"; import Footer from "./framework/Footer.jsx"; import Overview from "./overview"; -import MajorityStrict from "./lists/majorityStrict"; +import MajorityStrict from "./lists/majorityStrict.jsx"; import Uncertainty from "./lists/uncertainty"; import UncertaintyNarrative from "./lists/uncertaintyNarrative"; import AllCommentsModeratedIn from "./lists/allCommentsModeratedIn.jsx"; -import ParticipantGroups from "./lists/participantGroups"; +import ParticipantGroups from "./lists/participantGroups.jsx"; // import CommentsGraph from "./commentsGraph/commentsGraph"; import ParticipantsGraph from "./participantsGraph/participantsGraph"; // import BoxPlot from "./boxPlot/boxPlot"; diff --git a/client-report/src/components/lists/majorityStrict.js b/client-report/src/components/lists/majorityStrict.jsx similarity index 100% rename from client-report/src/components/lists/majorityStrict.js rename to client-report/src/components/lists/majorityStrict.jsx diff --git a/client-report/src/components/lists/metadata.js b/client-report/src/components/lists/metadata.jsx similarity index 97% rename from client-report/src/components/lists/metadata.js rename to client-report/src/components/lists/metadata.jsx index e1bfe265f..7af09ff06 100644 --- a/client-report/src/components/lists/metadata.js +++ b/client-report/src/components/lists/metadata.jsx @@ -2,7 +2,7 @@ import React from "react"; import CommentList from "./commentList.jsx"; -import * as globals from "../globals"; +import * as globals from "../globals.js"; const Metadata = ({ conversation, comments, ptptCount, formatTid, math, voteColors }) => { if (!conversation) { diff --git a/client-report/src/components/lists/participantGroup.js b/client-report/src/components/lists/participantGroup.jsx similarity index 84% rename from client-report/src/components/lists/participantGroup.js rename to client-report/src/components/lists/participantGroup.jsx index 444ec3900..e0dfab54d 100644 --- a/client-report/src/components/lists/participantGroup.js +++ b/client-report/src/components/lists/participantGroup.jsx @@ -1,9 +1,7 @@ // Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . import React from "react"; -import * as globals from "../globals"; -// import Flex from "../framework/flex" -// import style from "../../util/style"; +import * as globals from "../globals.js"; import CommentList from "./commentList.jsx"; const ParticipantGroup = ({ @@ -12,12 +10,9 @@ const ParticipantGroup = ({ conversation, comments, groupVotesForThisGroup, - // groupVotesForOtherGroups, - // demographicsForGroup, ptptCount, groupName, formatTid, - // groupNames, math, voteColors, }) => { @@ -27,6 +22,8 @@ const ParticipantGroup = ({ groupLabel = "Group " + globals.groupLabels[gid]; } + console.log(groupComments) + return (
c.tid)} comments={comments} voteColors={voteColors}/> - - - -
); }; diff --git a/client-report/src/components/lists/participantGroups.js b/client-report/src/components/lists/participantGroups.js deleted file mode 100644 index d2bed6971..000000000 --- a/client-report/src/components/lists/participantGroups.js +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . - -import React from "react"; -// import _ from "lodash"; -import Group from "./participantGroup"; -// import style from "../../util/style"; -import * as globals from "../globals"; -import Metadata from "./metadata"; - -class ParticipantGroups extends React.Component { - constructor(props) { - super(props); - this.state = { - - }; - } - getStyles() { - return { - base: { - - } - }; - } - render() { - const styles = this.getStyles(); - if (!this.props.conversation) { - return
Loading Groups
; - } - return ( -
-
-

Opinion Groups

-

- Across {this.props.ptptCount} total participants, {this.props.math["group-votes"].length} opinion groups emerged. There are two factors that define an opinion group. First, each opinion group is made up of a number of participants who tended to vote similarly on multiple statements. Second, each group of participants who voted similarly will have also voted distinctly differently from other groups. -

- - { - this.props.math && this.props.comments ? _.map(this.props.math["repness"], (groupComments, gid) => { - gid = Number(gid); - - let otherGroupVotes = { - votes: [], - "n-members": 0, - }; - - let MAX_CLUSTERS = 50; - let temp = this.props.math["group-votes"]; - for (let ogid = 0; ogid < MAX_CLUSTERS; ogid++) { - if (ogid === gid || !temp[ogid]) { - continue; - } - otherGroupVotes["n-members"] += temp[ogid]["n-members"]; - let commentVotes = temp[ogid].votes; - _.each(commentVotes, (voteObj, tid) => { - tid = Number(tid); - if (voteObj) { - if (!otherGroupVotes.votes[tid]) { - otherGroupVotes.votes[tid] = {A: 0, D: 0, S: 0}; - } - otherGroupVotes.votes[tid].A += voteObj.A; - otherGroupVotes.votes[tid].D += voteObj.D; - otherGroupVotes.votes[tid].S += voteObj.S; - - } - }); - } - return ( - - ); - }) : "Loading Groups" - } -
-
- ); - } -} - -export default ParticipantGroups; diff --git a/client-report/src/components/lists/participantGroups.jsx b/client-report/src/components/lists/participantGroups.jsx new file mode 100644 index 000000000..9d0567f21 --- /dev/null +++ b/client-report/src/components/lists/participantGroups.jsx @@ -0,0 +1,128 @@ +// // Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . + +import React, { useState, useEffect } from "react"; +import Group from "./participantGroup.jsx"; +import * as globals from "../globals.js"; +import Metadata from "./metadata.jsx"; + +const ParticipantGroups = ({ + conversation, + ptptCount, + math, + comments, + style, + voteColors, + formatTid, + demographics, + groupNames, + badTids, + repfulAgreeTidsByGroup, + repfulDisageeTidsByGroup, + report, +}) => { + const [isLoading, setIsLoading] = useState(true); + const [groups, setGroups] = useState([]); + + useEffect(() => { + if (!conversation || !math || !comments) return; + + const processGroups = () => { + const processedGroups = Object.keys(math["repness"]).map((gid) => { // Use Object.keys and map + gid = Number(gid); + + let otherGroupVotes = { + votes: [], + "n-members": 0, + }; + + const MAX_CLUSTERS = 50; + const temp = math["group-votes"]; + + for (let ogid = 0; ogid < MAX_CLUSTERS; ogid++) { + if (ogid === gid || !temp[ogid]) { + continue; + } + otherGroupVotes["n-members"] += temp[ogid]["n-members"]; + const commentVotes = temp[ogid].votes; + + Object.keys(commentVotes || {}).forEach((tid) => { // Use Object.keys and forEach + tid = Number(tid); + const voteObj = commentVotes[tid]; + if (voteObj) { + if (!otherGroupVotes.votes[tid]) { + otherGroupVotes.votes[tid] = { A: 0, D: 0, S: 0 }; + } + otherGroupVotes.votes[tid].A += voteObj.A; + otherGroupVotes.votes[tid].D += voteObj.D; + otherGroupVotes.votes[tid].S += voteObj.S; + } + }); + } + + return ( + + ); + }); + + setGroups(processedGroups); + setIsLoading(false); + }; + + processGroups(); + }, [conversation, math, comments]); + + const styles = { + base: {}, + ...style, + }; + + return ( +
+
+

Opinion Groups

+

+ Across {ptptCount} total participants, {math && Object.keys(math["group-votes"])?.length}{" "} + opinion groups emerged. There are two factors that define an opinion + group. First, each opinion group is made up of a number of participants + who tended to vote similarly on multiple statements. Second, each group + of participants who voted similarly will have also voted distinctly + differently from other groups. +

+ + {isLoading ? ( +
Loading Groups
+ ) : ( + groups + )} +
+
+ ); +}; + +export default ParticipantGroups; \ No newline at end of file diff --git a/client-report/src/components/lists/participantGroups.test.jsx b/client-report/src/components/lists/participantGroups.test.jsx new file mode 100644 index 000000000..eb8bf7d62 --- /dev/null +++ b/client-report/src/components/lists/participantGroups.test.jsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import ParticipantGroups from './ParticipantGroups'; +import * as globals from '../globals'; // Mock globals if necessary +import Metadata from './metadata.jsx'; // Mock Metadata +import Group from './participantGroup.jsx'; // Mock Group +import '@testing-library/jest-dom'; + +jest.mock('../globals', () => ({ + primaryHeading: { fontSize: '20px' }, + paragraph: { fontSize: '16px' }, +})); + +jest.mock('./metadata.jsx', () => () =>
); +jest.mock('./participantGroup.jsx', () => ({ groupName }) => ( +
{groupName}
+)); + +describe('ParticipantGroups Component', () => { + const mockProps = { + conversation: {}, + ptptCount: 100, + math: { + "group-votes": { + 0: { "n-members": 20, votes: { 1: { A: 10, D: 5, S: 5 }, 2: { A: 15, D: 0, S: 5 } } }, + 1: { "n-members": 30, votes: { 1: { A: 5, D: 15, S: 10 }, 3: { A: 20, D: 5, S: 5 } } }, + }, + repness: { + 0: { someData: 'group0data' }, + 1: { someData: 'group1data' }, + }, + }, + comments: {}, + voteColors: {}, + formatTid: jest.fn((tid) => `TID${tid}`), + demographics: { + 0: {someDemo: "demo0"}, + 1: {someDemo: "demo1"} + }, + groupNames: { + 0: "Group A", + 1: "Group B" + }, + badTids: [], + repfulAgreeTidsByGroup: {}, + repfulDisageeTidsByGroup: {}, + report: {}, + style: { backgroundColor: 'lightgray' } + }; + + it('renders loading message when data is missing', () => { + render(); + expect(screen.getByText('Loading Groups')).toBeInTheDocument(); + }); + + it('renders component with groups when data is present', () => { + render(); + + expect(screen.getByText('Opinion Groups')).toBeInTheDocument(); + expect(screen.getByText(/Across 100 total participants, 2 opinion groups emerged/)).toBeInTheDocument(); + expect(screen.getByTestId('mock-metadata')).toBeInTheDocument(); + expect(screen.getByTestId('mock-group-Group A')).toBeInTheDocument(); + expect(screen.getByTestId('mock-group-Group B')).toBeInTheDocument(); + }); + + it("renders correct number of groups", () => { + render(); + expect(screen.getAllByTestId(/mock-group-/).length).toBe(2); + }) +}); \ No newline at end of file From 266611bb1604bbba6238bfc016ec5fee3d8cefc8 Mon Sep 17 00:00:00 2001 From: tevko Date: Fri, 13 Dec 2024 15:44:12 -0600 Subject: [PATCH 08/16] uncertaintynarrative and test --- client-report/src/components/app.js | 12 +---- .../lists/{uncertainty.js => uncertainty.jsx} | 4 +- ...yNarrative.js => uncertaintyNarrative.jsx} | 6 +-- .../lists/uncertaintyNarrative.test.jsx | 49 +++++++++++++++++++ 4 files changed, 55 insertions(+), 16 deletions(-) rename client-report/src/components/lists/{uncertainty.js => uncertainty.jsx} (95%) rename client-report/src/components/lists/{uncertaintyNarrative.js => uncertaintyNarrative.jsx} (94%) create mode 100644 client-report/src/components/lists/uncertaintyNarrative.test.jsx diff --git a/client-report/src/components/app.js b/client-report/src/components/app.js index 4b22aa822..0d36367cc 100644 --- a/client-report/src/components/app.js +++ b/client-report/src/components/app.js @@ -1,28 +1,20 @@ // Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . import React from "react"; -// import { connect } from "react-redux"; -// import probabilities from "../sampleData/probabilities"; -// import covariance from "../sampleData/covariance"; -// import correlation from "../sampleData/correlation"; -// import correlationHClust from "../sampleData/correlationHClust" import _ from "lodash"; import * as globals from "./globals"; import URLs from "../util/url"; import DataUtils from "../util/dataUtils"; -// import Matrix from "./correlationMatrix/matrix"; import Heading from "./framework/heading.jsx"; import Footer from "./framework/Footer.jsx"; import Overview from "./overview"; import MajorityStrict from "./lists/majorityStrict.jsx"; -import Uncertainty from "./lists/uncertainty"; -import UncertaintyNarrative from "./lists/uncertaintyNarrative"; +import Uncertainty from "./lists/uncertainty.jsx"; +import UncertaintyNarrative from "./lists/uncertaintyNarrative.jsx"; import AllCommentsModeratedIn from "./lists/allCommentsModeratedIn.jsx"; import ParticipantGroups from "./lists/participantGroups.jsx"; -// import CommentsGraph from "./commentsGraph/commentsGraph"; import ParticipantsGraph from "./participantsGraph/participantsGraph"; -// import BoxPlot from "./boxPlot/boxPlot"; import Beeswarm from "./beeswarm/beeswarm.jsx"; import Controls from "./controls/controls.jsx"; diff --git a/client-report/src/components/lists/uncertainty.js b/client-report/src/components/lists/uncertainty.jsx similarity index 95% rename from client-report/src/components/lists/uncertainty.js rename to client-report/src/components/lists/uncertainty.jsx index d7c9a41db..6cebc7821 100644 --- a/client-report/src/components/lists/uncertainty.js +++ b/client-report/src/components/lists/uncertainty.jsx @@ -2,9 +2,9 @@ import React from "react"; import CommentList from "./commentList.jsx"; -import * as globals from "../globals"; +import * as globals from "../globals.js"; // import style from "../../util/style"; -import Narrative from "../narrative"; +import Narrative from "../narrative/index.js"; const Uncertainty = ({ conversation, diff --git a/client-report/src/components/lists/uncertaintyNarrative.js b/client-report/src/components/lists/uncertaintyNarrative.jsx similarity index 94% rename from client-report/src/components/lists/uncertaintyNarrative.js rename to client-report/src/components/lists/uncertaintyNarrative.jsx index 3ea454977..d77acbe05 100644 --- a/client-report/src/components/lists/uncertaintyNarrative.js +++ b/client-report/src/components/lists/uncertaintyNarrative.jsx @@ -2,15 +2,13 @@ import React from "react"; import CommentList from "./commentList.jsx"; -import * as globals from "../globals"; -// import style from "../../util/style"; -import Narrative from "../narrative"; +import * as globals from "../globals.js"; +import Narrative from "../narrative/index.js"; const UncertaintyNarrative = ({ conversation, comments, ptptCount, - uncertainty, formatTid, math, voteColors, diff --git a/client-report/src/components/lists/uncertaintyNarrative.test.jsx b/client-report/src/components/lists/uncertaintyNarrative.test.jsx new file mode 100644 index 000000000..d5c028d43 --- /dev/null +++ b/client-report/src/components/lists/uncertaintyNarrative.test.jsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import UncertaintyNarrative from './uncertaintyNarrative'; +import * as globals from '../globals.js'; // Mock globals if necessary +jest.mock('../narrative/index.js', () => ({ model }) =>
); // Mock Narrative +jest.mock('./commentList.jsx', () => () =>
); +import '@testing-library/jest-dom'; + +describe('UncertaintyNarrative Component', () => { + const mockProps = { + conversation: {}, + comments: {}, + ptptCount: 100, + formatTid: jest.fn((tid) => `TID${tid}`), + math: {}, + voteColors: {}, + narrative: { + uncertainty: { + responseClaude: { content: [{ text: '"paragraphs":[{"sentences":[{"clauses":[{"citations":["T1","T2"]}]}]}]}' }] }, + responseGemini: '{"paragraphs":[{"sentences":[{"clauses":[{"citations":["T3"]}]}]}]}' + } + }, + model: 'claude' + }; + + it('renders loading message when data is missing', () => { + render(); + expect(screen.getByText('Loading Uncertainty...')).toBeInTheDocument(); + }); + + it('renders component with narrative and comment list when data is present (Claude model)', () => { + render(); + + expect(screen.getByText('Uncertainty Narrative')).toBeInTheDocument(); + expect(screen.getByText('This narrative summary may contain hallucinations. Check each clause.')).toBeInTheDocument(); + expect(screen.getByTestId('mock-narrative-claude')).toBeInTheDocument(); + expect(screen.getByTestId('mock-comment-list')).toBeInTheDocument(); + }); + + it('renders component with narrative and comment list when data is present (Gemini model)', () => { + render(); + + expect(screen.getByText('Uncertainty Narrative')).toBeInTheDocument(); + expect(screen.getByText('This narrative summary may contain hallucinations. Check each clause.')).toBeInTheDocument(); + expect(screen.getByTestId('mock-narrative-gemini')).toBeInTheDocument(); + expect(screen.getByTestId('mock-comment-list')).toBeInTheDocument(); + }); + +}); \ No newline at end of file From 3c35ad73ae2d5bdd4edef983d65065147f21fdfa Mon Sep 17 00:00:00 2001 From: tevko Date: Fri, 13 Dec 2024 15:51:59 -0600 Subject: [PATCH 09/16] being participantsGraph --- client-report/src/components/app.js | 1 - .../components/lists/consensusNarrative.jsx | 2 +- .../lists/consensusNarrative.test.jsx | 4 +- .../src/components/lists/uncertainty.jsx | 2 +- .../components/lists/uncertaintyNarrative.jsx | 2 +- .../lists/uncertaintyNarrative.test.jsx | 2 +- .../narrative/{index.js => index.jsx} | 0 .../{groupLabels.js => groupLabels.jsx} | 10 ---- .../participantsGraph/{hull.js => hull.jsx} | 0 .../participantsGraph/participantsGraph.js | 53 +------------------ 10 files changed, 7 insertions(+), 69 deletions(-) rename client-report/src/components/narrative/{index.js => index.jsx} (100%) rename client-report/src/components/participantsGraph/{groupLabels.js => groupLabels.jsx} (95%) rename client-report/src/components/participantsGraph/{hull.js => hull.jsx} (100%) diff --git a/client-report/src/components/app.js b/client-report/src/components/app.js index 0d36367cc..9e457f0d9 100644 --- a/client-report/src/components/app.js +++ b/client-report/src/components/app.js @@ -22,7 +22,6 @@ import net from "../util/net"; import $ from "jquery"; -import Narrative from "./narrative"; import ConsensusNarrative from "./lists/consensusNarrative.jsx"; import RawDataExport from "./RawDataExport"; diff --git a/client-report/src/components/lists/consensusNarrative.jsx b/client-report/src/components/lists/consensusNarrative.jsx index b204a5288..489fe4ff8 100644 --- a/client-report/src/components/lists/consensusNarrative.jsx +++ b/client-report/src/components/lists/consensusNarrative.jsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import * as globals from "../globals.js"; -import Narrative from "../narrative/index.js"; +import Narrative from "../narrative/index.jsx"; import CommentList from "./commentList.jsx"; const ConsensusNarrative = ({ math, diff --git a/client-report/src/components/lists/consensusNarrative.test.jsx b/client-report/src/components/lists/consensusNarrative.test.jsx index 6692741a6..02778599d 100644 --- a/client-report/src/components/lists/consensusNarrative.test.jsx +++ b/client-report/src/components/lists/consensusNarrative.test.jsx @@ -3,10 +3,10 @@ import { render, screen, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom'; import ConsensusNarrative from './consensusNarrative'; import * as globals from '../globals.js'; -import Narrative from '../narrative/index.js'; +import Narrative from '../narrative/index.jsx'; import CommentList from './commentList.jsx'; -jest.mock('../narrative/index.js', () => { +jest.mock('../narrative/index.jsx', () => { return ({ sectionData, model }) => (
Narrative Component - Model: {model} - Data: {JSON.stringify(sectionData)} diff --git a/client-report/src/components/lists/uncertainty.jsx b/client-report/src/components/lists/uncertainty.jsx index 6cebc7821..3d8b40c3d 100644 --- a/client-report/src/components/lists/uncertainty.jsx +++ b/client-report/src/components/lists/uncertainty.jsx @@ -4,7 +4,7 @@ import React from "react"; import CommentList from "./commentList.jsx"; import * as globals from "../globals.js"; // import style from "../../util/style"; -import Narrative from "../narrative/index.js"; +import Narrative from "../narrative/index.jsx"; const Uncertainty = ({ conversation, diff --git a/client-report/src/components/lists/uncertaintyNarrative.jsx b/client-report/src/components/lists/uncertaintyNarrative.jsx index d77acbe05..81c4d6ab5 100644 --- a/client-report/src/components/lists/uncertaintyNarrative.jsx +++ b/client-report/src/components/lists/uncertaintyNarrative.jsx @@ -3,7 +3,7 @@ import React from "react"; import CommentList from "./commentList.jsx"; import * as globals from "../globals.js"; -import Narrative from "../narrative/index.js"; +import Narrative from "../narrative/index.jsx"; const UncertaintyNarrative = ({ conversation, diff --git a/client-report/src/components/lists/uncertaintyNarrative.test.jsx b/client-report/src/components/lists/uncertaintyNarrative.test.jsx index d5c028d43..7c49e0a47 100644 --- a/client-report/src/components/lists/uncertaintyNarrative.test.jsx +++ b/client-report/src/components/lists/uncertaintyNarrative.test.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import UncertaintyNarrative from './uncertaintyNarrative'; import * as globals from '../globals.js'; // Mock globals if necessary -jest.mock('../narrative/index.js', () => ({ model }) =>
); // Mock Narrative +jest.mock('../narrative/index.jsx', () => ({ model }) =>
); // Mock Narrative jest.mock('./commentList.jsx', () => () =>
); import '@testing-library/jest-dom'; diff --git a/client-report/src/components/narrative/index.js b/client-report/src/components/narrative/index.jsx similarity index 100% rename from client-report/src/components/narrative/index.js rename to client-report/src/components/narrative/index.jsx diff --git a/client-report/src/components/participantsGraph/groupLabels.js b/client-report/src/components/participantsGraph/groupLabels.jsx similarity index 95% rename from client-report/src/components/participantsGraph/groupLabels.js rename to client-report/src/components/participantsGraph/groupLabels.jsx index 9adff438f..8253d0ae4 100644 --- a/client-report/src/components/participantsGraph/groupLabels.js +++ b/client-report/src/components/participantsGraph/groupLabels.jsx @@ -3,16 +3,6 @@ import * as globals from "../globals"; import React from "react"; -// const getBackgroundRectWidth = (ptptCount) => { -// let width = 46; /* smallest number */ -// if (ptptCount >= 100 && ptptCount < 1000) { -// width = 52; -// } else if (ptptCount > 1000) { -// width = 59; -// } -// return width; -// } - const Users = ({selectedGroup}) => { return ( diff --git a/client-report/src/components/participantsGraph/hull.js b/client-report/src/components/participantsGraph/hull.jsx similarity index 100% rename from client-report/src/components/participantsGraph/hull.js rename to client-report/src/components/participantsGraph/hull.jsx diff --git a/client-report/src/components/participantsGraph/participantsGraph.js b/client-report/src/components/participantsGraph/participantsGraph.js index 12dfed5db..e992b250e 100644 --- a/client-report/src/components/participantsGraph/participantsGraph.js +++ b/client-report/src/components/participantsGraph/participantsGraph.js @@ -7,9 +7,8 @@ import graphUtil from "../../util/graphUtil"; import Axes from "../graphAxes"; import * as d3contour from "d3-contour"; import * as d3chromatic from "d3-scale-chromatic"; -// import GroupLabels from "./groupLabels"; import Comments from "../commentsGraph/comments.jsx"; -import Hull from "./hull"; +import Hull from "./hull.jsx"; import CommentList from "../lists/commentList.jsx"; const pointsPerSquarePixelMax = 0.0017; /* choose dynamically ? */ @@ -48,21 +47,6 @@ const Participants = ({ points, math }) => { ); - // return ( - // {globals.groupSymbols[pt.gid]} - // ) })} ); @@ -144,25 +128,6 @@ class ParticipantsGraph extends React.Component {

- { - // - } - {/* */} - - - - - -
- -
- {this.state.selectedComment ? ( - - ) : ( -

Click a statement, identified by its number, to explore regions of the graph.

- )} -
- - {this.state.showParticipants ? ( -

- {hulls.map((h, i) => { - return ( - - {`${globals.groupSymbols[i]}`} - - {" "} - {`${globals.groupLabels[i]}`}{" "} - - - ); - })} -

- ) : null} - - - - - - - - - - {this.state.showRadialAxes ? ( - - - - - - ) : null} - {this.state.showContour - ? contours.map((contour, i) => ) - : null} - {this.state.showAxes ? ( - - ) : null} - {this.state.showGroupOutline - ? hulls.map((hull) => { - let gid = hull.group[0].gid; - if (_.isNumber(this.props.showOnlyGroup)) { - if (gid !== this.props.showOnlyGroup) { - return ""; - } - } - return ; - }) - : null} - {this.state.showParticipants ? ( - - ) : null} - {this.state.showComments ? ( - - ) : null} - {this.state.showGroupLabels - ? this.props.math["group-clusters"].map((g, i) => { - // console.log('g',g ) - return ( - - {globals.groupLabels[g.id]} - - ); - }) - : null} - {this.state.consensusDivisionColorScale ? ( - - - - - - - - - ) : null} - -
- ); - } -} - -export default ParticipantsGraph; diff --git a/client-report/src/components/participantsGraph/participantsGraph.jsx b/client-report/src/components/participantsGraph/participantsGraph.jsx new file mode 100644 index 000000000..50f0fdd87 --- /dev/null +++ b/client-report/src/components/participantsGraph/participantsGraph.jsx @@ -0,0 +1,389 @@ +// Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . + +import React, { useState } from "react"; +import * as globals from "../globals.js"; +import graphUtil from "../../util/graphUtil.js"; +import Axes from "../graphAxes.js"; +import * as d3contour from "d3-contour"; +import * as d3chromatic from "d3-scale-chromatic"; +import Comments from "../commentsGraph/comments.jsx"; +import Hull from "./hull.jsx"; +import CommentList from "../lists/commentList.jsx"; + +const pointsPerSquarePixelMax = 0.0017; /* choose dynamically ? */ +const contourBandwidth = 20; +const colorScaleDownFactor = 0.5; /* The colors are too dark. This helps. */ + + +const Contour = ({ contour }) => { + const color = window.d3 + .scaleSequential(d3chromatic.interpolateYlGnBu) + .domain([0, pointsPerSquarePixelMax]); + const geoPath = window.d3.geoPath(); + return +}; + +const Participants = ({ points, math }) => { + if (!points) { + return null; + } + + return ( + + {points.map((pt, i) => { + return ( + + + + {" "} + {globals.groupSymbols[pt.gid]} + + + ); + })} + + ); +}; + +const ParticipantsGraph = (props) => { + const [selectedComment, setSelectedComment] = useState(null); + const [showContour, setShowContour] = useState(false); + const [showGroupLabels, setShowGroupLabels] = useState(true); + const [showParticipants, setShowParticipants] = useState(false); + const [showGroupOutline, setShowGroupOutline] = useState(false); + const [showComments, setShowComments] = useState(true); + const [showAxes, setShowAxes] = useState(true); + const [showRadialAxes, setShowRadialAxes] = useState(true); + + const handleCommentClick = (sc) => { + return () => { + setSelectedComment(sc); + }; + } + + const getInnerRadialAxisColor = () => { + let color = globals.brandColors.lightgrey; + if (props.consensusDivisionColorScale && props.colorBlindMode) { + color = globals.brandColors.blue; + } else if (props.consensusDivisionColorScale && !props.colorBlindMode) { + color = props.voteColors.agree; + } + return color; + } + + if (!props.math) { + return null; + } + + const { + xx, + yy, + commentsPoints, + xCenter, + yCenter, + baseClustersScaled, + commentScaleupFactorX, + commentScaleupFactorY, + hulls, + } = graphUtil(props.comments, props.math, props.badTids); + + const contours = d3contour + .contourDensity() + .x(function (d) { + return d.x; + }) + .y(function (d) { + return d.y; + }) + .size([globals.side, globals.side]) + // .bandwidth(10)(baseClustersScaled) + .bandwidth(contourBandwidth)(baseClustersScaled); + + return ( +
+
+

Graph

+

+ Which statements were voted on similarly? How do participants relate to each other? +

+

+ In this graph, statements are positioned more closely to statements which were voted on + similarly. Participants, in turn, are positioned more closely to statements on which + they agreed, and further from statements on which they disagreed. This means + participants who voted similarly are closer together. +

+
+
+ + + + + + +
+ +
+ {selectedComment ? ( + + ) : ( +

Click a statement, identified by its number, to explore regions of the graph.

+ )} +
+ + {showParticipants ? ( +

+ {hulls.map((h, i) => { + return ( + + {`${globals.groupSymbols[i]}`} + + {" "} + {`${globals.groupLabels[i]}`}{" "} + + + ); + })} +

+ ) : null} + + + + + + + + + + {showRadialAxes ? ( + + + + + + ) : null} + {showContour + ? contours.map((contour, i) => ) + : null} + {showAxes ? ( + + ) : null} + {showGroupOutline + ? hulls.map((hull) => { + let gid = hull.group[0].gid; + if (typeof props.showOnlyGroup === 'number' && isFinite(props.showOnlyGroup)) { + if (gid !== props.showOnlyGroup) { + return ""; + } + } + return ; + }) + : null} + {showParticipants ? ( + + ) : null} + {showComments ? ( + + ) : null} + {showGroupLabels + ? props.math["group-clusters"].map((g, i) => { + // console.log('g',g ) + return ( + + {globals.groupLabels[g.id]} + + ); + }) + : null} + {props.consensusDivisionColorScale ? ( + + + + + + + + + ) : null} + +
+ ); +} + +export default ParticipantsGraph; diff --git a/client-report/src/components/participantsGraph/participantsGraph.test.jsx b/client-report/src/components/participantsGraph/participantsGraph.test.jsx new file mode 100644 index 000000000..03c164bd1 --- /dev/null +++ b/client-report/src/components/participantsGraph/participantsGraph.test.jsx @@ -0,0 +1,121 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import ParticipantsGraph from './participantsGraph'; +import '@testing-library/jest-dom'; +import * as d3contour from "d3-contour"; +import * as d3chromatic from "d3-scale-chromatic"; +import * as d3geo from "d3-geo"; + +Object.defineProperty(window, 'd3', { + writable: true, +}); + +global.window.d3 = { + scaleLinear: jest.fn().mockReturnValue({ + rangeRound: jest.fn().mockReturnValue({ + domain: jest.fn() + }), + domain: jest.fn() + }), + scaleSequential: jest.fn(() => ({ + domain: jest.fn(() => jest.fn()), // Mock domain function + })), + geoPath: jest.fn(() => jest.fn()), + extent: jest.fn(() => [0, 1]), // Mock extent to return a default range + forceSimulation: jest.fn().mockReturnValue({ + force: jest.fn().mockReturnThis(), + stop: jest.fn().mockReturnThis(), + tick: jest.fn() + }), + forceX: jest.fn().mockReturnValue({ // Add a mock return for forceX + strength: jest.fn().mockReturnThis() // Add a mock for strength + }), + forceY: jest.fn(), + forceCollide: jest.fn(), + voronoi: jest.fn().mockReturnValue({ + extent: jest.fn().mockReturnThis(), + x: jest.fn().mockReturnThis(), + y: jest.fn().mockReturnThis(), + polygons: jest.fn().mockReturnValue([ + { + join: jest.fn(), + data: {} + }, + ]) + }) +} + +jest.mock('d3-contour', () => ({ + contourDensity: jest.fn(() => ({ + x: jest.fn(() => ({ + y: jest.fn(() => ({ + size: jest.fn(() => ({ + bandwidth: jest.fn(() => jest.fn()) + })) + })) + })) + })) +})); + +jest.mock('d3-scale-chromatic', () => ({ + interpolateYlGnBu: jest.fn() +})); + +jest.mock('d3-geo', () => ({ + geoPath: jest.fn() +})); + +import * as d3 from 'd3'; + +// Mock data (replace with your actual data structure) +const mockProps = { + math: { + "base-clusters": { count: { id1: 10, id2: 5 }, id: 0, x: 0, y: 0 }, + "group-clusters": [ + { id: 0, center: [0.5, 0.5] }, + { id: 1, center: [0.2, 0.8] }, + ], + tids: ['tid1', 'tid2'], + pca: { + 'comment-projection': [ + [], + [] + ] + } + }, + comments: [ + { tid: 'tid1', text: 'Comment 1' }, + { tid: 'tid2', text: 'Comment 2' }, + ], + voteColors: { agree: 'green', disagree: 'red' }, + // Add other props as needed +}; + +// Test case for rendering with basic data +test('renders participants graph with basic data', () => { + render(); + + // Check for presence of elements + expect(screen.getByRole('button', { name: 'Axes' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Statements' })).toBeInTheDocument(); + + // Check for group labels (if data includes group labels) + if (mockProps.math["group-clusters"][0].hasOwnProperty('label')) { + expect(screen.getByText(mockProps.math["group-clusters"][0].label)).toBeInTheDocument(); + } +}); + +// Test case for clicking "Statements" button +test('clicking statements button shows comments', () => { + render(); + + const statementsButton = screen.getByRole('button', { name: 'Statements' }); + expect(statementsButton.textContent).toBe('Statements'); + + fireEvent.click(statementsButton); + + // Check for presence of comments (implementation might vary) + expect(screen.getByText('Comment 1')).toBeInTheDocument(); +}); + + diff --git a/client-report/src/util/graphUtil.js b/client-report/src/util/graphUtil.js index 5a3c33d89..6851ba8eb 100644 --- a/client-report/src/util/graphUtil.js +++ b/client-report/src/util/graphUtil.js @@ -2,13 +2,17 @@ import * as globals from "../components/globals"; import createHull from "hull.js"; +import _ from 'lodash' const graphUtil = (comments, math, badTids) => { const allXs = []; const allYs = []; - const commentsByTid = _.keyBy(comments, "tid"); + const commentsByTid = comments.reduce((accumulator, comment) => { + accumulator[comment.tid] = comment; + return accumulator; + }, {}); const indexToTid = math.tids; const tidToIndex = []; for (let i = 0; i < indexToTid.length; i++) { From 6d80224eec225a5225aa55f2be7cee6ed8902033 Mon Sep 17 00:00:00 2001 From: tevko Date: Sun, 15 Dec 2024 17:41:10 -0600 Subject: [PATCH 11/16] app.js conversion to functional --- client-report/src/components/app.js | 751 ++++++++++++++-------------- 1 file changed, 368 insertions(+), 383 deletions(-) diff --git a/client-report/src/components/app.js b/client-report/src/components/app.js index e98900410..a475b614a 100644 --- a/client-report/src/components/app.js +++ b/client-report/src/components/app.js @@ -1,7 +1,6 @@ // Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . -import React from "react"; -import _ from "lodash"; +import React, { useState, useEffect } from "react"; import * as globals from "./globals"; import URLs from "../util/url"; @@ -17,16 +16,12 @@ import ParticipantGroups from "./lists/participantGroups.jsx"; import ParticipantsGraph from "./participantsGraph/participantsGraph.jsx"; import Beeswarm from "./beeswarm/beeswarm.jsx"; import Controls from "./controls/controls.jsx"; - import net from "../util/net"; - -import $ from "jquery"; - import ConsensusNarrative from "./lists/consensusNarrative.jsx"; import RawDataExport from "./RawDataExport"; -var pathname = window.location.pathname; // "/report/2arcefpshi" -var report_id = pathname.split("/")[2]; +const pathname = window.location.pathname; // "/report/2arcefpshi" +const report_id = pathname.split("/")[2]; function assertExists(obj, key) { if (typeof obj[key] === "undefined") { @@ -34,49 +29,62 @@ function assertExists(obj, key) { } } -class App extends React.Component { - constructor(props) { - super(props); - this.state = { - loading: true, - consensus: null, - comments: null, - participants: null, - conversation: null, - groupDemographics: null, - colorBlindMode: false, - model: "claude", - isNarrativeReport: window.location.pathname.split("/")[1] === "narrativeReport", - dimensions: { - width: window.innerWidth, - height: window.innerHeight, - }, - shouldPoll: false, - voteColors: { - agree: globals.brandColors.agree, - disagree: globals.brandColors.disagree, - pass: globals.brandColors.pass, - }, - narrative: { - }, - }; - } - - async componentDidUpdate() { +const App = (props) => { + const [loading, setLoading] = useState(true); + const [consensus, setConsensus] = useState(null); + const [math, setMath] = useState(null); + const [comments, setComments] = useState(null); + const [participants, setParticipants] = useState(null); + const [conversation, setConversation] = useState(null); + const [groupDemographics, setGroupDemographics] = useState(null); + const [colorBlindMode, setColorBlindMode] = useState(false); + const [model, setModel] = useState("claude"); + const [isNarrativeReport, setIsNarrativeReport] = useState(window.location.pathname.split("/")[1] === "narrativeReport"); + const [dimensions, setDimensions] = useState({ + width: window.innerWidth, + height: window.innerHeight, + }); + const [shouldPoll, setShouldPoll] = useState(false); + const [voteColors, setVoteColors] = useState({ + agree: globals.brandColors.agree, + disagree: globals.brandColors.disagree, + pass: globals.brandColors.pass, + }); + const [narrative, setNarrative] = useState({}); + const [errorText, setErrorText] = useState(null); + const [extremity, setExtremity] = useState(null); + const [uncertainty, setUncertainty] = useState(null); + const [ptptCount, setPtptCount] = useState(null); + const [ptptCountTotal, SetPtptCountTotal] = useState(null); + const [filteredCorrelationMatrix, setFilteredCorrelationMatrix] = useState(null); + const [filteredCorrelationTids, setFilteredCorrelationTids] = useState(null); + const [badTids, setBadTids] = useState(null); + const [groupNames, setGroupNames] = useState(null); + const [repfulAgreeTidsByGroup, setRepfulAgreeTidsByGroup] = useState(null); + const[repfulDisageeTidsByGroup, setRepfulDisageeTidsByGroup] = useState(null); + const [formatTid, setFormatTid] = useState(null); + const [report, setReport] = useState(null); + const [computedStats, setComputedStats] = useState(null); + const [nothingToShow, setNothingToShow] = useState(true); + const [hasError, setError] = useState(false); + + let corMatRetries; + + useEffect(() => { if ( window.location.pathname.split("/")[1] === "narrativeReport" && - this.state.isNarrativeReport !== true + isNarrativeReport !== true ) { - this.setState({ isNarrativeReport: true }); + setIsNarrativeReport(true); } else if ( - this.state.isNarrativeReport && + isNarrativeReport && window.location.pathname.split("/")[1] !== "narrativeReport" ) { - this.setState({ isNarrativeReport: false }); + setIsNarrativeReport(false); } - } + }, [window.location?.pathname]); - getMath(conversation_id) { + const getMath = async (conversation_id) => { return net .polisGet("/api/v3/math/pca2", { lastVoteTimestamp: 0, @@ -90,7 +98,7 @@ class App extends React.Component { }); } - getComments(conversation_id, isStrictMod) { + const getComments = (conversation_id, isStrictMod) => { return net.polisGet("/api/v3/comments", { conversation_id: conversation_id, report_id: report_id, @@ -102,18 +110,18 @@ class App extends React.Component { }); } - getParticipantsOfInterest(conversation_id) { + const getParticipantsOfInterest = (conversation_id) => { return net.polisGet("/api/v3/ptptois", { conversation_id: conversation_id, }); } - getConversation(conversation_id) { + const getConversation = (conversation_id) => { return net.polisGet("/api/v3/conversations", { conversation_id: conversation_id, }); } - async getNarrative(report_id) { + const getNarrative = async (report_id) => { const urlPrefix = URLs.urlPrefix; const response = await fetch(`${urlPrefix}api/v3/reportNarrative?report_id=${report_id}`, { credentials: "include", @@ -138,11 +146,11 @@ class App extends React.Component { } const decodedChunk = decoder.decode(value, { stream: true }); - if (!decodedChunk.includes('POLIS-PING:')) this.setState(state => ({ narrative: Object.assign(state.narrative, JSON.parse(decodedChunk)) })) + if (!decodedChunk.includes('POLIS-PING:')) setNarrative(n => Object.assign(n, JSON.parse(decodedChunk))); } } - getReport(report_id) { + const getReport = (report_id) => { return net .polisGet("/api/v3/reports", { report_id: report_id, @@ -154,21 +162,21 @@ class App extends React.Component { return null; }); } - getGroupDemographics(conversation_id) { + const getGroupDemographics = (conversation_id) => { return net.polisGet("/api/v3/group_demographics", { conversation_id: conversation_id, report_id: report_id, }); } - getConversationStats(conversation_id) { + const getConversationStats = (conversation_id) => { return net.polisGet("/api/v3/conversationStats", { conversation_id: conversation_id, report_id: report_id, }); } - getCorrelationMatrix(math_tick) { + const getCorrelationMatrix = (math_tick) => { const attemptResponse = net.polisGet("/api/v3/math/correlationMatrix", { math_tick: math_tick, report_id: report_id, @@ -178,21 +186,23 @@ class App extends React.Component { attemptResponse.then( (response) => { if (response.status && response.status === "pending") { - this.corMatRetries = _.isNumber(this.corMatRetries) ? this.corMatRetries + 1 : 1; + if (typeof corMatRetries === 'number') { + corMatRetries = corMatRetries + 1; + } else { + corMatRetries = 1; + } setTimeout( () => { - this.getCorrelationMatrix(math_tick).then(resolve, reject); + getCorrelationMatrix(math_tick).then(resolve, reject); }, - this.corMatRetries < 10 ? 200 : 3000 + corMatRetries < 10 ? 200 : 3000 ); // try to get a quick response, but don't keep polling at that rate for more than 10 seconds. } else if ( globals.enableMatrix && response && response.status === "polis_report_needs_comment_selection" ) { - this.setState({ - errorText: "Select some comments", - }); + setErrorText("Select some comments"); reject("Currently, No comments are selected for display in the matrix."); } else { resolve(response); @@ -205,37 +215,34 @@ class App extends React.Component { }); } - getData() { - const reportPromise = this.getReport(report_id); + const getData = async () => { + const reportPromise = getReport(report_id); const mathPromise = reportPromise.then((report) => { - return this.getMath(report.conversation_id); + return getMath(report.conversation_id); }); const commentsPromise = reportPromise.then((report) => { return conversationPromise.then((conv) => { - return this.getComments(report.conversation_id, conv.strict_moderation); + return getComments(report.conversation_id, conv.strict_moderation); }); }); const groupDemographicsPromise = reportPromise.then((report) => { - return this.getGroupDemographics(report.conversation_id); + return getGroupDemographics(report.conversation_id); }); - //const conversationStatsPromise = reportPromise.then((report) => { - //return this.getConversationStats(report.conversation_id) - //}); const participantsOfInterestPromise = reportPromise.then((report) => { - return this.getParticipantsOfInterest(report.conversation_id); + return getParticipantsOfInterest(report.conversation_id); }); const matrixPromise = globals.enableMatrix ? mathPromise.then((math) => { const math_tick = math.math_tick; - return this.getCorrelationMatrix(math_tick); + return getCorrelationMatrix(math_tick); }) : Promise.resolve(); const conversationPromise = reportPromise.then((report) => { - return this.getConversation(report.conversation_id); + return getConversation(report.conversation_id); }); const narrativePromise = reportPromise.then((report) => { - if (this.state.isNarrativeReport) this.getNarrative(report.report_id); + if (isNarrativeReport) getNarrative(report.report_id); }); Promise.all([ @@ -246,20 +253,18 @@ class App extends React.Component { participantsOfInterestPromise, matrixPromise, conversationPromise, - //conversationStatsPromise, narrativePromise, ]) .then((a) => { let [ - report, + _report, mathResult, - comments, - groupDemographics, - participants, + _comments, + _groupDemographics, + _participants, correlationHClust, - conversation, + _conversation, narrative, - //conversationstats, ] = a; assertExists(mathResult, "base-clusters"); @@ -281,17 +286,19 @@ class App extends React.Component { let indexToTid = mathResult.tids; // # ptpts that voted - var ptptCountTotal = conversation.participant_count; + var _ptptCountTotal = _conversation.participant_count; // # ptpts that voted enough to be included in math - var ptptCount = 0; - _.each(mathResult["group-votes"], (val /*, key*/) => { - ptptCount += val["n-members"]; - }); + var _ptptCount = 0; + const groupVotes = mathResult["group-votes"]; + for (const key in groupVotes) { + const val = groupVotes[key]; + _ptptCount += val["n-members"]; + } - var badTids = {}; - var filteredTids = {}; - var filteredProbabilities = {}; + var _badTids = {}; + var _filteredTids = {}; + var _filteredProbabilities = {}; // prep Correlation matrix. if (globals.enableMatrix) { @@ -300,29 +307,29 @@ class App extends React.Component { for (let row = 0; row < probabilities.length; row++) { if (probabilities[row][0] === "NaN") { let tid = correlationHClust.comments[row]; - badTids[tid] = true; + _badTids[tid] = true; } } - filteredProbabilities = probabilities + _filteredProbabilities = probabilities .map((row) => { return row.filter((cell, colNum) => { let colTid = correlationHClust.comments[colNum]; - return badTids[colTid] !== true; + return _badTids[colTid] !== true; }); }) .filter((row, rowNum) => { let rowTid = correlationHClust.comments[rowNum]; - return badTids[rowTid] !== true; + return _badTids[rowTid] !== true; }); - filteredTids = tids.filter((tid /*, index*/) => { - return badTids[tid] !== true; + _filteredTids = tids.filter((tid /*, index*/) => { + return _badTids[tid] !== true; }); } var maxTid = -1; - for (let i = 0; i < comments.length; i++) { - if (comments[i].tid > maxTid) { - maxTid = comments[i].tid; + for (let i = 0; i < _comments.length; i++) { + if (_comments[i].tid > maxTid) { + maxTid = _comments[i].tid; } } var tidWidth = ("" + maxTid).length; @@ -332,349 +339,327 @@ class App extends React.Component { n = n + ""; return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n; } - function formatTid(tid) { - // let padded = "" + tid; - // return '#' + pad(""+tid, tidWidth); + function _formatTid(tid) { return pad("" + tid, tidWidth); } - let repfulAgreeTidsByGroup = {}; - let repfulDisageeTidsByGroup = {}; + const _repfulAgreeTidsByGroup = {}; + const _repfulDisageeTidsByGroup = {}; + if (mathResult.repness) { - _.each(mathResult.repness, (entries, gid) => { + for (const gid in mathResult.repness) { + const entries = mathResult.repness[gid]; + entries.forEach((entry) => { if (entry["repful-for"] === "agree") { - repfulAgreeTidsByGroup[gid] = repfulAgreeTidsByGroup[gid] || []; - repfulAgreeTidsByGroup[gid].push(entry.tid); + _repfulAgreeTidsByGroup[gid] = _repfulAgreeTidsByGroup[gid] || []; + _repfulAgreeTidsByGroup[gid].push(entry.tid); } else if (entry["repful-for"] === "disagree") { - repfulDisageeTidsByGroup[gid] = repfulDisageeTidsByGroup[gid] || []; - repfulDisageeTidsByGroup[gid].push(entry.tid); + _repfulDisageeTidsByGroup[gid] = _repfulDisageeTidsByGroup[gid] || []; + _repfulDisageeTidsByGroup[gid].push(entry.tid); } }); - }); + } } // ====== REMEMBER: gid's start at zero, (0, 1, 2) but we show them as group 1, 2, 3 in participation view ====== - let groupNames = {}; + let _groupNames = {}; for (let i = 0; i <= 9; i++) { - let label = report["label_group_" + i]; + let label = _report["label_group_" + i]; if (label) { - groupNames[i] = label; + _groupNames[i] = label; } } - let uncertainty = []; - - // let maxCount = _.reduce(comments, (memo, c) => { return Math.max(c.count, memo);}, 1); - comments.map((c) => { + let _uncertainty = []; + _comments.map((c) => { var unc = c.pass_count / c.count; if (unc > 0.3) { c.unc = unc; - uncertainty.push(c); + _uncertainty.push(c); } }); - uncertainty.sort((a, b) => { + _uncertainty.sort((a, b) => { return b.unc * b.unc * b.pass_count - a.unc * a.unc * a.pass_count; }); - uncertainty = uncertainty.slice(0, 5); + _uncertainty = _uncertainty.slice(0, 5); - let extremity = {}; - _.each(mathResult.pca["comment-extremity"], function (e, index) { - extremity[indexToTid[index]] = e; - }); + const _extremity = {}; + + for (const index in mathResult.pca["comment-extremity"]) { + const e = mathResult.pca["comment-extremity"][index]; + const tid = indexToTid[index]; + _extremity[tid] = e; + } var uniqueCommenters = {}; var voteTotals = DataUtils.getVoteTotals(mathResult); - comments = comments.map((c) => { + _comments = _comments.map((c) => { c["group-aware-consensus"] = mathResult["group-aware-consensus"][c.tid]; uniqueCommenters[c.pid] = 1; c = Object.assign(c, voteTotals[c.tid]); return c; }); - var numUniqueCommenters = _.keys(uniqueCommenters).length; - var totalVotes = _.reduce( - _.values(mathResult["user-vote-counts"]), - function (memo, num) { - return memo + num; - }, - 0 - ); - const computedStats = { - votesPerVoterAvg: totalVotes / ptptCountTotal, - commentsPerCommenterAvg: comments.length / numUniqueCommenters, + var numUniqueCommenters = Object.keys(uniqueCommenters).length; + let totalVotes = 0; + for (const key in mathResult["user-vote-counts"]) { + totalVotes += mathResult["user-vote-counts"][key]; + } + const _computedStats = { + votesPerVoterAvg: totalVotes / _ptptCountTotal, + commentsPerCommenterAvg: _comments.length / numUniqueCommenters, }; - this.setState({ - loading: false, - math: mathResult, - consensus: mathResult.consensus, - extremity: extremity, - uncertainty: uncertainty.map((c) => { - return c.tid; - }), - comments: comments, - demographics: groupDemographics, - participants: participants, - conversation: conversation, - ptptCount: ptptCount, - ptptCountTotal: ptptCountTotal, - filteredCorrelationMatrix: filteredProbabilities, - filteredCorrelationTids: filteredTids, - badTids: badTids, - groupNames: groupNames, - repfulAgreeTidsByGroup: repfulAgreeTidsByGroup, - repfulDisageeTidsByGroup: repfulDisageeTidsByGroup, - formatTid: formatTid, - report: report, - // narrative: this.state.isNarrativeReport ? narrative : undefined, - //conversationStats: conversationstats, - computedStats: computedStats, - nothingToShow: !comments.length || !groupDemographics.length, - }); + setLoading(false); + setMath(mathResult); + setConsensus(mathResult.consensus); + setExtremity(_extremity); + setUncertainty(_uncertainty.map((c) => { + return c.tid; + })); + setComments(_comments); + setGroupDemographics(_groupDemographics); + setParticipants(_participants); + setConversation(_conversation); + setPtptCount(_ptptCount); + SetPtptCountTotal(_ptptCountTotal); + setFilteredCorrelationMatrix(_filteredProbabilities); + setFilteredCorrelationTids(_filteredTids); + setBadTids(_badTids); + setGroupNames(_groupNames); + setRepfulAgreeTidsByGroup(_repfulAgreeTidsByGroup); + setRepfulDisageeTidsByGroup(_repfulDisageeTidsByGroup); + setFormatTid(_formatTid); + setReport(_report); + setComputedStats(_computedStats); + setNothingToShow(!comments.length || !_groupDemographics.length); }) .catch((err) => { - this.setState({ - error: true, - errorText: String(err), - }); + setError(true); + setErrorText(String(err)); }); } - async UNSAFE_componentWillMount() { - this.getData(); - - setInterval(() => { - if (this.state.shouldPoll) { - this.getData(); - } - }, 20 * 1000); - - window.addEventListener( - "resize", - _.throttle(() => { - this.setState({ - dimensions: { - width: window.innerWidth, - height: window.innerHeight, - }, - }); - }, 500) - ); - } - - onAutoRefreshEnabled() { - this.setState({ - shouldPoll: true, + useEffect(() => { + const init = async () => { + await getData(); + setInterval(() => { + if (shouldPoll) { + getData(); + } + }, 20 * 1000); + }; + function handleResize() { + setDimensions({ + width: window.innerWidth, + height: window.innerHeight, + }); + } + + let resizeTimeout; + window.addEventListener("resize", () => { + clearTimeout(resizeTimeout); + resizeTimeout = setTimeout(handleResize, 500); }); + init(); + }, []); + + const onAutoRefreshEnabled = () => { + setShouldPoll(true); } - onAutoRefreshDisabled() { - this.setState({ - shouldPoll: false, - }); + const onAutoRefreshDisabled = () => { + setShouldPoll(false); } - handleColorblindModeClick() { - var colorBlind = !this.state.colorBlindMode; + const handleColorblindModeClick = () => { + var colorBlind = !colorBlindMode; if (colorBlind) { - this.setState({ - colorBlindMode: colorBlind, - voteColors: Object.assign(this.state.voteColors, { - agree: globals.brandColors.agreeColorblind, - }), - }); + setColorBlindMode(colorBlind); + setVoteColors(Object.assign(voteColors, { + agree: globals.brandColors.agreeColorblind, + })); } else { - this.setState({ - colorBlindMode: colorBlind, - voteColors: Object.assign(this.state.voteColors, { - agree: globals.brandColors.agree, - }), - }); + setColorBlindMode(colorBlind); + setVoteColors(Object.assign(voteColors, { + agree: globals.brandColors.agree, + })); } } - render() { - if (this.state.error) { - return ( -
-
Error Loading
-
{this.state.errorText}
-
- ); - } - if (this.state.nothingToShow) { - return ( -
-
Nothing to show yet
-
- ); - } - if (this.state.loading) { - return ( -
-
Loading ...
-
- ); - } - + if (hasError) { + return ( +
+
Error Loading
+
{errorText}
+
+ ); + } + if (nothingToShow) { + return ( +
+
Nothing to show yet
+
+ ); + } + if (loading) { return ( -
- -
- - - {/* This may eventually need to go back in below */} - {/* stats={this.state.conversationStats} */} - - - {!this.state.isNarrativeReport && ( - - )} - - {this.state.isNarrativeReport ? ( - <> - -

Current Model: {this.state.model}

- - - - ) : ( - <> - - {/* -

Consensus

-

Inclusive Majority

- */} - - - - - {/* {false ? : null} - {globals.enableMatrix && false ? : ""} */} - - {/* */} - - - )} -
-
+
+
Loading ...
); } + + return ( +
+ +
+ + + {/* This may eventually need to go back in below */} + {/* stats={conversationStats} */} + + + {!isNarrativeReport && ( + + )} + + {isNarrativeReport ? ( + <> + +

Current Model: {model}

+ + + + ) : ( + <> + + + + + {/* {false ? : null} + {globals.enableMatrix && false ? : ""} */} + + {/* */} + + + )} +
+
+
+ ); } export default App; - -window.$ = $; From 4a9a7295b5b1db1b313f9e91cdb3561a9faf23c5 Mon Sep 17 00:00:00 2001 From: tevko Date: Sun, 15 Dec 2024 18:07:45 -0600 Subject: [PATCH 12/16] free of bugs --- client-report/src/components/app.js | 17 ++- client-report/src/util/net.js | 178 +++++++++++++++++++--------- 2 files changed, 136 insertions(+), 59 deletions(-) diff --git a/client-report/src/components/app.js b/client-report/src/components/app.js index a475b614a..0505efec3 100644 --- a/client-report/src/components/app.js +++ b/client-report/src/components/app.js @@ -62,7 +62,7 @@ const App = (props) => { const [groupNames, setGroupNames] = useState(null); const [repfulAgreeTidsByGroup, setRepfulAgreeTidsByGroup] = useState(null); const[repfulDisageeTidsByGroup, setRepfulDisageeTidsByGroup] = useState(null); - const [formatTid, setFormatTid] = useState(null); + const [formatTid, setFormatTid] = useState(() => v => v); const [report, setReport] = useState(null); const [computedStats, setComputedStats] = useState(null); const [nothingToShow, setNothingToShow] = useState(true); @@ -301,7 +301,7 @@ const App = (props) => { var _filteredProbabilities = {}; // prep Correlation matrix. - if (globals.enableMatrix) { + if (globals.enableMatrix && correlationHClust) { var probabilities = correlationHClust.matrix; var tids = correlationHClust.comments; for (let row = 0; row < probabilities.length; row++) { @@ -429,12 +429,13 @@ const App = (props) => { setGroupNames(_groupNames); setRepfulAgreeTidsByGroup(_repfulAgreeTidsByGroup); setRepfulDisageeTidsByGroup(_repfulDisageeTidsByGroup); - setFormatTid(_formatTid); + setFormatTid(() => _formatTid); setReport(_report); setComputedStats(_computedStats); - setNothingToShow(!comments.length || !_groupDemographics.length); + setNothingToShow(!_comments.length || !_groupDemographics.length); }) .catch((err) => { + console.error(err); setError(true); setErrorText(String(err)); }); @@ -462,6 +463,7 @@ const App = (props) => { resizeTimeout = setTimeout(handleResize, 500); }); init(); + console.log("calling init") }, []); const onAutoRefreshEnabled = () => { @@ -510,6 +512,8 @@ const App = (props) => { ); } + console.log("FORMATTID", typeof formatTid) + return (
@@ -536,7 +540,7 @@ const App = (props) => { comments={comments} ptptCount={ptptCount} ptptCountTotal={ptptCountTotal} - demographics={demographics} + demographics={groupDemographics} conversation={conversation} voteColors={voteColors} /> @@ -581,6 +585,7 @@ const App = (props) => { probabilities={filteredCorrelationMatrix} probabilitiesTids={filteredCorrelationTids} voteColors={voteColors} + formatTid={formatTid} /> { . +// // Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . -import URLs from "./url"; +// import URLs from "./url"; + +// var urlPrefix = URLs.urlPrefix; + +// function polisAjax(api, data, type) { +// if (typeof api !== "string") { +// throw "api param should be a string"; +// } + +// if (api && api.length && api[0] === '/') { +// api = api.slice(1); +// } + +// var url = urlPrefix + api; + +// // Add the auth token if needed. +// // if (_.contains(authenticatedCalls, api)) { +// // var token = tokenStore.get(); +// // if (!token) { +// // needAuthCallbacks.fire(); +// // console.error("auth needed"); +// // return $.Deferred().reject("auth needed"); +// // } +// // //data = $.extend({ token: token}, data); // moving to cookies +// // } + +// var promise; +// var config = { +// url: url, +// contentType: "application/json; charset=utf-8", +// headers: { +// //"Cache-Control": "no-cache" // no-cache +// "Cache-Control": "max-age=0" +// }, +// xhrFields: { +// withCredentials: true +// }, +// // crossDomain: true, +// dataType: "json" +// }; +// if ("GET" === type) { +// promise = $.ajax($.extend(config, { +// type: "GET", +// data: data +// })); +// } else if ("POST" === type) { +// promise = $.ajax($.extend(config, { +// type: "POST", +// data: JSON.stringify(data) +// })); +// } + +// promise.fail( function(jqXHR/*, message, errorType*/) { + +// // sendEvent("Error", api, jqXHR.status); + +// // logger.error("SEND ERROR"); +// console.dir(arguments); +// if (403 === jqXHR.status) { +// // eb.trigger(eb.authNeeded); +// } +// //logger.dir(data); +// //logger.dir(message); +// //logger.dir(errorType); +// }); +// return promise; +// } + +// function polisPost(api, data) { +// return polisAjax(api, data, "POST"); +// } -var urlPrefix = URLs.urlPrefix; +// function polisGet(api, data) { +// return polisAjax(api, data, "GET"); +// } -// var pid = "unknownpid"; +// const PolisNet = { +// polisAjax: polisAjax, +// polisPost: polisPost, +// polisGet: polisGet, +// }; +// export default PolisNet; + +// Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public License, version 3, as published by the +// Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU Affero General Public License for more details. You should have received a copy of the +// GNU Affero General Public License along with this program. If not, see . + +import URLs from "./url"; + +const urlPrefix = URLs.urlPrefix; function polisAjax(api, data, type) { - if (!_.isString(api)) { + if (typeof api !== "string") { throw "api param should be a string"; } @@ -15,59 +103,42 @@ function polisAjax(api, data, type) { api = api.slice(1); } - var url = urlPrefix + api; - - // Add the auth token if needed. - // if (_.contains(authenticatedCalls, api)) { - // var token = tokenStore.get(); - // if (!token) { - // needAuthCallbacks.fire(); - // console.error("auth needed"); - // return $.Deferred().reject("auth needed"); - // } - // //data = $.extend({ token: token}, data); // moving to cookies - // } - - var promise; - var config = { - url: url, - contentType: "application/json; charset=utf-8", + let url = urlPrefix + api; + + const options = { + method: type, headers: { - //"Cache-Control": "no-cache" // no-cache - "Cache-Control": "max-age=0" + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "max-age=0", }, - xhrFields: { - withCredentials: true - }, - // crossDomain: true, - dataType: "json" + credentials: "include", // This sends cookies with the request }; - if ("GET" === type) { - promise = $.ajax($.extend(config, { - type: "GET", - data: data - })); - } else if ("POST" === type) { - promise = $.ajax($.extend(config, { - type: "POST", - data: JSON.stringify(data) - })); - } - - promise.fail( function(jqXHR/*, message, errorType*/) { - // sendEvent("Error", api, jqXHR.status); + if (type === "POST") { + options.body = JSON.stringify(data); + } else if (type === "GET" && data) { + // Add data as query parameters for GET requests + const queryParams = new URLSearchParams(data); + url += `?${queryParams}`; + } - // logger.error("SEND ERROR"); - console.dir(arguments); - if (403 === jqXHR.status) { - // eb.trigger(eb.authNeeded); - } - //logger.dir(data); - //logger.dir(message); - //logger.dir(errorType); - }); - return promise; + return fetch(url, options) + .then(response => { + if (!response.ok) { + // Handle error responses (e.g., 403) + console.error("Error:", response.status, response.statusText); + if (response.status === 403) { + // eb.trigger(eb.authNeeded); + } + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.json(); + }) + .catch(error => { + console.error("Fetch error:", error); + // Handle fetch errors + throw error; + }); } function polisPost(api, data) { @@ -83,5 +154,6 @@ const PolisNet = { polisPost: polisPost, polisGet: polisGet, }; + export default PolisNet; From 5218dd85c82480fee59ef80a90758660ded1b1e0 Mon Sep 17 00:00:00 2001 From: tevko Date: Mon, 16 Dec 2024 15:21:22 -0600 Subject: [PATCH 13/16] test fixing --- client-report/jest.config.js | 1 + client-report/package-lock.json | 75 +++++++++++++++++++ client-report/package.json | 1 + client-report/setupTests.js | 5 ++ .../src/components/{app.js => app.jsx} | 15 ++-- client-report/src/components/app.test.jsx | 29 +++++++ .../participantsGraph.test.jsx | 55 ++++++-------- client-report/src/index.js | 2 +- 8 files changed, 142 insertions(+), 41 deletions(-) create mode 100644 client-report/setupTests.js rename client-report/src/components/{app.js => app.jsx} (98%) create mode 100644 client-report/src/components/app.test.jsx diff --git a/client-report/jest.config.js b/client-report/jest.config.js index 2e5dfe061..d44176cd8 100644 --- a/client-report/jest.config.js +++ b/client-report/jest.config.js @@ -193,6 +193,7 @@ const config = { // Whether to use watchman for file crawling // watchman: true, + setupFilesAfterEnv: ["/setupTests.js"] }; module.exports = config; diff --git a/client-report/package-lock.json b/client-report/package-lock.json index b32d43ac0..53bfb838f 100644 --- a/client-report/package-lock.json +++ b/client-report/package-lock.json @@ -46,6 +46,7 @@ "html-webpack-plugin": "^5.5.3", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", + "jest-fetch-mock": "^3.0.3", "mini-css-extract-plugin": "^2.7.6", "webpack": "~5.88.0", "webpack-cli": "~5.1.4", @@ -4472,6 +4473,16 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/cross-fetch": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", + "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -8293,6 +8304,17 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-fetch-mock": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz", + "integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-fetch": "^3.0.4", + "promise-polyfill": "^8.1.3" + } + }, "node_modules/jest-get-type": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", @@ -9395,6 +9417,52 @@ "tslib": "^2.0.3" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -10185,6 +10253,13 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", "integrity": "sha512-yN0WQmuCX63LP/TMvAg31nvT6m4vDqJEiiv2CAZqWOGNWutc9DfDk1NPYYmKUFmaVM2UwDowH4u5AHWYP/jxKw==" }, + "node_modules/promise-polyfill": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz", + "integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==", + "dev": true, + "license": "MIT" + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", diff --git a/client-report/package.json b/client-report/package.json index 41b75043a..e6d63206c 100644 --- a/client-report/package.json +++ b/client-report/package.json @@ -33,6 +33,7 @@ "html-webpack-plugin": "^5.5.3", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", + "jest-fetch-mock": "^3.0.3", "mini-css-extract-plugin": "^2.7.6", "webpack": "~5.88.0", "webpack-cli": "~5.1.4", diff --git a/client-report/setupTests.js b/client-report/setupTests.js new file mode 100644 index 000000000..247eb4e46 --- /dev/null +++ b/client-report/setupTests.js @@ -0,0 +1,5 @@ +// import fetch from 'node-fetch'; +import '@testing-library/jest-dom'; +import fetchMock from 'jest-fetch-mock'; + +fetchMock.enableMocks(); \ No newline at end of file diff --git a/client-report/src/components/app.js b/client-report/src/components/app.jsx similarity index 98% rename from client-report/src/components/app.js rename to client-report/src/components/app.jsx index 0505efec3..8e005844e 100644 --- a/client-report/src/components/app.js +++ b/client-report/src/components/app.jsx @@ -2,12 +2,12 @@ import React, { useState, useEffect } from "react"; -import * as globals from "./globals"; -import URLs from "../util/url"; -import DataUtils from "../util/dataUtils"; +import * as globals from "./globals.js"; +import URLs from "../util/url.js"; +import DataUtils from "../util/dataUtils.js"; import Heading from "./framework/heading.jsx"; import Footer from "./framework/Footer.jsx"; -import Overview from "./overview"; +import Overview from "./overview.js"; import MajorityStrict from "./lists/majorityStrict.jsx"; import Uncertainty from "./lists/uncertainty.jsx"; import UncertaintyNarrative from "./lists/uncertaintyNarrative.jsx"; @@ -16,9 +16,9 @@ import ParticipantGroups from "./lists/participantGroups.jsx"; import ParticipantsGraph from "./participantsGraph/participantsGraph.jsx"; import Beeswarm from "./beeswarm/beeswarm.jsx"; import Controls from "./controls/controls.jsx"; -import net from "../util/net"; +import net from "../util/net.js"; import ConsensusNarrative from "./lists/consensusNarrative.jsx"; -import RawDataExport from "./RawDataExport"; +import RawDataExport from "./RawDataExport.js"; const pathname = window.location.pathname; // "/report/2arcefpshi" const report_id = pathname.split("/")[2]; @@ -463,7 +463,6 @@ const App = (props) => { resizeTimeout = setTimeout(handleResize, 500); }); init(); - console.log("calling init") }, []); const onAutoRefreshEnabled = () => { @@ -512,8 +511,6 @@ const App = (props) => { ); } - console.log("FORMATTID", typeof formatTid) - return (
diff --git a/client-report/src/components/app.test.jsx b/client-report/src/components/app.test.jsx new file mode 100644 index 000000000..fafc3cdbe --- /dev/null +++ b/client-report/src/components/app.test.jsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import App from './app'; + +// Mock data for testing +const mockData = { + conversation: { + conversation_id: 123, + }, + loading: false, + errorText: null, + nothingToShow: false, +}; + +jest.mock('./globals.js', () => ({ + brandColors: { + agree: 'green', + disagree: 'red', + pass: 'yellow', + }, + enableMatrix: true, +})); + +test('renders nothing to show message if there is no data', () => { + render(); + const nothingToShowText = screen.getByText(/Nothing to show yet/); + expect(nothingToShowText).toBeInTheDocument(); +}); \ No newline at end of file diff --git a/client-report/src/components/participantsGraph/participantsGraph.test.jsx b/client-report/src/components/participantsGraph/participantsGraph.test.jsx index 03c164bd1..4f9c41021 100644 --- a/client-report/src/components/participantsGraph/participantsGraph.test.jsx +++ b/client-report/src/components/participantsGraph/participantsGraph.test.jsx @@ -11,15 +11,15 @@ Object.defineProperty(window, 'd3', { }); global.window.d3 = { - scaleLinear: jest.fn().mockReturnValue({ - rangeRound: jest.fn().mockReturnValue({ - domain: jest.fn() - }), - domain: jest.fn() + scaleLinear: jest.fn(() => { + const mockScale = { // Create a mock scale object + domain: jest.fn(() => mockScale), // Return the mockScale itself + rangeRound: jest.fn(() => mockScale), // Return the mockScale itself + range: jest.fn(() => jest.fn()), // Add range for completeness + // Add other methods as needed (e.g., tickFormat) + }; + return mockScale; // Return the mock scale object }), - scaleSequential: jest.fn(() => ({ - domain: jest.fn(() => jest.fn()), // Mock domain function - })), geoPath: jest.fn(() => jest.fn()), extent: jest.fn(() => [0, 1]), // Mock extent to return a default range forceSimulation: jest.fn().mockReturnValue({ @@ -70,25 +70,32 @@ import * as d3 from 'd3'; // Mock data (replace with your actual data structure) const mockProps = { math: { - "base-clusters": { count: { id1: 10, id2: 5 }, id: 0, x: 0, y: 0 }, + "base-clusters": { count: { id1: 10, id2: 5 }, id: 0, x: [0], y: [0] }, "group-clusters": [ - { id: 0, center: [0.5, 0.5] }, - { id: 1, center: [0.2, 0.8] }, + { id: 0, center: [0.5, 0.5], members: [0,1] }, + { id: 1, center: [0.2, 0.8], members: [0,1] }, ], - tids: ['tid1', 'tid2'], + tids: [0, 1], pca: { 'comment-projection': [ - [], - [] + [ + -2.7290480249930327, + -0.7919407161501923, + ], + [ + 1.4406572645339384, + 1.1672237459165768, + ] ] } }, comments: [ - { tid: 'tid1', text: 'Comment 1' }, - { tid: 'tid2', text: 'Comment 2' }, + { tid: 0, txt: 'Comment 1' }, + { tid: 1, txt: 'Comment 2' }, ], voteColors: { agree: 'green', disagree: 'red' }, - // Add other props as needed + badTids: [], + formatTid: (n) => n }; // Test case for rendering with basic data @@ -105,17 +112,3 @@ test('renders participants graph with basic data', () => { } }); -// Test case for clicking "Statements" button -test('clicking statements button shows comments', () => { - render(); - - const statementsButton = screen.getByRole('button', { name: 'Statements' }); - expect(statementsButton.textContent).toBe('Statements'); - - fireEvent.click(statementsButton); - - // Check for presence of comments (implementation might vary) - expect(screen.getByText('Comment 1')).toBeInTheDocument(); -}); - - diff --git a/client-report/src/index.js b/client-report/src/index.js index 6f7b0fb57..00e2c3423 100644 --- a/client-report/src/index.js +++ b/client-report/src/index.js @@ -4,7 +4,7 @@ import React from "react"; import { createRoot } from 'react-dom/client'; import './index.css'; -import App from "./components/app"; +import App from "./components/app.jsx"; // const store = configureStore(); From d71cc97039b61df6b3bfc7651ae7766d60e7df2f Mon Sep 17 00:00:00 2001 From: tevko Date: Mon, 16 Dec 2024 18:10:43 -0600 Subject: [PATCH 14/16] finish conversion from class to functional, underscore and jquery removed --- .../{RawDataExport.js => RawDataExport.jsx} | 0 client-report/src/components/app.jsx | 56 +++++---- .../components/{barChart.js => barChart.jsx} | 2 +- client-report/src/components/comment.js | 86 -------------- client-report/src/components/comment.jsx | 66 +++++++++++ client-report/src/components/graphAxes.js | 87 -------------- client-report/src/components/graphAxes.jsx | 31 +++++ .../components/lists/consensusNarrative.jsx | 1 + .../src/components/lists/participantGroup.jsx | 2 - .../lists/participantGroups.test.jsx | 2 +- .../components/{overview.js => overview.jsx} | 19 +-- .../participantsGraph/participantsGraph.jsx | 2 +- client-report/src/index.js | 2 - client-report/src/store/index.js | 25 ---- client-report/src/util/dataUtils.js | 51 +++++--- client-report/src/util/dataUtils.test.js | 101 ++++++++++++++++ client-report/src/util/graphUtil.js | 74 ++++++++---- client-report/src/util/graphUtil.test.js | 109 ++++++++++++++++++ client-report/src/util/net.js | 90 --------------- package-lock.json | 6 + 20 files changed, 437 insertions(+), 375 deletions(-) rename client-report/src/components/{RawDataExport.js => RawDataExport.jsx} (100%) rename client-report/src/components/{barChart.js => barChart.jsx} (97%) delete mode 100644 client-report/src/components/comment.js create mode 100644 client-report/src/components/comment.jsx delete mode 100644 client-report/src/components/graphAxes.js create mode 100644 client-report/src/components/graphAxes.jsx rename client-report/src/components/{overview.js => overview.jsx} (83%) delete mode 100644 client-report/src/store/index.js create mode 100644 client-report/src/util/dataUtils.test.js create mode 100644 client-report/src/util/graphUtil.test.js create mode 100644 package-lock.json diff --git a/client-report/src/components/RawDataExport.js b/client-report/src/components/RawDataExport.jsx similarity index 100% rename from client-report/src/components/RawDataExport.js rename to client-report/src/components/RawDataExport.jsx diff --git a/client-report/src/components/app.jsx b/client-report/src/components/app.jsx index 8e005844e..23094e88d 100644 --- a/client-report/src/components/app.jsx +++ b/client-report/src/components/app.jsx @@ -7,7 +7,7 @@ import URLs from "../util/url.js"; import DataUtils from "../util/dataUtils.js"; import Heading from "./framework/heading.jsx"; import Footer from "./framework/Footer.jsx"; -import Overview from "./overview.js"; +import Overview from "./overview.jsx"; import MajorityStrict from "./lists/majorityStrict.jsx"; import Uncertainty from "./lists/uncertainty.jsx"; import UncertaintyNarrative from "./lists/uncertaintyNarrative.jsx"; @@ -18,7 +18,7 @@ import Beeswarm from "./beeswarm/beeswarm.jsx"; import Controls from "./controls/controls.jsx"; import net from "../util/net.js"; import ConsensusNarrative from "./lists/consensusNarrative.jsx"; -import RawDataExport from "./RawDataExport.js"; +import RawDataExport from "./RawDataExport.jsx"; const pathname = window.location.pathname; // "/report/2arcefpshi" const report_id = pathname.split("/")[2]; @@ -50,7 +50,7 @@ const App = (props) => { disagree: globals.brandColors.disagree, pass: globals.brandColors.pass, }); - const [narrative, setNarrative] = useState({}); + const [narrative, setNarrative] = useState(null); const [errorText, setErrorText] = useState(null); const [extremity, setExtremity] = useState(null); const [uncertainty, setUncertainty] = useState(null); @@ -146,7 +146,7 @@ const App = (props) => { } const decodedChunk = decoder.decode(value, { stream: true }); - if (!decodedChunk.includes('POLIS-PING:')) setNarrative(n => Object.assign(n, JSON.parse(decodedChunk))); + if (!decodedChunk.includes('POLIS-PING:')) setNarrative(n => Object.assign((n || {}), JSON.parse(decodedChunk))); } } @@ -511,6 +511,8 @@ const App = (props) => { ); } + console.log(conversation, narrative) + return (
@@ -550,27 +552,31 @@ const App = (props) => { <>

Current Model: {model}

- - + {narrative ? ( + <> + + + + ) : "...Loading"} ) : ( <> diff --git a/client-report/src/components/barChart.js b/client-report/src/components/barChart.jsx similarity index 97% rename from client-report/src/components/barChart.js rename to client-report/src/components/barChart.jsx index 50d24b5cc..faa4a913b 100644 --- a/client-report/src/components/barChart.js +++ b/client-report/src/components/barChart.jsx @@ -2,7 +2,7 @@ import React from "react"; -const BarChart = ({comment, groupVotes, ptptCount/*, conversation*/}) => { +const BarChart = ({comment, groupVotes, ptptCount}) => { const rectStartX = 70; const barHeight = 15; diff --git a/client-report/src/components/comment.js b/client-report/src/components/comment.js deleted file mode 100644 index c1815e800..000000000 --- a/client-report/src/components/comment.js +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . - -import React from "react"; -import PropTypes from "prop-types"; -import Flex from "./flex"; -// import ParticipantHeader from "./participant-header"; -import * as globals from "./globals"; -import BarChart from "./barChart"; - -class Comment extends React.Component { - static propTypes = { - dispatch: PropTypes.func, - params: PropTypes.object, - acceptButton: PropTypes.bool, - rejectButton: PropTypes.bool, - acceptClickHandler: PropTypes.func, - rejectClickHandler: PropTypes.func, - } - getDate() { - const date = new Date(+this.props.comment.created); - return `${date.getMonth()+1} / ${date.getUTCDate()} / ${date.getFullYear()}` - } - getVoteBreakdown(/*comment*/) { - if (typeof this.props.comment.agree_count !== "undefined") { - return ({this.props.comment.agree_count} agreed, {this.props.comment.disagree_count} disagreed, {this.props.comment.pass_count} passed); - } - return ""; - } - - render() { - // const showAsAnon = !this.props.comment.social || this.props.comment.anon || this.props.comment.is_seed; - - const styles = Object.assign({}, globals.paragraph, {fontStyle: "italic"}); - - return ( - - - - {this.props.formatTid(this.props.comment.tid)} - - {this.props.comment.is_meta ? "Metadata: " : ''} - { this.props.comment.txt } - - - - - - - ); - } - } - - export default Comment; - - //

{this.props.comment.demographics.gender}

- //

{this.props.comment.demographics.age}

- - // { - // showAsAnon ? - // "Anonymous" : - // - // } diff --git a/client-report/src/components/comment.jsx b/client-report/src/components/comment.jsx new file mode 100644 index 000000000..27fe32356 --- /dev/null +++ b/client-report/src/components/comment.jsx @@ -0,0 +1,66 @@ +// // Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . + +import React from "react"; +import PropTypes from "prop-types"; +import Flex from "./flex"; +import * as globals from "./globals"; +import BarChart from "./barChart"; + +const Comment = ({ dispatch, params, acceptButton, rejectButton, acceptClickHandler, rejectClickHandler, comment, formatTid, conversation, ptptCount }) => { + const getDate = () => { + const date = new Date(+comment.created); + return `${date.getMonth() + 1} / ${date.getUTCDate()} / ${date.getFullYear()}`; + }; + + const getVoteBreakdown = () => { + if (typeof comment.agree_count !== "undefined") { + return ( + + ({comment.agree_count} agreed, {comment.disagree_count} disagreed, {comment.pass_count} passed) + + ); + } + return ""; + }; + + const styles = { ...globals.paragraph, fontStyle: "italic" }; + + return ( + + + + {formatTid(comment.tid)} - {comment.is_meta ? "Metadata: " : ""} + {comment.txt} + + + + + + + + ); +}; + +Comment.propTypes = { + dispatch: PropTypes.func, + params: PropTypes.object, + acceptButton: PropTypes.bool, + rejectButton: PropTypes.bool, + acceptClickHandler: PropTypes.func, + rejectClickHandler: PropTypes.func, + comment: PropTypes.object, + formatTid: PropTypes.func, + conversation: PropTypes.object, + ptptCount: PropTypes.number, +}; + +export default Comment; diff --git a/client-report/src/components/graphAxes.js b/client-report/src/components/graphAxes.js deleted file mode 100644 index 784e97879..000000000 --- a/client-report/src/components/graphAxes.js +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . - -import React from "react"; -import * as globals from "./globals"; - -const GraphAxes = ({yCenter, xCenter/*, report*/}) => { - return ( - - - - - ); -}; - -export default GraphAxes; -// -// {/* Bottom axis */} -// -// {report.label_x_neg ? -// {globals.axisLabels.leftArrow} -// {" "} -// {report.label_x_neg} -// : ""} -// {report.label_x_pos ? -// {report.label_x_pos} -// {" "} -// {globals.axisLabels.rightArrow} -// : ""} -// -// -// {/* Left axis */} -// -// {report.label_y_neg ? -// {globals.axisLabels.leftArrow} -// {" "} -// {report.label_y_neg} -// : ""} -// {report.label_y_pos ? -// {report.label_y_pos} -// {" "} -// {globals.axisLabels.rightArrow} -// : ""} -// diff --git a/client-report/src/components/graphAxes.jsx b/client-report/src/components/graphAxes.jsx new file mode 100644 index 000000000..02bf824dc --- /dev/null +++ b/client-report/src/components/graphAxes.jsx @@ -0,0 +1,31 @@ +// Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . + +import React from "react"; +import * as globals from "./globals"; + +const GraphAxes = ({yCenter, xCenter/*, report*/}) => { + return ( + + + + + ); +}; + +export default GraphAxes; diff --git a/client-report/src/components/lists/consensusNarrative.jsx b/client-report/src/components/lists/consensusNarrative.jsx index 489fe4ff8..ed3c2d41c 100644 --- a/client-report/src/components/lists/consensusNarrative.jsx +++ b/client-report/src/components/lists/consensusNarrative.jsx @@ -12,6 +12,7 @@ const ConsensusNarrative = ({ narrative, model }) => { + console.log(narrative?.group_informed_consensus) if (!narrative?.group_informed_consensus) { return
Loading Consensus...
; } diff --git a/client-report/src/components/lists/participantGroup.jsx b/client-report/src/components/lists/participantGroup.jsx index e0dfab54d..6527c75ca 100644 --- a/client-report/src/components/lists/participantGroup.jsx +++ b/client-report/src/components/lists/participantGroup.jsx @@ -22,8 +22,6 @@ const ParticipantGroup = ({ groupLabel = "Group " + globals.groupLabels[gid]; } - console.log(groupComments) - return (
. import React from "react"; -import _ from "lodash"; import * as globals from "./globals"; const computeVoteTotal = (users) => { let voteTotal = 0; - _.each(users, (count) => { - voteTotal += count; - }); + for (const count in users) { + voteTotal += users[count]; + } return voteTotal; }; -// const computeUniqueCommenters = (comments) => { - -// } - const Number = ({ number, label }) => (

{number.toLocaleString()}

@@ -81,11 +76,3 @@ const Overview = ({ conversation, ptptCount, ptptCountTotal, math, computedStats }; export default Overview; - -//

{conversation && conversation.participant_count ? "A total of "+ptptCount+" people participated. " : null}

- -// It was presented {conversation ? conversation.medium : "loading"} to an audience of {conversation ? conversation.audiences : "loading"}. -// The conversation was run for {conversation ? conversation.duration : "loading"}. -// {demographics ? demographics.foo : "loading"} were women - -// {conversation && conversation.description ? "The specific question was '"+conversation.description+"'. ": null} diff --git a/client-report/src/components/participantsGraph/participantsGraph.jsx b/client-report/src/components/participantsGraph/participantsGraph.jsx index 50f0fdd87..3e7d39da8 100644 --- a/client-report/src/components/participantsGraph/participantsGraph.jsx +++ b/client-report/src/components/participantsGraph/participantsGraph.jsx @@ -3,7 +3,7 @@ import React, { useState } from "react"; import * as globals from "../globals.js"; import graphUtil from "../../util/graphUtil.js"; -import Axes from "../graphAxes.js"; +import Axes from "../graphAxes.jsx"; import * as d3contour from "d3-contour"; import * as d3chromatic from "d3-scale-chromatic"; import Comments from "../commentsGraph/comments.jsx"; diff --git a/client-report/src/index.js b/client-report/src/index.js index 00e2c3423..79c6568a1 100644 --- a/client-report/src/index.js +++ b/client-report/src/index.js @@ -6,8 +6,6 @@ import { createRoot } from 'react-dom/client'; import './index.css'; import App from "./components/app.jsx"; -// const store = configureStore(); - class Root extends React.Component { render() { return ( diff --git a/client-report/src/store/index.js b/client-report/src/store/index.js deleted file mode 100644 index 90d873609..000000000 --- a/client-report/src/store/index.js +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . - -import { createStore, applyMiddleware, compose } from "redux"; -import thunk from "redux-thunk"; - -import rootReducer from "../reducers"; - -const middleware = [thunk]; - -let finalCreateStore; - -if (process.env.NODE_ENV === "production") { - finalCreateStore = applyMiddleware(...middleware)(createStore); -} else { - finalCreateStore = compose( - applyMiddleware(...middleware), - window.devToolsExtension ? window.devToolsExtension() : (f) => f - )(createStore); -} - -const configureStore = function (initialState) { - return finalCreateStore(rootReducer, initialState); -}; - -export default configureStore; diff --git a/client-report/src/util/dataUtils.js b/client-report/src/util/dataUtils.js index 8ab2bc3c9..23cb9eea0 100644 --- a/client-report/src/util/dataUtils.js +++ b/client-report/src/util/dataUtils.js @@ -1,29 +1,42 @@ // Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . -import _ from "lodash"; +const getVoteTotals = (math_main) => { + const x = {}; + const gv = math_main["group-votes"]; + if (gv) { + for (const gid in gv) { + if (gv.hasOwnProperty(gid)) { // Important: check own properties + const data = gv[gid]; + if (data && data.votes) { + for (const tid in data.votes) { + if (data.votes.hasOwnProperty(tid)) { // Important: check own properties + const counts = data.votes[tid]; + x[tid] = x[tid] || { agreed: 0, disagreed: 0, saw: 0 }; + x[tid].agreed += counts?.A || 0; + x[tid].disagreed += counts?.D || 0; + x[tid].saw += counts?.S || 0; + } + } + } + } + } + } -function getVoteTotals(math_main) { - var x = {}; - var gv = math_main["group-votes"]; - _.each(gv, function(data/*, gid*/) { - _.each(data.votes, function(counts, tid) { - var z = x[tid] = x[tid] || {agreed:0, disagreed:0, saw:0}; - z.agreed += counts.A; - z.disagreed += counts.D; - z.saw += counts.S; - }); - }); - _.each(x, function(z) { - z.pctAgreed = z.agreed / z.saw; - z.pctDisagreed = z.disagreed / z.saw; - z.pctVoted = (z.saw - z.disagreed - z.agreed) / z.saw; - }); + for (const tid in x) { + if (x.hasOwnProperty(tid)) { // Important: check own properties + const z = x[tid]; + z.pctAgreed = z.saw > 0 ? z.agreed / z.saw : 0; + z.pctDisagreed = z.saw > 0 ? z.disagreed / z.saw : 0; + z.pctVoted = z.saw > 0 ? (z.saw - z.disagreed - z.agreed) / z.saw : 0; + } + } return x; -} +}; const dataUtils = { - getVoteTotals: getVoteTotals, + getVoteTotals, }; + export default dataUtils; diff --git a/client-report/src/util/dataUtils.test.js b/client-report/src/util/dataUtils.test.js new file mode 100644 index 000000000..5f1b56072 --- /dev/null +++ b/client-report/src/util/dataUtils.test.js @@ -0,0 +1,101 @@ +import dataUtils from './dataUtils'; + +describe('dataUtils', () => { + describe('getVoteTotals', () => { + it('should return an empty object if math_main is empty or has no group-votes', () => { + expect(dataUtils.getVoteTotals({})).toEqual({}); + expect(dataUtils.getVoteTotals({ otherData: 'something' })).toEqual({}); + expect(dataUtils.getVoteTotals({ "group-votes": null })).toEqual({}); + expect(dataUtils.getVoteTotals({ "group-votes": undefined })).toEqual({}); + }); + + it('should calculate vote totals correctly for a single group', () => { + const math_main = { + "group-votes": { + 0: { + votes: { + 1: { A: 10, D: 5, S: 15 }, + 2: { A: 5, D: 10, S: 15 }, + }, + }, + }, + }; + const expected = { + 1: { agreed: 10, disagreed: 5, saw: 15, pctAgreed: 10/15, pctDisagreed: 5/15, pctVoted: 0/15 }, + 2: { agreed: 5, disagreed: 10, saw: 15, pctAgreed: 5/15, pctDisagreed: 10/15, pctVoted: 0/15 }, + }; + expect(dataUtils.getVoteTotals(math_main)).toEqual(expected); + }); + + it('should calculate vote totals correctly for multiple groups', () => { + const math_main = { + "group-votes": { + 0: { + votes: { + 1: { A: 10, D: 5, S: 15 }, + 2: { A: 5, D: 10, S: 15 }, + }, + }, + 1: { + votes: { + 1: { A: 2, D: 3, S: 5 }, + 3: { A: 7, D: 1, S: 8 }, + }, + }, + }, + }; + const expected = { + 1: { agreed: 12, disagreed: 8, saw: 20, pctAgreed: 12/20, pctDisagreed: 8/20, pctVoted: 0/20 }, + 2: { agreed: 5, disagreed: 10, saw: 15, pctAgreed: 5/15, pctDisagreed: 10/15, pctVoted: 0/15 }, + 3: { agreed: 7, disagreed: 1, saw: 8, pctAgreed: 7/8, pctDisagreed: 1/8, pctVoted: 0/8 }, + }; + expect(dataUtils.getVoteTotals(math_main)).toEqual(expected); + }); + + it('should handle missing A, D, or S counts', () => { + const math_main = { + "group-votes": { + 0: { + votes: { + 1: { A: 10, D: 5 }, // Missing S + 2: { S: 15 }, // Missing A and D + }, + }, + }, + }; + const expected = { + 1: { agreed: 10, disagreed: 5, saw: 0, pctAgreed: 0, pctDisagreed: 0, pctVoted: 0 }, + 2: { agreed: 0, disagreed: 0, saw: 15, pctAgreed: 0, pctDisagreed: 0, pctVoted: 1 }, + }; + expect(dataUtils.getVoteTotals(math_main)).toEqual(expected); + }); + + it('should handle empty votes object', () => { + const math_main = { + "group-votes": { + 0: { + votes: {}, + }, + }, + }; + const expected = {}; + expect(dataUtils.getVoteTotals(math_main)).toEqual(expected); + }); + + it('should handle saw = 0 to prevent division by zero', () => { + const math_main = { + "group-votes": { + 0: { + votes: { + 1: { A: 10, D: 5, S: 0 }, + }, + }, + }, + }; + const expected = { + 1: { agreed: 10, disagreed: 5, saw: 0, pctAgreed: 0, pctDisagreed: 0, pctVoted: 0 }, + }; + expect(dataUtils.getVoteTotals(math_main)).toEqual(expected); + }); + }); +}); \ No newline at end of file diff --git a/client-report/src/util/graphUtil.js b/client-report/src/util/graphUtil.js index 6851ba8eb..990e90ab5 100644 --- a/client-report/src/util/graphUtil.js +++ b/client-report/src/util/graphUtil.js @@ -2,7 +2,6 @@ import * as globals from "../components/globals"; import createHull from "hull.js"; -import _ from 'lodash' const graphUtil = (comments, math, badTids) => { @@ -82,24 +81,56 @@ const graphUtil = (comments, math, badTids) => { // let minClusterY = _.min(allYs); // let maxClusterY = _.max(allYs); - var greatestAbsPtptX = Math.abs(_.maxBy(baseClusters, (pt) => { return Math.abs(pt.x); }).x); - var greatestAbsPtptY = Math.abs(_.maxBy(baseClusters, (pt) => { return Math.abs(pt.y); }).y); + let greatestAbsPtptX = baseClusters.reduce((max, pt) => { + return Math.max(max, Math.abs(pt.x)); + }, 0); // Initialize max to 0 + + let greatestAbsPtptY = baseClusters.reduce((max, pt) => { + return Math.max(max, Math.abs(pt.y)); + }, 0); // Initialize max to 0 + // var greatestAbsCommentX = Math.abs(_.maxBy(commentsPoints, (pt) => { return Math.abs(pt.x); }).x); // var greatestAbsCommentY = Math.abs(_.maxBy(commentsPoints, (pt) => { return Math.abs(pt.y); }).y); - const xx = d3.scaleLinear().domain([-greatestAbsPtptX, greatestAbsPtptX]).range([border, globals.side - border]); - const yy = d3.scaleLinear().domain([-greatestAbsPtptY, greatestAbsPtptY]).range([border, globals.side - border]); + const xx = window.d3.scaleLinear().domain([-greatestAbsPtptX, greatestAbsPtptX]).range([border, globals.side - border]); + const yy = window.d3.scaleLinear().domain([-greatestAbsPtptY, greatestAbsPtptY]).range([border, globals.side - border]); const xCenter = globals.side / 2; const yCenter = globals.side / 2; - var maxCommentX = _.maxBy(commentsPoints, (pt) => { return pt.x; }).x; - var minCommentX = _.minBy(commentsPoints, (pt) => { return pt.x; }).x; - var maxCommentY = _.maxBy(commentsPoints, (pt) => { return pt.y; }).y; - var minCommentY = _.minBy(commentsPoints, (pt) => { return pt.y; }).y; + let maxCommentX = commentsPoints.length > 0 ? commentsPoints[0].x : undefined; // Handle empty array + for (let i = 1; i < commentsPoints.length; i++) { + if (commentsPoints[i].x > maxCommentX) { + maxCommentX = commentsPoints[i].x; + } + } + + // Find minCommentX + let minCommentX = commentsPoints.length > 0 ? commentsPoints[0].x : undefined; // Handle empty array + for (let i = 1; i < commentsPoints.length; i++) { + if (commentsPoints[i].x < minCommentX) { + minCommentX = commentsPoints[i].x; + } + } + + // Find maxCommentY + let maxCommentY = commentsPoints.length > 0 ? commentsPoints[0].y : undefined; // Handle empty array + for (let i = 1; i < commentsPoints.length; i++) { + if (commentsPoints[i].y > maxCommentY) { + maxCommentY = commentsPoints[i].y; + } + } + + // Find minCommentY + let minCommentY = commentsPoints.length > 0 ? commentsPoints[0].y : undefined; // Handle empty array + for (let i = 1; i < commentsPoints.length; i++) { + if (commentsPoints[i].y < minCommentY) { + minCommentY = commentsPoints[i].y; + } + } // xGreatestMapped = xCenter + xScale * maxCommentX // globals.side - border = xCenter + xScale * maxCommentX @@ -143,19 +174,22 @@ const graphUtil = (comments, math, badTids) => { const hulls = []; - _.each(baseClustersScaledAndGrouped, (group) => { - const pairs = group.map((g) => { /* create an array of arrays */ - return [g.x, g.y] - }) - const hull = createHull( - pairs, - 400 - ) + for (const group of Object.entries(baseClustersScaledAndGrouped)) { + // Destructure the group entry (key and value) + const [groupName, groupPoints] = group; + + // Create an array of coordinate pairs + const pairs = groupPoints.map((g) => [g.x, g.y]); + + // Calculate the convex hull + const hull = createHull(pairs, 400); + + // Push the result to hulls hulls.push({ - group, + group: groupName, hull, - }) - }) + }); + } return { xx, diff --git a/client-report/src/util/graphUtil.test.js b/client-report/src/util/graphUtil.test.js new file mode 100644 index 000000000..2549f33ad --- /dev/null +++ b/client-report/src/util/graphUtil.test.js @@ -0,0 +1,109 @@ +import graphUtil from "./graphUtil"; +import createHull from "hull.js"; +import * as d3contour from "d3-contour"; +import * as d3chromatic from "d3-scale-chromatic"; +import * as d3geo from "d3-geo"; + +Object.defineProperty(window, 'd3', { + writable: true, +}); + +global.window.d3 = { + scaleLinear: jest.fn(() => { + const mockScale = { // Create a mock scale object + domain: jest.fn(() => mockScale), // Return the mockScale itself + rangeRound: jest.fn(() => mockScale), // Return the mockScale itself + range: jest.fn(() => jest.fn()), // Add range for completeness + // Add other methods as needed (e.g., tickFormat) + }; + return mockScale; // Return the mock scale object + }), + geoPath: jest.fn(() => jest.fn()), + extent: jest.fn(() => [0, 1]), // Mock extent to return a default range + forceSimulation: jest.fn().mockReturnValue({ + force: jest.fn().mockReturnThis(), + stop: jest.fn().mockReturnThis(), + tick: jest.fn() + }), + forceX: jest.fn().mockReturnValue({ // Add a mock return for forceX + strength: jest.fn().mockReturnThis() // Add a mock for strength + }), + forceY: jest.fn(), + forceCollide: jest.fn(), + voronoi: jest.fn().mockReturnValue({ + extent: jest.fn().mockReturnThis(), + x: jest.fn().mockReturnThis(), + y: jest.fn().mockReturnThis(), + polygons: jest.fn().mockReturnValue([ + { + join: jest.fn(), + data: {} + }, + ]) + }) +} + +import * as d3 from 'd3'; + +jest.mock("hull.js"); // Mock createHull for isolation + +describe("graphUtil", () => { + it("should calculate commentsPoints with proper filtering", () => { + const mockComments = [ + { tid: 1, txt: "Comment 1" }, + { tid: 2, txt: "Comment 2" }, + { tid: 4, txt: "Comment 4" }, + ]; + const mockMath = { + pca: { "comment-projection": [[1], [2], [4]] }, + tids: [1, 2, 4], + "base-clusters": { + x: [10, 20, 30], + y: [40, 50, 60], + id: [100, 200, 300], + }, + "group-clusters": [], + }; + const mockBadTids = {}; // No badTids + + const result = graphUtil(mockComments, mockMath, mockBadTids); + + expect(result.commentsPoints.length).toBe(1); // Only 2 comments after filtering + expect(result.commentsPoints).toEqual([{"tid": 1, "txt": "Comment 1", "x": 1, "y": 2}]); + }); + + it("should calculate hulls for each group in baseClustersScaledAndGrouped", () => { + const mockCreateHull = jest.fn().mockReturnValue("Mock Hull"); + createHull.mockImplementation(mockCreateHull); // Mock createHull behavior + + const mockBaseClustersScaledAndGrouped = { + group1: [ + { x: 1, y: 2 }, + { x: 3, y: 4 }, + ], + group2: [{ x: 5, y: 6 }], + }; + + const mockComments = [ + { tid: 1, txt: "Comment 1" }, + { tid: 2, txt: "Comment 2" }, + { tid: 4, txt: "Comment 4" }, + ]; + + const mockMath = { + pca: { "comment-projection": [[1], [2], [4]] }, + tids: [1, 2, 4], + "base-clusters": { + x: [10, 20, 30], + y: [40, 50, 60], + id: [100, 200, 300], + }, + "group-clusters": [], + }; + const mockBadTids = {}; // No badTids + + const result = graphUtil(mockComments, mockMath, mockBadTids); + + expect(mockCreateHull).toHaveBeenCalledTimes(1); + }); +}); \ No newline at end of file diff --git a/client-report/src/util/net.js b/client-report/src/util/net.js index 069d5f9b1..fe7e27635 100644 --- a/client-report/src/util/net.js +++ b/client-report/src/util/net.js @@ -1,95 +1,5 @@ // // Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . -// import URLs from "./url"; - -// var urlPrefix = URLs.urlPrefix; - -// function polisAjax(api, data, type) { -// if (typeof api !== "string") { -// throw "api param should be a string"; -// } - -// if (api && api.length && api[0] === '/') { -// api = api.slice(1); -// } - -// var url = urlPrefix + api; - -// // Add the auth token if needed. -// // if (_.contains(authenticatedCalls, api)) { -// // var token = tokenStore.get(); -// // if (!token) { -// // needAuthCallbacks.fire(); -// // console.error("auth needed"); -// // return $.Deferred().reject("auth needed"); -// // } -// // //data = $.extend({ token: token}, data); // moving to cookies -// // } - -// var promise; -// var config = { -// url: url, -// contentType: "application/json; charset=utf-8", -// headers: { -// //"Cache-Control": "no-cache" // no-cache -// "Cache-Control": "max-age=0" -// }, -// xhrFields: { -// withCredentials: true -// }, -// // crossDomain: true, -// dataType: "json" -// }; -// if ("GET" === type) { -// promise = $.ajax($.extend(config, { -// type: "GET", -// data: data -// })); -// } else if ("POST" === type) { -// promise = $.ajax($.extend(config, { -// type: "POST", -// data: JSON.stringify(data) -// })); -// } - -// promise.fail( function(jqXHR/*, message, errorType*/) { - -// // sendEvent("Error", api, jqXHR.status); - -// // logger.error("SEND ERROR"); -// console.dir(arguments); -// if (403 === jqXHR.status) { -// // eb.trigger(eb.authNeeded); -// } -// //logger.dir(data); -// //logger.dir(message); -// //logger.dir(errorType); -// }); -// return promise; -// } - -// function polisPost(api, data) { -// return polisAjax(api, data, "POST"); -// } - -// function polisGet(api, data) { -// return polisAjax(api, data, "GET"); -// } - -// const PolisNet = { -// polisAjax: polisAjax, -// polisPost: polisPost, -// polisGet: polisGet, -// }; -// export default PolisNet; - -// Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or -// modify it under the terms of the GNU Affero General Public License, version 3, as published by the -// Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT -// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -// See the GNU Affero General Public License for more details. You should have received a copy of the -// GNU Affero General Public License along with this program. If not, see . - import URLs from "./url"; const urlPrefix = URLs.urlPrefix; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..e3854eb52 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "polis", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} From 700b0554bd5677cf1b8fb5d93c2eb30e915d2a71 Mon Sep 17 00:00:00 2001 From: tevko Date: Mon, 16 Dec 2024 19:42:47 -0600 Subject: [PATCH 15/16] fix setstate bug --- client-report/src/components/app.jsx | 69 +++++++++++-------- .../components/lists/consensusNarrative.jsx | 8 +-- .../components/lists/uncertaintyNarrative.jsx | 6 +- .../src/components/narrative/index.jsx | 2 - 4 files changed, 48 insertions(+), 37 deletions(-) diff --git a/client-report/src/components/app.jsx b/client-report/src/components/app.jsx index 23094e88d..675e74446 100644 --- a/client-report/src/components/app.jsx +++ b/client-report/src/components/app.jsx @@ -67,6 +67,8 @@ const App = (props) => { const [computedStats, setComputedStats] = useState(null); const [nothingToShow, setNothingToShow] = useState(true); const [hasError, setError] = useState(false); + const [parsedNarrativeUncertainty, setParsedNarrativeUncertainty] = useState(null); + const [parsedNarrativeConsensus, setParsedNarrativeConsensus] = useState(null); let corMatRetries; @@ -84,6 +86,15 @@ const App = (props) => { } }, [window.location?.pathname]); + useEffect(() => { + if (narrative?.group_informed_consensus) { + setParsedNarrativeConsensus(narrative.group_informed_consensus); + } + if (narrative?.uncertainty) { + setParsedNarrativeUncertainty(narrative.uncertainty); + } + }, [narrative?.uncertainty, narrative?.group_informed_consensus, JSON.stringify(narrative)]) + const getMath = async (conversation_id) => { return net .polisGet("/api/v3/math/pca2", { @@ -146,7 +157,11 @@ const App = (props) => { } const decodedChunk = decoder.decode(value, { stream: true }); - if (!decodedChunk.includes('POLIS-PING:')) setNarrative(n => Object.assign((n || {}), JSON.parse(decodedChunk))); + if (!decodedChunk.includes('POLIS-PING:')) { + const o = narrative || {}; + const c = JSON.parse(decodedChunk); + setNarrative({...o, ...c}); + } } } @@ -511,8 +526,6 @@ const App = (props) => { ); } - console.log(conversation, narrative) - return (
@@ -552,31 +565,31 @@ const App = (props) => { <>

Current Model: {model}

- {narrative ? ( - <> - - - - ) : "...Loading"} + {parsedNarrativeConsensus ? ( + + ) : "...Loading Consensus \n"} + {parsedNarrativeUncertainty ? ( + + ) : "...Loading Uncertainty \n"} ) : ( <> diff --git a/client-report/src/components/lists/consensusNarrative.jsx b/client-report/src/components/lists/consensusNarrative.jsx index ed3c2d41c..ccc4e34ca 100644 --- a/client-report/src/components/lists/consensusNarrative.jsx +++ b/client-report/src/components/lists/consensusNarrative.jsx @@ -12,11 +12,11 @@ const ConsensusNarrative = ({ narrative, model }) => { - console.log(narrative?.group_informed_consensus) - if (!narrative?.group_informed_consensus) { + + if (!narrative) { return
Loading Consensus...
; } - const txt = model === "claude" ? narrative.group_informed_consensus.responseClaude.content[0].text : narrative.group_informed_consensus.responseGemini; + const txt = model === "claude" ? narrative.responseClaude.content[0].text : narrative.responseGemini; const narrativeJSON = model === "claude" ? JSON.parse(`{${txt}`) : JSON.parse(txt); @@ -40,7 +40,7 @@ const ConsensusNarrative = ({

This narrative summary may contain hallucinations. Check each clause.

- +
{ - if (!conversation || !narrative || !narrative?.uncertainty?.responseClaude || !narrative?.uncertainty?.responseGemini) { + if (!conversation || !narrative || !narrative?.responseClaude || !narrative?.responseGemini) { return
Loading Uncertainty...
; } - const txt = model === "claude" ? narrative?.uncertainty.responseClaude.content[0].text : narrative?.uncertainty.responseGemini; + const txt = model === "claude" ? narrative?.responseClaude.content[0].text : narrative?.responseGemini; const narrativeJSON = model === "claude" ? JSON.parse(`{${txt}`) : JSON.parse(txt); @@ -44,7 +44,7 @@ const UncertaintyNarrative = ({

This narrative summary may contain hallucinations. Check each clause.

- +
{ if (!sectionData) return null; - console.log("narrativeData", sectionData); - const txt = model === "claude" ? sectionData.responseClaude.content[0].text : sectionData.responseGemini; const respData = model === "claude" ? JSON.parse(`{${txt}`) : JSON.parse(txt); From e0ad2004afc4cc652c266249fc59e426784999a0 Mon Sep 17 00:00:00 2001 From: tevko Date: Mon, 16 Dec 2024 19:45:01 -0600 Subject: [PATCH 16/16] fix tests --- client-report/src/components/lists/consensusNarrative.test.jsx | 2 -- .../src/components/lists/uncertaintyNarrative.test.jsx | 2 -- 2 files changed, 4 deletions(-) diff --git a/client-report/src/components/lists/consensusNarrative.test.jsx b/client-report/src/components/lists/consensusNarrative.test.jsx index 02778599d..d5db918c7 100644 --- a/client-report/src/components/lists/consensusNarrative.test.jsx +++ b/client-report/src/components/lists/consensusNarrative.test.jsx @@ -24,12 +24,10 @@ jest.mock('./commentList.jsx', () => { describe('ConsensusNarrative Component', () => { const mockNarrativeData = { - group_informed_consensus: { responseClaude: { content: [{ text: '"paragraphs":[{"sentences":[{"clauses":[{"citations":["tid1","tid2"]}]}]}]}' }], }, responseGemini: '{"paragraphs":[{"sentences":[{"clauses":[{"citations":["tid2","tid3","tid1"]}]}]}]}', - }, }; diff --git a/client-report/src/components/lists/uncertaintyNarrative.test.jsx b/client-report/src/components/lists/uncertaintyNarrative.test.jsx index 7c49e0a47..3921a5b83 100644 --- a/client-report/src/components/lists/uncertaintyNarrative.test.jsx +++ b/client-report/src/components/lists/uncertaintyNarrative.test.jsx @@ -15,10 +15,8 @@ describe('UncertaintyNarrative Component', () => { math: {}, voteColors: {}, narrative: { - uncertainty: { responseClaude: { content: [{ text: '"paragraphs":[{"sentences":[{"clauses":[{"citations":["T1","T2"]}]}]}]}' }] }, responseGemini: '{"paragraphs":[{"sentences":[{"clauses":[{"citations":["T3"]}]}]}]}' - } }, model: 'claude' };