From 956efd46e0b8bed2f665c8dcc53e52928f52a6dc Mon Sep 17 00:00:00 2001 From: Lim Ke En Date: Wed, 4 Dec 2024 12:21:41 +0800 Subject: [PATCH 01/10] Feature: New Voting System --- .../src/components/myvotes/CommunityCard.tsx | 113 ++++++++ .../components/vote/CommunityCategories.tsx | 92 +++++++ .../src/components/vote/OldVoteCategories.tsx | 240 +++++++++++++++++ .../src/components/vote/VoteCategories.tsx | 71 ++---- .../src/components/vote/VoteNoteChart.tsx | 17 ++ checkers-app/src/components/vote/VoteTags.tsx | 22 +- .../src/components/vote/VotingSystem.tsx | 241 ++++++++++++++++++ checkers-app/src/components/vote/index.tsx | 36 ++- checkers-app/src/services/api.ts | 4 +- .../src/definitions/api/handlers/getVote.ts | 3 +- .../api/handlers/patchVoteRequest.ts | 1 + 11 files changed, 764 insertions(+), 76 deletions(-) create mode 100644 checkers-app/src/components/myvotes/CommunityCard.tsx create mode 100644 checkers-app/src/components/vote/CommunityCategories.tsx create mode 100644 checkers-app/src/components/vote/OldVoteCategories.tsx create mode 100644 checkers-app/src/components/vote/VoteNoteChart.tsx create mode 100644 checkers-app/src/components/vote/VotingSystem.tsx diff --git a/checkers-app/src/components/myvotes/CommunityCard.tsx b/checkers-app/src/components/myvotes/CommunityCard.tsx new file mode 100644 index 00000000..6748aad2 --- /dev/null +++ b/checkers-app/src/components/myvotes/CommunityCard.tsx @@ -0,0 +1,113 @@ +import React, {useState} from "react"; +import {Card, CardBody, Typography, Button} from "@material-tailwind/react"; +import { UserIcon } from "@heroicons/react/24/solid"; + +interface PropType { + en: string; + cn: string; + links: string[]; + downvoted: boolean; +} + +// Helper function to detect URLs and split the text +const splitTextByUrls = (text: string) => { + // This regex will match URLs + const urlRegex = /(https?:\/\/[^\s]+)/g; + let match; + let lastIndex = 0; + const parts = []; + + // Find al matches and their indices + while ((match = urlRegex.exec(text)) !== null) { + const url = match[0]; + const index = match.index; + + // Push text before URL + if (index > lastIndex) { + parts.push({ text: text.substring(lastIndex, index), isUrl: false}); + } + + // Push URL + parts.push({ text: url, isUrl: true}); + + // Update lastIndex to end of current URL + lastIndex = index + url.length; +} + + // Push remaining text after last URL + if (lastIndex < text.length) { + parts.push({ text: text.substring(lastIndex), isUrl: false}); + } + + return parts; + } + +export default function CommunityCard(prop: PropType){ + const [isExpanded, setIsExpanded] = useState(false); + const lengthBeforeTruncation = 300; + const { en, cn, links, downvoted } = prop; + + const toggleExpansion = () => { + setIsExpanded(!isExpanded); + }; + + let displayText = en ?? ""; + + const shouldTruncate = displayText.length > lengthBeforeTruncation; + const textToShow = + isExpanded || !shouldTruncate + ? displayText + : displayText.slice(0, lengthBeforeTruncation) + "..."; + + // Split text by URLs + const textParts = splitTextByUrls(textToShow) + + return ( + + + + +

Community Note:

+
+ + + {textParts.map((part, index) => { + // Split the text part by new lines + const lines = part.text.split("\n"); + return ( + + {lines.map((line, lineIndex) => ( + + {part.isUrl ? ( + + {line} + + ) : ( + {line} + )} + {lineIndex < lines.length - 1 &&
} +
+ ))} +
+ ); + })} +
+ {shouldTruncate && ( + + )} +
+
+ ) +} \ No newline at end of file diff --git a/checkers-app/src/components/vote/CommunityCategories.tsx b/checkers-app/src/components/vote/CommunityCategories.tsx new file mode 100644 index 00000000..23f112d2 --- /dev/null +++ b/checkers-app/src/components/vote/CommunityCategories.tsx @@ -0,0 +1,92 @@ +import { useState } from "react"; +import { TrophyIcon } from "@heroicons/react/20/solid"; +import { CheckBadgeIcon } from "@heroicons/react/24/solid"; +import { XCircleIcon } from "@heroicons/react/24/outline"; +import { Button } from "@material-tailwind/react" +import { ForwardIcon } from "@heroicons/react/24/solid"; + +interface PropType { + messageId: string | null; + voteRequestId: string | null; + currentCommunityCategory: string | null; + onNextStep: (value: number) => void; + onSelectCommunityCategory: (communityCategory: string | null) => void; +} + +const CATEGORIES = [ + { + name: "great", + icon: , + display: "Great", + description: "Good response, can't do any better", + }, + { + name: "acceptable", + icon: , + display: "Acceptable", + description: "Acceptable response, but can be improved" + }, + { + name: "unacceptable", + icon: , + display: "Unacceptable", + description: "Unacceptable response" + } +] + + +export default function CommunityCategories(Prop: PropType) { + const currentCategory = Prop.currentCommunityCategory + const messageId = Prop.messageId; + const voteRequestId = Prop.voteRequestId; + const [selectedCategory, setSelectedCategory] = useState(currentCategory); + const [communityCategory, setCommunityCategory] = useState(currentCategory); + + const handleCommunityCategoryChange = (category: string) => { + switch(category) { + default: + setCommunityCategory(category); + Prop.onSelectCommunityCategory(category); + break; + } + } + + const handleSelection = (categoryName: string) => { + setSelectedCategory(categoryName); + handleCommunityCategoryChange(categoryName); + } + + const handleNextStep = (value: number) => { + Prop.onNextStep(value) + } + + return ( +
+ {CATEGORIES.map((cat, index) => ( + <> + + + ))} + + {communityCategory ? ( + + ) : null} +
+ ) +} diff --git a/checkers-app/src/components/vote/OldVoteCategories.tsx b/checkers-app/src/components/vote/OldVoteCategories.tsx new file mode 100644 index 00000000..211ba6c8 --- /dev/null +++ b/checkers-app/src/components/vote/OldVoteCategories.tsx @@ -0,0 +1,240 @@ +import { XCircleIcon } from "@heroicons/react/24/solid"; +import { ShieldExclamationIcon } from "@heroicons/react/24/solid"; +import { QuestionMarkCircleIcon } from "@heroicons/react/20/solid"; +import { NewspaperIcon } from "@heroicons/react/20/solid"; +import { FaceSmileIcon } from "@heroicons/react/20/solid"; +import { PaperAirplaneIcon } from "@heroicons/react/20/solid"; +import { MegaphoneIcon } from "@heroicons/react/24/solid"; +import { EllipsisHorizontalCircleIcon } from "@heroicons/react/24/solid"; +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { Button } from "@material-tailwind/react"; +import { patchVote } from "../../services/api"; +import { useUser } from "../../providers/UserContext"; +import { TooltipWithHelperIcon } from "../common/ToolTip"; +import { Typography } from "@material-tailwind/react"; +import VoteTags from "./VoteTags"; +import InfoOptions from "./InfoOptions"; +import NVCOptions from "./NvcOptions"; + +interface PropType { + messageId: string | null; + voteRequestId: string | null; + currentCategory: string | null; + currentTruthScore: number | null; + currentTags: string[] | null; + numberPointScale: number; +} + +function getSelectedCategory(primaryCategory: string | null) { + switch (primaryCategory) { + case "irrelevant": + return "nvc"; + case "legitimate": + return "nvc"; + default: + return primaryCategory; + } +} + +const CATEGORIES = [ + { + name: "scam", + icon: , + display: "Scam", + description: "Intended to obtain money/personal information via deception", + }, + { + name: "illicit", + icon: , + display: "Illicit", + description: + "Other potential illicit activity, e.g. moneylending/prostitution", + }, + { + name: "info", + icon: , + display: "News/Info/Opinion", + description: + "Content intended to inform/convince/mislead a broad base of people", + }, + { + name: "satire", + icon: , + display: "Satire", + description: "Content clearly satirical in nature", + }, + { + name: "spam", + icon: , + display: "Marketing/Spam", + description: + "Content intended to (i) promote or publicise a non-malicious product, service or event or (ii) convince recipient to spread non-malicious messages to others", + }, + { + name: "nvc", + icon: , + display: "No Verifiable Content", + description: + "Content that isn't capable of being checked using publicly-available information due to its nature", + }, + { + name: "unsure", + icon: , + display: "Unsure", + description: + "Either (i) Needs more information from sender to assess, or (ii) a search of publicly available information yields no useful results", + }, + { + name: "pass", + icon: , + display: "Pass", + description: "Skip this message if you're really unable to assess it", + }, +]; + +export default function OldVoteCategories(Prop: PropType) { + const navigate = useNavigate(); + const { incrementSessionVotedCount } = useUser(); + + const currentCategory = Prop.currentCategory; + const currentTruthScore = Prop.currentTruthScore; + const currentTags = Prop.currentTags ?? []; + const messageId = Prop.messageId; + const voteRequestId = Prop.voteRequestId; + const [selectedCategory, setSelectedCategory] = useState( + getSelectedCategory(currentCategory) + ); + const [voteCategory, setVoteCategory] = useState( + currentCategory + ); + //take global values from user context + const [truthScore, setTruthScore] = useState( + currentTruthScore + ); + const [tags, setTags] = useState(currentTags); + + const handleTruthScoreChange = ( + event: React.ChangeEvent + ) => { + setTruthScore(Number(event.target.value)); + }; + + const handleL2VoteChange = (event: React.ChangeEvent) => { + handleVoteCategoryChange(event.target.value); + }; + + const handleVoteCategoryChange = (category: string) => { + switch (category) { + default: + setVoteCategory(category); + break; + } + }; + + const handleSelection = (categoryName: string) => { + setSelectedCategory(categoryName); + + switch (categoryName) { + case "nvc": + setVoteCategory("nvc"); + break; + default: + handleVoteCategoryChange(categoryName); + break; + } + }; + + //function to update vote request in firebase + const handleSubmitVote = ( + category: string, + truthScore: number | null, + tags: string[] | null, + comCategory: string | null, + ) => { + if (category === "nvc") { + return; + } + if (messageId && voteRequestId) { + //call api to update vote + patchVote( + messageId, + voteRequestId, + category, + comCategory, + category === "info" ? truthScore : null, + tags + ) + .then(() => { + incrementSessionVotedCount(); + navigate("/votes"); + }) + .catch((error) => { + console.error("Error updating vote: ", error); + }); + } + }; + + const onSelectTagOption = (tags: string[]) => { + setTags(tags); + console.log(tags); + }; + + const handleDropdownToggle = (isOpen: boolean) => {} + + return ( +
+ + Select category: + + {CATEGORIES.map((cat, index) => ( + <> + + {/* Conditionally render InfoOptions right after the "info" button if it has been selected */} + {selectedCategory === "info" && cat.name === "info" && ( + + )} + {selectedCategory === "nvc" && cat.name === "nvc" && ( + + )} + + ))} + + + + {voteCategory ? ( +
+ +
+ ) : null} +
+ ); +} \ No newline at end of file diff --git a/checkers-app/src/components/vote/VoteCategories.tsx b/checkers-app/src/components/vote/VoteCategories.tsx index 10888f5e..214d3f0f 100644 --- a/checkers-app/src/components/vote/VoteCategories.tsx +++ b/checkers-app/src/components/vote/VoteCategories.tsx @@ -6,14 +6,12 @@ import { FaceSmileIcon } from "@heroicons/react/20/solid"; import { PaperAirplaneIcon } from "@heroicons/react/20/solid"; import { MegaphoneIcon } from "@heroicons/react/24/solid"; import { EllipsisHorizontalCircleIcon } from "@heroicons/react/24/solid"; +import { ForwardIcon } from "@heroicons/react/24/solid"; import { useState } from "react"; import { useNavigate } from "react-router-dom"; import { Button } from "@material-tailwind/react"; -import { patchVote } from "../../services/api"; import { useUser } from "../../providers/UserContext"; import { TooltipWithHelperIcon } from "../common/ToolTip"; -import { Typography } from "@material-tailwind/react"; -import VoteTags from "./VoteTags"; import InfoOptions from "./InfoOptions"; import NVCOptions from "./NvcOptions"; @@ -24,6 +22,9 @@ interface PropType { currentTruthScore: number | null; currentTags: string[] | null; numberPointScale: number; + onNextStep: (value: number) => void; + onVoteCategorySelection: (value:string) => void; + onTruthScoreChange: (value: number|null) => void; } function getSelectedCategory(primaryCategory: string | null) { @@ -94,8 +95,6 @@ const CATEGORIES = [ ]; export default function VoteCategories(Prop: PropType) { - const navigate = useNavigate(); - const { incrementSessionVotedCount } = useUser(); const currentCategory = Prop.currentCategory; const currentTruthScore = Prop.currentTruthScore; @@ -112,12 +111,12 @@ export default function VoteCategories(Prop: PropType) { const [truthScore, setTruthScore] = useState( currentTruthScore ); - const [tags, setTags] = useState(currentTags); const handleTruthScoreChange = ( event: React.ChangeEvent ) => { setTruthScore(Number(event.target.value)); + Prop.onTruthScoreChange(Number(event.target.value)) }; const handleL2VoteChange = (event: React.ChangeEvent) => { @@ -128,6 +127,7 @@ export default function VoteCategories(Prop: PropType) { switch (category) { default: setVoteCategory(category); + Prop.onVoteCategorySelection(category) break; } }; @@ -138,6 +138,7 @@ export default function VoteCategories(Prop: PropType) { switch (categoryName) { case "nvc": setVoteCategory("nvc"); + Prop.onVoteCategorySelection(categoryName) break; default: handleVoteCategoryChange(categoryName); @@ -145,44 +146,12 @@ export default function VoteCategories(Prop: PropType) { } }; - //function to update vote request in firebase - const handleSubmitVote = ( - category: string, - truthScore: number | null, - tags: string[] | null - ) => { - if (category === "nvc") { - return; - } - if (messageId && voteRequestId) { - //call api to update vote - patchVote( - messageId, - voteRequestId, - category, - category === "info" ? truthScore : null, - tags - ) - .then(() => { - incrementSessionVotedCount(); - navigate("/votes"); - }) - .catch((error) => { - console.error("Error updating vote: ", error); - }); - } - }; - - const onSelectTagOption = (tags: string[]) => { - setTags(tags); - console.log(tags); - }; + const handleNextStep = (value: number) => { + Prop.onNextStep(value) + } return (
- - Select category: - {CATEGORIES.map((cat, index) => ( <> -
- ) : null} + + ): null} + ); } diff --git a/checkers-app/src/components/vote/VoteNoteChart.tsx b/checkers-app/src/components/vote/VoteNoteChart.tsx new file mode 100644 index 00000000..9d15c9b5 --- /dev/null +++ b/checkers-app/src/components/vote/VoteNoteChart.tsx @@ -0,0 +1,17 @@ +import { PieChart, Pie, ResponsiveContainer, Cell, Legend } from "recharts"; +import { AssessedInfo } from "../../types"; + +interface VotingNoteChartProps { + assessedInfo: AssessedInfo | null; +} + +export default function VotingNoteChart(Props: VotingNoteChartProps) { + const assessedInfo = Props.assessedInfo + console.log(assessedInfo) + + return ( +
+ +
+ ) +} \ No newline at end of file diff --git a/checkers-app/src/components/vote/VoteTags.tsx b/checkers-app/src/components/vote/VoteTags.tsx index ebf61fa7..dcf4ea5e 100644 --- a/checkers-app/src/components/vote/VoteTags.tsx +++ b/checkers-app/src/components/vote/VoteTags.tsx @@ -1,7 +1,5 @@ -import { Typography } from "@material-tailwind/react"; import { MultiValue } from "react-select"; import Select from "react-select"; -import { TooltipWithHelperIcon } from "../common/ToolTip"; const options = [ { value: "generated", label: "🤖 AI Generated" }, @@ -11,9 +9,10 @@ const options = [ interface VoteTagsProps { tags: string[]; onSelectTag: (tag: string[]) => void; + onDropdownToggle: (isOpen:boolean) => void; // Pass open/close state to parent } -const VoteTags: React.FC = ({ tags, onSelectTag }) => { +const VoteTags: React.FC = ({ tags, onSelectTag, onDropdownToggle}) => { const selectedOptions = tags .map((tag) => options.find((option) => option.value === tag)) .filter( @@ -30,19 +29,6 @@ const VoteTags: React.FC = ({ tags, onSelectTag }) => { return (
-
- - Select tags: - - -
-