diff --git a/.gitignore b/.gitignore index b45fcb9..d5a15e0 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,10 @@ yarn-error.log* next-env.d.ts # vscode -.vscode \ No newline at end of file +.vscode + +# idea +.idea + +# pnpm-lock +pnpm-lock.yaml diff --git a/README.md b/README.md index 908b025..ff60154 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ## Team Member | Name | Roles | -|-------------|-------------------------------------| +| ----------- | ----------------------------------- | | Aaron Lee | Technical Officer/Client Liaison | | Emily Zhang | Project Manager/Front-end Developer | | Yilin Lyu | Scrum Mater/Front-end Developer | @@ -27,6 +27,10 @@ - zustand - jest - husky +- icons + - [lucide](https://lucide.dev/icons/) + - [mdi](https://pictogrammers.com/library/mdi/) + - [iconfont](https://www.iconfont.cn/) # Environment and Tools diff --git a/app/dashboard/components/Information-icon-text.tsx b/app/dashboard/components/Information-icon-text.tsx new file mode 100644 index 0000000..f9f25ef --- /dev/null +++ b/app/dashboard/components/Information-icon-text.tsx @@ -0,0 +1,58 @@ +import { useState } from "react"; +import { + Tooltip, + TooltipTrigger, + TooltipContent, + TooltipProvider, +} from "@/components/ui/tooltip"; +import { FaInfoCircle } from "react-icons/fa"; +import Link from "next/link"; + +interface TooltipWithIconProps { + title: string; + linkText: string; + linkHref: string; + description: string; +} + +const TooltipWithIcon: React.FC = ({ + title, + linkText, + linkHref, + description, +}) => { + return ( + + + + + {/* Information Icon */} + + + + + +
{title}
+
+ {description}{" "} + + {linkText} + + . +
+
+
+
+ ); +}; + +export default TooltipWithIcon; diff --git a/app/dashboard/components/assertion-table.tsx b/app/dashboard/components/assertion-table.tsx new file mode 100644 index 0000000..b031ee6 --- /dev/null +++ b/app/dashboard/components/assertion-table.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import { Avatar } from "@/components/ui/avatar"; + +interface Assertion { + index: number; + content: string; + type: string; + winner: number; // 候选人的 ID + name: string; // 候选人名字 +} + +interface AssertionTableProps { + assertions: Assertion[]; +} + +const AssertionTable: React.FC = ({ assertions }) => { + return ( +
+ + + + + + + + + + {assertions.map((assertion) => ( + + + + + + ))} + +
IndexContentType
+ {assertion.index} + +
+ + {assertion.content} +
+
+ {assertion.type} +
+
+ ); +}; + +export default AssertionTable; diff --git a/app/dashboard/components/assertions-details-modal.tsx b/app/dashboard/components/assertions-details-modal.tsx new file mode 100644 index 0000000..ad1c690 --- /dev/null +++ b/app/dashboard/components/assertions-details-modal.tsx @@ -0,0 +1,130 @@ +import React, { useEffect, useState } from "react"; +import { Avatar } from "@/components/ui/avatar"; +import { FaInfoCircle } from "react-icons/fa"; +import Link from "next/link"; +import TooltipWithIcon from "@/app/dashboard/components/Information-icon-text"; + +// 更新 Assertion 接口,添加 candidateId 字段 +interface Assertion { + index: number; + winner: number; + content: string; + type: string; + difficulty: number; + margin: number; +} + +interface AssertionsDetailsModalProps { + isOpen: boolean; + onClose: () => void; + assertions: Assertion[]; + maxDifficulty: number; + minMargin: number; +} + +const AssertionsDetailsModal: React.FC = ({ + isOpen, + onClose, + assertions, + maxDifficulty, + minMargin, +}) => { + const [isTooltipVisible, setTooltipVisible] = useState(false); + useEffect(() => { + if (isOpen) { + document.body.style.overflow = "hidden"; + } else { + document.body.style.overflow = "auto"; + } + }, [isOpen]); + + if (!isOpen) return null; + return ( +
+
+ {/* 关闭按钮 */} + + +

+ Assertions Details + +

+
+

+ Maximum Difficulty:{" "} + {maxDifficulty}{" "} + Minimum Margin: {minMargin} +

+
+ +
+ + + + + + + + + + + + {assertions.map((assertion) => ( + + + + + + + + ))} + +
+ Index + + Content + + Type + + Difficulty + + Margin +
{assertion.index} +
+ {/* 使用 Avatar 组件,传入 candidateId */} + + {assertion.content} +
+
{assertion.type}{assertion.difficulty}{assertion.margin}
+
+
+
+ ); +}; + +export default AssertionsDetailsModal; diff --git a/app/dashboard/components/avatar-assign-color.tsx b/app/dashboard/components/avatar-assign-color.tsx new file mode 100644 index 0000000..0be0f08 --- /dev/null +++ b/app/dashboard/components/avatar-assign-color.tsx @@ -0,0 +1,56 @@ +//avatar-assign-color.tsx +import React, { useEffect, useMemo, useState } from "react"; +import useMultiWinnerDataStore from "@/store/multi-winner-data"; +import { AvatarColor } from "@/utils/avatar-color"; + +interface AvatarProps { + onComplete: () => void; +} + +const AvatarAssignColor: React.FC = ({ onComplete }) => { + const { candidateList, setCandidateList } = useMultiWinnerDataStore(); + const avatarColor = useMemo(() => new AvatarColor(), []); + const [hasCompleted, setHasCompleted] = useState(false); + + useEffect(() => { + if (hasCompleted) return; + + console.log("candidateList:", candidateList); + + if (candidateList.length === 0) { + onComplete(); + setHasCompleted(true); + return; + } + + let hasUpdated = false; + + const updatedCandidates = candidateList.map((candidate, index) => { + if (!candidate.color) { + hasUpdated = true; + const color = avatarColor.getColor(index); + return { ...candidate, color }; + } + return candidate; + }); + + if (hasUpdated) { + setCandidateList(updatedCandidates); + } + + onComplete(); + setHasCompleted(true); + }, [candidateList, avatarColor, setCandidateList, onComplete, hasCompleted]); + + return ( +
+ {candidateList.map((candidate) => ( +
+ {candidate.name} +
+ ))} +
+ ); +}; + +export default AvatarAssignColor; diff --git a/app/dashboard/components/card.tsx b/app/dashboard/components/card.tsx new file mode 100644 index 0000000..2c801f1 --- /dev/null +++ b/app/dashboard/components/card.tsx @@ -0,0 +1,21 @@ +import React from "react"; + +interface CardProps { + title: string; + value: string | number; + icon?: React.ReactNode; +} + +const Card: React.FC = ({ title, value, icon }) => { + return ( +
+
+

{title}

+

{value}

+
+ {icon &&
{icon}
} +
+ ); +}; + +export default Card; diff --git a/app/dashboard/components/elimination-tree/candidate-list-bar.tsx b/app/dashboard/components/elimination-tree/candidate-list-bar.tsx new file mode 100644 index 0000000..a77ab63 --- /dev/null +++ b/app/dashboard/components/elimination-tree/candidate-list-bar.tsx @@ -0,0 +1,67 @@ +import { + Tooltip, + TooltipContent, + TooltipProvider, +} from "@/components/ui/tooltip"; +import { Candidate } from "./constants"; +import SearchDropdown from "./search-dropdown"; +import { TooltipTrigger } from "@radix-ui/react-tooltip"; +import { useState } from "react"; + +type CandidateListBarProps = { + selectedWinnerId: number | null; + handleSelectWinner: (id: number) => void; + useAvatar: boolean; + candidateList: Candidate[]; +}; + +function CandidateListBar({ + selectedWinnerId, + handleSelectWinner, + useAvatar, + candidateList, +}: CandidateListBarProps) { + const handleCandidateSelect = (candidateId: number) => { + // setSelectedCandidateId(candidateId); + handleSelectWinner(candidateId); // Call to display the tree or other action + }; + return ( +
+
+ {candidateList.map((candidate) => ( +
+ + + {/* handleSelectWinner(candidate.id)}*/} + {/* className="leading-9 w-10 h-10 rounded-full cursor-pointer text-center border-2 border-black text-xs overflow-hidden whitespace-nowrap text-ellipsis"*/} + {/*>*/} + {/* {candidate.name}*/} + {/**/} + + handleCandidateSelect(candidate.id)} + className={`leading-9 w-10 h-10 rounded-full cursor-pointer text-center border-2 text-xs overflow-hidden whitespace-nowrap text-ellipsis ${ + selectedWinnerId === candidate.id + ? "border-blue-500" // Outer blue circle when selected + : "border-black" + }`} + > + {candidate.name} + + {candidate.name} + + +
+ ))} +
+ {/**/} + +
+ ); +} + +export default CandidateListBar; diff --git a/app/dashboard/components/elimination-tree/constants.ts b/app/dashboard/components/elimination-tree/constants.ts new file mode 100644 index 0000000..0ffad80 --- /dev/null +++ b/app/dashboard/components/elimination-tree/constants.ts @@ -0,0 +1,263 @@ +// const testData = { +// name: "Root", +// children: [ +// { +// name: "Child 1", +// children: [ +// { +// name: "Grandchild 1.1", +// }, +// { +// name: "Grandchild 1.2", +// }, +// ], +// }, +// { +// name: "Child 2", +// children: [ +// { +// name: "Grandchild 2.1", +// children: [ +// { +// name: "Great-Grandchild 2.1.1", +// }, +// ], +// }, +// ], +// }, +// ], +// }; +const testData = { + name: "Alice", + children: [ + { + name: "Bob", + }, + { + name: "Chuan", + children: [ + { + name: "Diego", + }, + ], + }, + ], +}; + +const dataOneTree = { + id: 1, // winnerId + name: "Alice", + children: [ + { + id: 2, + name: "Bob", + }, + { + id: 3, + name: "Chuan", + children: [ + { + id: 4, + name: "Diego", + }, + ], + }, + ], +}; +const dataOneTree2 = { + id: 1, + name: "Alice", + children: [ + { + id: 2, + name: "Bob", + children: [ + { + id: 3, + name: "Chuan", + children: [ + { + id: 4, + name: "Diego", + }, + ], + }, + { + id: 4, + name: "Diego", + children: [ + { + id: 3, + name: "Chuan", + }, + ], + }, + ], + }, + { + id: 3, + name: "Chuan", + children: [ + { + id: 2, + name: "Bob", + children: [ + { + id: 4, + name: "Diego", + }, + ], + }, + { + id: 4, + name: "Diego", + children: [ + { + id: 2, + name: "Bob", + }, + ], + }, + ], + }, + { + id: 4, + name: "Diego", + children: [ + { + id: 2, + name: "Bob", + children: [ + { + id: 3, + name: "Chuan", + }, + ], + }, + { + id: 3, + name: "Chuan", + children: [ + { + id: 2, + name: "Bob", + }, + ], + }, + ], + }, + ], +}; + +// step by step +const dataStepByStep = { + type: "step-by-step", + process: [ + { + step: 0, // initial state + trees: [ + // oneTree * 4 + // 多个类似oneTree的结构放在这 + dataOneTree, + // dataTreeTwo, + ], + }, + { + step: 1, + assertion: "", // 目前就采用纯文本的方式吧,后续如果有更多要求这个可能会变成一个object + // Chuan beats Alice if only {Alice,Chuan} remain + before: [ + // assertion应用之前 + // oneTree * 4 + // 多个类似oneTree的结构放在这 + dataOneTree, + ], + after: [ + // assertion应用之后 + // oneTree * 4 + // 多个类似oneTree的结构放在这 + dataOneTree, + ], + }, + { + step: 2, + assertion: "", // 目前就采用纯文本的方式吧,后续如果有更多要求这个可能会变成一个object + // Chuan beats Alice if only {Alice,Chuan} remain + before: [ + // assertion应用之前 + // oneTree * 4 + // 多个类似oneTree的结构放在这 + dataOneTree, + ], + after: [ + // assertion应用之后 + // oneTree * 4 + // 多个类似oneTree的结构放在这 + dataOneTree, + ], + }, + ], +}; + +// multi-winner +const dataMultiWinner = [ + { + winnerInfo: { + // 这里要存一份,方便切换 + id: 2, // winnerId + name: "xxx", + }, + data: { + // stepByStep + }, + }, + { + winnerInfo: { + // 这里要存一份,方便切换 + id: 2, // winnerId + name: "xxx", + }, + data: { + // stepByStep + }, + }, +]; + +type Candidate = { + id: number; + name: string; + color?: string; +}; +const candidateList: Candidate[] = [ + { + id: 1, + name: "Alice", + color: "", + }, + { + id: 2, + name: "Bob", + color: "", + }, + { + id: 3, + name: "Chuan", + color: "", + }, + { + id: 4, + name: "Diego", + color: "", + }, +]; + +export type { Candidate }; + +export { + testData, + dataOneTree, + dataStepByStep, + dataMultiWinner, + dataOneTree2, + candidateList, +}; diff --git a/app/dashboard/components/elimination-tree/demo.ts b/app/dashboard/components/elimination-tree/demo.ts new file mode 100644 index 0000000..2651e9e --- /dev/null +++ b/app/dashboard/components/elimination-tree/demo.ts @@ -0,0 +1,2075 @@ +import { TreeNode } from "@/components/tree/helper"; + +type demoType = { + winnerInfo: { + id: number; + name: string; + }; + data: { + type: string; + process: Array<{ + step: number; + trees?: TreeNode | null; + assertion?: string; + before?: TreeNode | null; + after?: TreeNode | null; + }>; + }; +}[]; + +const demoFromCore: demoType = [ + { + winnerInfo: { + id: 0, + name: "Alice", + }, + data: { + type: "step-by-step", + process: [ + { + step: 0, + trees: { + id: 0, + name: "Alice", + children: [ + { + id: 1, + name: "Bob", + children: [ + { + id: 2, + name: "Chuan", + children: [ + { + id: 3, + name: "Diego", + children: [], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 2, + name: "Chuan", + children: [], + }, + ], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 1, + name: "Bob", + children: [ + { + id: 2, + name: "Chuan", + children: [], + }, + ], + }, + { + id: 2, + name: "Chuan", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + ], + }, + { + id: 2, + name: "Chuan", + children: [ + { + id: 1, + name: "Bob", + children: [ + { + id: 3, + name: "Diego", + children: [], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + ], + }, + ], + }, + }, + { + step: 1, + assertion: "Chuan beats Bob always", + before: { + id: 0, + name: "Alice", + children: [ + { + id: 1, + name: "Bob", + children: [ + { + id: 2, + name: "Chuan", + children: [ + { + id: 3, + name: "Diego", + children: [], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 2, + name: "Chuan", + children: [], + }, + ], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 1, + name: "Bob", + children: [ + { + id: 2, + name: "Chuan", + children: [], + }, + ], + }, + { + id: 2, + name: "Chuan", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + ], + }, + { + id: 2, + name: "Chuan", + children: [ + { + id: 1, + name: "Bob", + children: [ + { + id: 3, + name: "Diego", + children: [], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + ], + }, + ], + }, + after: { + id: 0, + name: "Alice", + children: [ + { + id: 2, + name: "Chuan", + children: [ + { + id: 1, + name: "Bob", + children: [ + { + id: 3, + name: "Diego", + children: [], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 2, + name: "Chuan", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + ], + }, + ], + }, + }, + { + step: 2, + assertion: "Chuan beats Alice if only {Alice,Chuan} remain", + before: { + id: 0, + name: "Alice", + children: [ + { + id: 2, + name: "Chuan", + children: [ + { + id: 1, + name: "Bob", + children: [ + { + id: 3, + name: "Diego", + children: [], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 2, + name: "Chuan", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + ], + }, + ], + }, + + after: { + id: 0, + name: "Alice", + children: [ + { + id: 3, + name: "Diego", + children: [ + { + id: 2, + name: "Chuan", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + ], + }, + ], + }, + }, + { + step: 3, + assertion: "Chuan beats Diego if only {Alice,Chuan,Diego} remain", + before: { + id: 0, + name: "Alice", + children: [ + { + id: 3, + name: "Diego", + children: [ + { + id: 2, + name: "Chuan", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + ], + }, + ], + }, + after: null, + }, + ], + }, + }, + { + winnerInfo: { + id: 1, + name: "Bob", + }, + data: { + type: "step-by-step", + process: [ + { + step: 0, + trees: { + id: 1, + name: "Bob", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 2, + name: "Chuan", + children: [ + { + id: 3, + name: "Diego", + children: [], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 2, + name: "Chuan", + children: [], + }, + ], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 2, + name: "Chuan", + children: [], + }, + ], + }, + { + id: 2, + name: "Chuan", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + { + id: 2, + name: "Chuan", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 3, + name: "Diego", + children: [], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + ], + }, + }, + { + step: 1, + assertion: "Chuan beats Bob always", + before: { + id: 1, + name: "Bob", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 2, + name: "Chuan", + children: [ + { + id: 3, + name: "Diego", + children: [], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 2, + name: "Chuan", + children: [], + }, + ], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 2, + name: "Chuan", + children: [], + }, + ], + }, + { + id: 2, + name: "Chuan", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + { + id: 2, + name: "Chuan", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 3, + name: "Diego", + children: [], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + ], + }, + + after: null, + }, + ], + }, + }, + { + winnerInfo: { + id: 2, + name: "Chuan", + }, + data: { + type: "step-by-step", + process: [ + { + step: 0, + trees: { + id: 2, + name: "Chuan", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 1, + name: "Bob", + children: [ + { + id: 3, + name: "Diego", + children: [], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + { + id: 1, + name: "Bob", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + { + id: 1, + name: "Bob", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 3, + name: "Diego", + children: [], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + ], + }, + }, + { + step: 1, + assertion: "Chuan beats Bob always", + before: { + id: 2, + name: "Chuan", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 1, + name: "Bob", + children: [ + { + id: 3, + name: "Diego", + children: [], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + { + id: 1, + name: "Bob", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + { + id: 1, + name: "Bob", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 3, + name: "Diego", + children: [], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + ], + }, + after: { + id: 2, + name: "Chuan", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 1, + name: "Bob", + children: [ + { + id: 3, + name: "Diego", + children: [], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + { + id: 1, + name: "Bob", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + { + id: 1, + name: "Bob", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 3, + name: "Diego", + children: [], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + ], + }, + }, + { + step: 2, + assertion: "Chuan beats Alice if only {Alice,Chuan} remain", + before: { + id: 2, + name: "Chuan", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 1, + name: "Bob", + children: [ + { + id: 3, + name: "Diego", + children: [], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + { + id: 1, + name: "Bob", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + { + id: 1, + name: "Bob", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 3, + name: "Diego", + children: [], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + ], + }, + after: { + id: 2, + name: "Chuan", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 1, + name: "Bob", + children: [ + { + id: 3, + name: "Diego", + children: [], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + { + id: 1, + name: "Bob", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + { + id: 1, + name: "Bob", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 3, + name: "Diego", + children: [], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + ], + }, + }, + { + step: 3, + assertion: "Chuan beats Diego if only {Alice,Chuan,Diego} remain", + before: { + id: 2, + name: "Chuan", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 1, + name: "Bob", + children: [ + { + id: 3, + name: "Diego", + children: [], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + { + id: 1, + name: "Bob", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + { + id: 1, + name: "Bob", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 3, + name: "Diego", + children: [], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + ], + }, + after: { + id: 2, + name: "Chuan", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 1, + name: "Bob", + children: [ + { + id: 3, + name: "Diego", + children: [], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + { + id: 1, + name: "Bob", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + { + id: 1, + name: "Bob", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 3, + name: "Diego", + children: [], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + ], + }, + }, + { + step: 4, + assertion: "Alice beats Diego if only {Alice,Chuan,Diego} remain", + before: { + id: 2, + name: "Chuan", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 1, + name: "Bob", + children: [ + { + id: 3, + name: "Diego", + children: [], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + { + id: 1, + name: "Bob", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + { + id: 1, + name: "Bob", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 3, + name: "Diego", + children: [], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + ], + }, + after: { + id: 2, + name: "Chuan", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 1, + name: "Bob", + children: [ + { + id: 3, + name: "Diego", + children: [], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + ], + }, + { + id: 1, + name: "Bob", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 3, + name: "Diego", + children: [], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 1, + name: "Bob", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + ], + }, + }, + { + step: 5, + assertion: "Alice beats Bob if only {Alice,Bob,Chuan,Diego} remain", + before: { + id: 2, + name: "Chuan", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 1, + name: "Bob", + children: [ + { + id: 3, + name: "Diego", + children: [], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + ], + }, + { + id: 1, + name: "Bob", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 3, + name: "Diego", + children: [], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 1, + name: "Bob", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + ], + }, + after: { + id: 2, + name: "Chuan", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 1, + name: "Bob", + children: [ + { + id: 3, + name: "Diego", + children: [], + }, + ], + }, + { + id: 3, + name: "Diego", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + ], + }, + { + id: 1, + name: "Bob", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 3, + name: "Diego", + children: [], + }, + ], + }, + ], + }, + ], + }, + }, + ], + }, + }, + { + winnerInfo: { + id: 3, + name: "Diego", + }, + data: { + type: "step-by-step", + process: [ + { + step: 0, + trees: { + id: 3, + name: "Diego", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 1, + name: "Bob", + children: [ + { + id: 2, + name: "Chuan", + children: [], + }, + ], + }, + { + id: 2, + name: "Chuan", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + ], + }, + { + id: 2, + name: "Chuan", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + { + id: 1, + name: "Bob", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + { + id: 1, + name: "Bob", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 2, + name: "Chuan", + children: [], + }, + ], + }, + { + id: 2, + name: "Chuan", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + ], + }, + }, + { + step: 1, + assertion: "Chuan beats Bob always", + before: { + id: 3, + name: "Diego", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 1, + name: "Bob", + children: [ + { + id: 2, + name: "Chuan", + children: [], + }, + ], + }, + { + id: 2, + name: "Chuan", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + ], + }, + { + id: 2, + name: "Chuan", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + { + id: 1, + name: "Bob", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + { + id: 1, + name: "Bob", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 2, + name: "Chuan", + children: [], + }, + ], + }, + { + id: 2, + name: "Chuan", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + ], + }, + after: { + id: 3, + name: "Diego", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 2, + name: "Chuan", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + ], + }, + { + id: 2, + name: "Chuan", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + { + id: 1, + name: "Bob", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + ], + }, + }, + { + step: 2, + assertion: "Chuan beats Alice if only {Alice,Chuan} remain", + before: { + id: 3, + name: "Diego", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 2, + name: "Chuan", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + ], + }, + { + id: 2, + name: "Chuan", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + { + id: 1, + name: "Bob", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + ], + }, + after: { + id: 3, + name: "Diego", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 2, + name: "Chuan", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + ], + }, + { + id: 2, + name: "Chuan", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + { + id: 1, + name: "Bob", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + ], + }, + }, + { + step: 3, + assertion: "Chuan beats Diego if only {Alice,Chuan,Diego} remain", + before: { + id: 3, + name: "Diego", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 2, + name: "Chuan", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + ], + }, + { + id: 2, + name: "Chuan", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + { + id: 1, + name: "Bob", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + ], + }, + after: { + id: 3, + name: "Diego", + children: [ + { + id: 2, + name: "Chuan", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + { + id: 1, + name: "Bob", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + ], + }, + }, + { + step: 4, + assertion: "Alice beats Diego if only {Alice,Chuan,Diego} remain", + before: { + id: 3, + name: "Diego", + children: [ + { + id: 2, + name: "Chuan", + children: [ + { + id: 0, + name: "Alice", + children: [ + { + id: 1, + name: "Bob", + children: [], + }, + ], + }, + { + id: 1, + name: "Bob", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + ], + }, + after: { + id: 3, + name: "Diego", + children: [ + { + id: 2, + name: "Chuan", + children: [ + { + id: 1, + name: "Bob", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + ], + }, + }, + { + step: 5, + assertion: "Alice beats Bob if only {Alice,Bob,Chuan,Diego} remain", + before: { + id: 3, + name: "Diego", + children: [ + { + id: 2, + name: "Chuan", + children: [ + { + id: 1, + name: "Bob", + children: [ + { + id: 0, + name: "Alice", + children: [], + }, + ], + }, + ], + }, + ], + }, + after: null, + }, + ], + }, + }, +]; + +export { demoFromCore }; diff --git a/app/dashboard/components/elimination-tree/dropdown.tsx b/app/dashboard/components/elimination-tree/dropdown.tsx new file mode 100644 index 0000000..9153128 --- /dev/null +++ b/app/dashboard/components/elimination-tree/dropdown.tsx @@ -0,0 +1,47 @@ +"use client"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Switch } from "@/components/ui/switch"; +import useTreeSettingsStore from "@/store/use-tree-setting-store"; +import { ChevronDown } from "lucide-react"; + +function Dropdown() { + const { stepByStep, hideAvatar, setHideAvatar, setStepByStep } = + useTreeSettingsStore(); + return ( + + + + + + +
+
Step by Step
+ +
+
+ +
+
Hide Avatar
+ +
+
+ +
Save
+
+
+
+ ); +} + +export default Dropdown; diff --git a/app/dashboard/components/elimination-tree/index.tsx b/app/dashboard/components/elimination-tree/index.tsx new file mode 100644 index 0000000..868a212 --- /dev/null +++ b/app/dashboard/components/elimination-tree/index.tsx @@ -0,0 +1,155 @@ +"use client"; +import CandidateListBar from "@/app/dashboard/components/elimination-tree/candidate-list-bar"; +import Dropdown from "@/app/dashboard/components/elimination-tree/dropdown"; +import StepByStep from "@/app/dashboard/components/elimination-tree/step-by-step"; +// import { demoFromCore } from "@/app/dashboard/components/elimination-tree/demo"; +import Tree from "../../../../components/tree"; +import { useEffect, useMemo, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { ArrowLeft, ArrowRight, Undo2 } from "lucide-react"; +import TooltipWithIcon from "@/app/dashboard/components/Information-icon-text"; +import useMultiWinnerDataStore from "@/store/multi-winner-data"; + +function EliminationTree() { + const { multiWinner } = useMultiWinnerDataStore(); + useEffect(() => { + console.log("?", multiWinner); + }, [multiWinner]); + + // Define loading state + const isLoading = multiWinner === null; + + // Ensure multiWinner has at least one winnerInfo + const [selectedWinnerId, setSelectedWinnerId] = useState(0); + const [selectedStep, setSelectedStep] = useState(1); // Ensure this is always defined + + const [resetHiddenNodes, setResetHiddenNodes] = useState(false); + + // Use useEffect to initialize selectedWinnerId when multiWinner loads + useEffect(() => { + if (multiWinner && multiWinner.length > 0) { + setSelectedWinnerId(multiWinner[0].winnerInfo.id); + } + }, [multiWinner]); + + // Memoize the possible winner list from demoFromCore + const possibleWinnerList = useMemo(() => { + return Array.isArray(multiWinner) + ? multiWinner.map((cur) => cur.winnerInfo) + : []; // Default to an empty array if demoFromCore is not an array + }, [multiWinner]); // Ensure this value does not change between renders + + const oneWinnerTrees = useMemo(() => { + return multiWinner + ? multiWinner.find((cur) => cur.winnerInfo.id === selectedWinnerId) || + null + : null; + }, [selectedWinnerId, multiWinner]); + + // Handle case when oneWinnerTrees is null + if (isLoading || !oneWinnerTrees) { + return ( +
+ loading {/* Replace with your loading component */} +
+ ); + } + + const { winnerInfo, data } = oneWinnerTrees; + const stepSize = data.process.length - 1; // Handle stepSize for 0 case + + const isNextDisabled = selectedStep >= stepSize; + + const NextComponent = ( + + ); + + const isBackDisabled = selectedStep <= 1; + + const BackComponent = ( + + ); + + const handleRevertAssertion = () => { + setResetHiddenNodes(true); + }; + + const handleResetComplete = () => { + setResetHiddenNodes(false); + }; + + return ( +
+
+ {/* Elimination Tree title and Tooltip with Icon */} +
+

Elimination Tree

+ +
+ + {/**/} +
+
+ { + handleRevertAssertion(); + setSelectedStep(1); // Reset step + setSelectedWinnerId(id); + }} + useAvatar={false} + candidateList={possibleWinnerList} + /> +
+
+ +
+ +
+
+
+
Applied Assertion:
+
{data.process[selectedStep].assertion}
+
+ +
+ +
+
+
+
+ ); +} + +export default EliminationTree; diff --git a/app/dashboard/components/elimination-tree/search-dropdown.tsx b/app/dashboard/components/elimination-tree/search-dropdown.tsx new file mode 100644 index 0000000..9e2e260 --- /dev/null +++ b/app/dashboard/components/elimination-tree/search-dropdown.tsx @@ -0,0 +1,86 @@ +"use client"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { useState } from "react"; +import { Candidate } from "./constants"; +import { Card, CardContent } from "@/components/ui/card"; +import Image from "next/image"; + +// function SearchDropdown({ candidateList }: { candidateList: Candidate[] }) { +// const [searchTerm, setSearchTerm] = useState(""); +// const [selectedCandidate, setSelectedCandidate] = useState( +// null, +// ); +// const [isOpen, setIsOpen] = useState(false); +// //不能用shadcn的popover,因为他的popover弹出瞬间会让input失去焦点 +// +// const filteredList = candidateList.filter((candidate) => +// candidate.name.toLowerCase().includes(searchTerm.toLowerCase()), +// ); +// const handleSelect = (candidate: Candidate) => { +// setSelectedCandidate(candidate); +// setSearchTerm(candidate.name); +// console.log("Selected Candidate:", candidate.name); +// setIsOpen(false); +// }; + +function SearchDropdown({ + candidateList, + onSelect, +}: { + candidateList: Candidate[]; + onSelect: (candidateId: number) => void; +}) { + const [searchTerm, setSearchTerm] = useState(""); + const [isOpen, setIsOpen] = useState(false); + + const filteredList = candidateList.filter((candidate) => + candidate.name.toLowerCase().includes(searchTerm.toLowerCase()), + ); + + const handleSelect = (candidate: Candidate) => { + setSearchTerm(candidate.name); + onSelect(candidate.id); + setIsOpen(false); + }; + return ( +
+ setSearchTerm(e.target.value)} + onFocus={() => setIsOpen(true)} + onBlur={() => setIsOpen(false)} + /> + {/* 虽然没用,但是得有这个才能正常触发popover */} + {isOpen && ( + + {filteredList.length > 0 ? ( + filteredList.map((candidate) => ( +
handleSelect(candidate)} + className="cursor-pointer p-2 rounded-sm hover:bg-gray-200 " + > + {/* {candidate.avatar && ( + {candidate.name} + )} */} + {candidate.name} +
+ )) + ) : ( +
No candidates found
+ )} +
+ )} +
+ ); +} + +export default SearchDropdown; diff --git a/app/dashboard/components/elimination-tree/step-by-step.tsx b/app/dashboard/components/elimination-tree/step-by-step.tsx new file mode 100644 index 0000000..b5fbc89 --- /dev/null +++ b/app/dashboard/components/elimination-tree/step-by-step.tsx @@ -0,0 +1,32 @@ +import { Dispatch, SetStateAction } from "react"; + +type StepByStepProps = { + stepSize?: number; + selectedStep: number; + setSelectedStep: Dispatch>; +}; +function StepByStep({ + stepSize = 5, + selectedStep, + setSelectedStep, +}: StepByStepProps) { + return ( +
+
+ {Array.from({ length: stepSize }, (_, index) => ( +
setSelectedStep(index + 1)} + className={`rounded-full w-10 h-10 text-center leading-10 mb-2 cursor-pointer z-10 font-bold ${selectedStep === index + 1 ? "bg-[#18a0fb]" : "bg-[#b3b3b3]"}`} + > + {index + 1} +
+ ))} +
+ ); +} + +export default StepByStep; diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx new file mode 100644 index 0000000..c722d4d --- /dev/null +++ b/app/dashboard/page.tsx @@ -0,0 +1,140 @@ +"use client"; +import React, { useState } from "react"; +import Card from "./components/card"; +import AssertionTable from "./components/assertion-table"; +import AssertionsDetailsModal from "./components/assertions-details-modal"; +import { FaUserFriends, FaTrophy, FaList } from "react-icons/fa"; +import { Button } from "@/components/ui/button"; +import Link from "next/link"; +import { ChevronRight, FilePenLine } from "lucide-react"; + +import EliminationTree from "./components/elimination-tree"; +import AvatarAssignColor from "./components/avatar-assign-color"; // 引入 Avatar 组件 +import useMultiWinnerDataStore from "@/store/multi-winner-data"; +import multiWinnerData from "@/store/multi-winner-data"; // 引入 zustand store + +const Dashboard: React.FC = () => { + const { candidateList, assertionList, winnerInfo } = + useMultiWinnerDataStore(); + + // 将所有 Hooks 移到顶层 + const [isAvatarReady, setIsAvatarReady] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); // 移到这里 + + // Avatar 完成后调用的函数 + const handleAvatarComplete = () => { + setIsAvatarReady(true); + }; + + const handleViewDetails = () => { + setIsModalOpen(true); + }; + + const handleCloseModal = () => { + setIsModalOpen(false); + }; + + // 仅在 Avatar 完成时渲染 Dashboard 内容 + if (!isAvatarReady) { + return ( +
+ +
Loading...
+
+ ); + } + + // 获取带有 name 字段的 assertionList + const assertionsWithNames = assertionList.map((assertion) => ({ + ...assertion, + name: + candidateList.find((candidate) => candidate.id === assertion.winner) + ?.name || "Unknown", + })); + + // 获取候选人的数量 + const candidateNum = candidateList.length; + + // // 获取胜利者的信息 + // const winner = + // assertionList.length > 0 + // ? candidateList.find( + // (candidate) => candidate.id === assertionList[0].winner, + // ) || { + // id: -1, + // name: "Unknown", + // } + // : { id: -1, name: "Unknown" }; + + // 获取断言的数量 + const assertionNum = assertionList.length; + + // 计算最大难度和最小差距 + const maxDifficulty = Math.max(...assertionList.map((a) => a.difficulty)); + const minMargin = Math.min(...assertionList.map((a) => a.margin)); + + return ( +
+ {/* 文件上传按钮 */} +
+ + + +
+ + {/* Grid 布局 */} +
+ {/* 左侧区域 */} +
+ {/* 数据卡片 */} +
+ } + /> + } + /> + } /> +
+ + {/* Elimination Tree section */} + +
+ + {/* 右侧区域:Assertion 表格 */} +
+
+

The Assertions

+
+ +
+
+

+ Parse from your uploaded file +

+ +
+
+ + {/* Modal 组件 */} + +
+ ); +}; + +export default Dashboard; diff --git a/app/explain-assertions/components/describe-raire-result.tsx b/app/explain-assertions/components/describe-raire-result.tsx new file mode 100644 index 0000000..5f08405 --- /dev/null +++ b/app/explain-assertions/components/describe-raire-result.tsx @@ -0,0 +1,159 @@ +// components/describe-raire-result.tsx + +import React from "react"; +import { + assertion_description_with_triangles, + Assertion, +} from "../../../lib/explain/prettyprint_assertions_and_pictures"; +// ... import other necessary functions ... + +interface DescribeRaireResultProps { + data: any; +} + +const DescribeRaireResult: React.FC = ({ data }) => { + // Helper functions adjusted to work within the component + const candidate_name = (id: number): string => { + if (data.metadata && Array.isArray(data.metadata.candidates)) { + const name = data.metadata.candidates[id]; + if (name) { + return name; + } + } + return `Candidate ${id}`; + }; + + const candidate_name_list = (ids: number[]): string => { + return ids.map(candidate_name).join(","); + }; + + const describe_time = ( + what: string, + time_taken: { seconds: number; work: number }, + ) => { + if (time_taken) { + const time_desc = + time_taken.seconds > 0.1 + ? `${Number(time_taken.seconds).toFixed(1)} seconds` + : `${Number(time_taken.seconds * 1000).toFixed(2)} milliseconds`; + return ( +

+ Time to {what}: {time_desc} ({time_taken.work} operations) +

+ ); + } + return null; + }; + + // Now, handle the rendering logic based on `data` + if (data.solution && data.solution.Ok) { + // Extract data for rendering + const { assertions, winner, num_candidates } = data.solution.Ok; + const candidate_names = + data.metadata && data.metadata.candidates + ? data.metadata.candidates + : Array.from({ length: num_candidates }, (_, i) => `Candidate ${i}`); + + // Prepare the assertion elements + const assertionElements = assertions.map((av: any, index: number) => { + const a = av.assertion as Assertion; + + let description: JSX.Element; + if (a.type === "NEN") { + description = ( + <> + NEN: {candidate_name(a.winner)} > {candidate_name(a.loser)} if + only {"{"} + {candidate_name_list(a.continuing!)} + {"}"} remain + + ); + } else if (a.type === "NEB") { + description = ( + <> + {candidate_name(a.winner)} NEB {candidate_name(a.loser)} + + ); + } else { + description = <>Unknown assertion type; + } + + return ( +
+ {/* Render risk, difficulty, margin, etc., if available */} + {description} +
+ ); + }); + + return ( +
+ {data.solution.Ok.warning_trim_timed_out && ( +

+ Warning: Trimming timed out. Some assertions may be redundant. +

+ )} + {describe_time( + "determine winners", + data.solution.Ok.time_to_determine_winners, + )} + {describe_time( + "find assertions", + data.solution.Ok.time_to_find_assertions, + )} + {describe_time( + "trim assertions", + data.solution.Ok.time_to_trim_assertions, + )} +

Assertions

+ {assertionElements} + {/* If you need to include the explanation, you can render another component here */} + {/* For example: */} + {/* */} +
+ ); + } else if (data.solution && data.solution.Err) { + // Handle error cases + const err = data.solution.Err; + let errorMessage = `Error: ${JSON.stringify(err)}`; + + // Customize error messages based on the error type + if (err === "InvalidCandidateNumber") { + errorMessage = + "Invalid candidate number in the preference list. Candidate numbers should be 0 to num_candidates-1 inclusive."; + } else if (err === "InvalidNumberOfCandidates") { + errorMessage = + "Invalid number of candidates. There should be at least one candidate."; + } else if (err === "TimeoutCheckingWinner") { + errorMessage = + "Timeout checking winner - either your problem is exceptionally difficult, or your timeout is exceedingly small."; + } else if (err.hasOwnProperty("TimeoutFindingAssertions")) { + errorMessage = `Timeout finding assertions - your problem is quite hard. Difficulty when interrupted: ${err.TimeoutFindingAssertions}`; + } else if (err === "InvalidTimeout") { + errorMessage = + "Timeout is not valid. Timeout should be a number greater than zero."; + } else if (Array.isArray(err.CouldNotRuleOut)) { + errorMessage = + "Impossible to audit. Could not rule out the following elimination order:"; + // You can render the list of candidates here if needed + } else if (Array.isArray(err.TiedWinners)) { + errorMessage = `Audit not possible as ${candidate_name_list(err.TiedWinners)} are tied IRV winners and a one vote difference would change the outcome.`; + } else if (Array.isArray(err.WrongWinner)) { + errorMessage = `The votes are not consistent with the provided winner. Perhaps ${candidate_name_list(err.WrongWinner)}?`; + } + + return ( +
+

{errorMessage}

+
+ ); + } else { + return ( +
+

Output is wrong format

+
+ ); + } +}; + +export default DescribeRaireResult; diff --git a/app/explain-assertions/components/explain-process.tsx b/app/explain-assertions/components/explain-process.tsx new file mode 100644 index 0000000..b7970b7 --- /dev/null +++ b/app/explain-assertions/components/explain-process.tsx @@ -0,0 +1,378 @@ +// utils/explainAssertions.ts + +import { + explain, + all_elimination_orders, + assertion_all_allowed_suffixes, +} from "../../../lib/explain/prettyprint_assertions_and_pictures"; + +// Function to infer the winner from assertions +const inferWinnerFromAssertions = ( + assertions: any[], + numCandidates: number, +): number | null => { + // Initialize possible elimination orders + let eliminationOrders = all_elimination_orders(numCandidates); + + // Apply each assertion to filter the elimination orders + for (let i = 0; i < assertions.length; i++) { + const assertionObj = assertions[i]; + const assertion = assertionObj.assertion; + eliminationOrders = assertion_all_allowed_suffixes( + assertion, + eliminationOrders, + numCandidates, + false, + ); + + if (eliminationOrders.length === 0) { + // No valid elimination orders remain, cannot infer a winner + return null; + } + } + + // Collect the set of possible winners + const possibleWinners: number[] = []; + for (let i = 0; i < eliminationOrders.length; i++) { + const order = eliminationOrders[i]; + const winner = order[order.length - 1]; + if (!possibleWinners.includes(winner)) { + possibleWinners.push(winner); + } + } + + if (possibleWinners.length === 1) { + // Unique winner inferred + return possibleWinners[0]; + } else { + // Multiple possible winners, cannot determine a unique winner + return null; + } +}; + +// JSON validation function +const validateInputData = ( + data: any, +): { error_message: string; state: number } | null => { + // Check if metadata and candidates array are present and valid + if (!data.metadata || !Array.isArray(data.metadata.candidates)) { + return { error_message: "Invalid metadata or candidates field", state: 0 }; + } + + // Check if solution and solution.Ok exist and are valid + if (!data.solution || !data.solution.Ok) { + return { error_message: "Invalid solution structure", state: 0 }; + } + + const solution = data.solution.Ok; + + // Check if difficulty and margin exist in solution.Ok and are valid numbers + if (typeof solution.difficulty !== "number" || solution.difficulty < 0) { + return { + error_message: "Invalid or missing 'difficulty' in solution.Ok", + state: 0, + }; + } + + if (typeof solution.margin !== "number" || solution.margin < 0) { + return { + error_message: "Invalid or missing 'margin' in solution.Ok", + state: 0, + }; + } + + // Check if assertions are present and valid as an array + if (!Array.isArray(solution.assertions)) { + return { error_message: "Invalid assertions field", state: 0 }; + } + + // Check if num_candidates matches the length of candidates array + const numCandidates = data.metadata.candidates.length; + if (solution.num_candidates !== numCandidates) { + return { + error_message: + "Mismatch between num_candidates and candidates array length", + state: 0, + }; + } + + // Validate if winner is within the valid range + if ( + typeof solution.winner !== "number" || + solution.winner < 0 || + solution.winner >= numCandidates + ) { + return { error_message: "Winner index out of range or invalid", state: 0 }; + } + + // Validate each assertion's completeness and fields + for (let index = 0; index < solution.assertions.length; index++) { + const assertionObj = solution.assertions[index]; + if (!assertionObj.assertion) { + return { + error_message: `Assertion at index ${index} missing 'assertion' field`, + state: 0, + }; + } + + const assertion = assertionObj.assertion; + + // Check if assertion.type exists and is a string + if (!assertion.type || typeof assertion.type !== "string") { + return { + error_message: `Assertion at index ${index} missing 'type' field or 'type' is not a string`, + state: 0, + }; + } + + // Check if assertion.winner and assertion.loser exist and are within valid range + if ( + typeof assertion.winner !== "number" || + assertion.winner < 0 || + assertion.winner >= numCandidates + ) { + return { + error_message: `Invalid or missing 'winner' index in assertion at index ${index}`, + state: 0, + }; + } + + if ( + typeof assertion.loser !== "number" || + assertion.loser < 0 || + assertion.loser >= numCandidates + ) { + return { + error_message: `Invalid or missing 'loser' index in assertion at index ${index}`, + state: 0, + }; + } + + // For assertions of type 'NEN', check if the continuing array is valid + if (assertion.type === "NEN") { + if (!Array.isArray(assertion.continuing)) { + return { + error_message: `Assertion of type 'NEN' at index ${index} missing 'continuing' array`, + state: 0, + }; + } + + // Check if the continuing array indices are valid + for (let i = 0; i < assertion.continuing.length; i++) { + const candidateIndex = assertion.continuing[i]; + if ( + typeof candidateIndex !== "number" || + candidateIndex < 0 || + candidateIndex >= numCandidates + ) { + return { + error_message: `Invalid index in 'continuing' array at position ${i} in assertion at index ${index}`, + state: 0, + }; + } + } + } else if (assertion.type !== "NEB") { + return { + error_message: `Unknown assertion type '${assertion.type}' at index ${index}`, + state: 0, + }; + } + } + + // At the end of validation, infer the winner and compare + const inferredWinner = inferWinnerFromAssertions( + solution.assertions, + numCandidates, + ); + + if (inferredWinner === null) { + return { + error_message: "Unable to infer a unique winner from the assertions.", + state: 1, + }; + } + + if (inferredWinner !== solution.winner) { + const winnerName = data.metadata.candidates[inferredWinner]; + const expectedWinnerName = data.metadata.candidates[solution.winner]; + return { + error_message: `Inferred winner (${winnerName}) does not match the winner in the JSON data (${expectedWinnerName}).`, + state: 1, + }; + } + + return null; // All validations passed +}; + +// Function to mark cut nodes in the 'before' tree by comparing with 'after' tree +const markCutNodes = (beforeTree: any, afterTree: any | null) => { + // Helper function to get all paths from a tree + const getPaths = (node: any, path: number[] = []): Set => { + const paths = new Set(); + const currentPath = [...path, node.id]; + paths.add(currentPath.join("-")); + if (node.children) { + for (let i = 0; i < node.children.length; i++) { + const child = node.children[i]; + const childPaths = getPaths(child, currentPath); + childPaths.forEach((p) => { + paths.add(p); + }); + } + } + return paths; + }; + + // If afterTree is null, apply the new logic + if (!afterTree) { + // Set 'cut: true' on root's child nodes, and 'eliminated: true' on other nodes + const markCutsAtRoot = (node: any) => { + if (node.children && node.children.length > 0) { + for (let i = 0; i < node.children.length; i++) { + const child = node.children[i]; + child.cut = true; + markEliminated(child); + } + } + }; + + const markEliminated = (node: any) => { + if (node.children && node.children.length > 0) { + for (let i = 0; i < node.children.length; i++) { + const child = node.children[i]; + child.eliminated = true; + markEliminated(child); + } + } + }; + + markCutsAtRoot(beforeTree); + return; + } + + // Get all paths from afterTree + const afterPaths = getPaths(afterTree); + + // Function to mark cuts in beforeTree + const markCuts = ( + node: any, + path: number[] = [], + isParentCut: boolean = false, + ): boolean => { + const currentPath = [...path, node.id]; + const pathStr = currentPath.join("-"); + + // Check if current path exists in afterPaths + const existsInAfter = afterPaths.has(pathStr); + + let hasValidChild = false; + + if (node.children && node.children.length > 0) { + for (let i = 0; i < node.children.length; i++) { + const child = node.children[i]; + // If the current node is being cut, pass isParentCut = true + const childValid = markCuts( + child, + currentPath, + isParentCut || !existsInAfter, + ); + if (childValid) { + hasValidChild = true; + } + } + } + + if ( + !existsInAfter || + (!hasValidChild && node.children && node.children.length > 0) + ) { + if (!isParentCut) { + node.cut = true; + } else { + node.eliminated = true; + } + return false; + } + + return true; + }; + + // Mark cuts in beforeTree + markCuts(beforeTree); +}; + +// Main function to process inputText and return the outputData +export function explainAssertions(inputText: string): any { + // Parse the JSON input + let inputData; + try { + inputData = JSON.parse(inputText); + } catch (e) { + return { + success: false, + state: 0, + error_message: "Invalid JSON input", + }; + } + + // Validate the input data + const validationResult = validateInputData(inputData); + + if (validationResult) { + // There is an error + return { + success: false, + error_message: validationResult.error_message, + state: validationResult.state, + }; + } + + try { + // If validation passes, call the explain function + const multiWinnerData = explain( + inputData.solution.Ok.assertions.map((a: any) => a.assertion), + inputData.metadata.candidates, + /* expand_fully_at_start */ true, + /* hide_winner */ false, + inputData.solution.Ok.winner, + ); + + // Process multiWinnerData to mark 'cut' and 'eliminated' nodes + if (multiWinnerData && Array.isArray(multiWinnerData)) { + for (let i = 0; i < multiWinnerData.length; i++) { + const winnerData = multiWinnerData[i]; + const process = winnerData.data.process; + if (process && Array.isArray(process)) { + for (let j = 0; j < process.length; j++) { + const step = process[j]; + if (step.before) { + // If 'after' exists, pass it; otherwise, pass null + const afterTree = step.after || null; + markCutNodes(step.before, afterTree); + } else if (step.trees) { + // For step 0, possibly only 'trees' property + markCutNodes(step.trees, step.trees); + } + } + } + } + } + + // Return the output data + return { + success: true, + data: multiWinnerData, + }; + } catch (error) { + // Handle any unexpected errors + let errorMessage = "An unexpected error occurred."; + if (error instanceof Error) { + errorMessage = error.message; + } + + return { + success: false, + error_message: errorMessage, + }; + } +} diff --git a/app/explain-assertions/page.tsx b/app/explain-assertions/page.tsx new file mode 100644 index 0000000..8930ebb --- /dev/null +++ b/app/explain-assertions/page.tsx @@ -0,0 +1,52 @@ +"use client"; +// pages/explain-assertions.tsx + +import React, { useState } from "react"; +import { explainAssertions } from "./components/explain-process"; + +const ExplainAssertionsPage = () => { + const [inputText, setInputText] = useState(""); + const [outputData, setOutputData] = useState(null); + const [error, setError] = useState(null); + + const handleInputChange = (event: React.ChangeEvent) => { + setInputText(event.target.value); + }; + + const handleExplain = () => { + // 直接将输入文本传递给 explainAssertions 方法 + const result = explainAssertions(inputText); + + if (result.success) { + setOutputData(result.data); + setError(null); + } else { + setError(result.error_message); + setOutputData(null); + } + }; + + return ( +
+

Explain Assertions

+