diff --git a/front_end/messages/cs.json b/front_end/messages/cs.json index 92001528d7..32108b8ffd 100644 --- a/front_end/messages/cs.json +++ b/front_end/messages/cs.json @@ -1684,8 +1684,8 @@ "resolutionScores": "Skóre výsledků", "participationSummary": "Shrnutí účasti", "participationSummaryPredictionNrStats": "Udělal(a) jste {userUpdates} {userUpdates, plural, =0 {aktualizací} =1 {aktualizaci} other {aktualizací} } vs. průměr komunity {communityUpdates} {communityUpdates, plural, =0 {aktualizací} =1 {aktualizaci} other {aktualizací} }.", - "participationSummaryCoverageBetterStats": "Vaše pokrytí bylo {userCoverage}%, což je lepší než průměrný předpovídač v této otázce ({averageCoverage}%).", - "participationSummaryCoverageWorseStats": "Vaše pokrytí bylo {userCoverage}%, což je horší než průměrný předpovídač v této otázce ({averageCoverage}%).", + "participationSummaryCoverageBetterStats": "Vaše pokrytí bylo {userCoverage}%, což je lepší než průměrný předpovídač v této otázce ({averageCoverage}%).", + "participationSummaryCoverageWorseStats": "Vaše pokrytí bylo {userCoverage}%, což je horší než průměrný předpovídač v této otázce ({averageCoverage}%).", "peer": "Vrstevník", "bothPeerAndBaseline": "jak vrstevnické, tak základní", "scores": "Skóre", @@ -2081,5 +2081,6 @@ "tournamentUnfollowModalSubmit": "Zrušit sledování", "switchBackToSlidersHint": "přepněte zpět na posuvníky pro plynulé přizpůsobení", "view": "Zobrazit", + "maxAttainableCoverageExplanation": "Otázky, které se vyřeší dříve, mají své skóre a pokrytí upravené, aby se zachovala správnost skóre. Více se dozvíte zde.", "thousandsOfOpenQuestions": "20 000+ otevřených otázek" } diff --git a/front_end/messages/en.json b/front_end/messages/en.json index 62c2f0a2ec..d5b2ad4b83 100644 --- a/front_end/messages/en.json +++ b/front_end/messages/en.json @@ -242,6 +242,7 @@ "communityRelativeLegacyScore": "Community Relative Legacy Score", "communityRelativeLegacyArchivedScore": "Community Relative Legacy Archived Score", "myCoverage": "My coverage", + "maxAttainableCoverageExplanation": "Questions that resolve early have their scores and coverage truncated to preserve score properness. Learn more here.", "myWeightedCoverage": "My weighted coverage", "questionWillOpenAt": "This question will open {{date}}.", "questionNotOpenYet": "This question is not yet open for predictions.", @@ -1567,8 +1568,8 @@ "participations": "Participations", "participationSummary": "Participation Summary", "participationSummaryPredictionNrStats": "You made {userUpdates} {userUpdates, plural, =0 {updates} =1 {update} other {updates} } vs. a community average of {communityUpdates} {communityUpdates, plural, =0 {updates} =1 {update} other {updates} }.", - "participationSummaryCoverageBetterStats": "Your coverage was {userCoverage}%, better than the average forecaster on this question ({averageCoverage}%).", - "participationSummaryCoverageWorseStats": "Your coverage was {userCoverage}%, worse than the average forecaster on this question ({averageCoverage}%).", + "participationSummaryCoverageBetterStats": "Your coverage was {userCoverage}%, better than the average forecaster on this question ({averageCoverage}%).", + "participationSummaryCoverageWorseStats": "Your coverage was {userCoverage}%, worse than the average forecaster on this question ({averageCoverage}%).", "participationSummaryScoreOutperformance": "Congrats, you outperformed the Community Prediction in {scoreTypes}.", "peer": "Peer", "bothPeerAndBaseline": "both Peer and Baseline Scores", diff --git a/front_end/messages/es.json b/front_end/messages/es.json index 141ecec500..5d887d3f7f 100644 --- a/front_end/messages/es.json +++ b/front_end/messages/es.json @@ -1683,8 +1683,8 @@ "resolutionScores": "Puntajes de Resolución", "participationSummary": "Resumen de Participación", "participationSummaryPredictionNrStats": "Hiciste {userUpdates} {userUpdates, plural, =0 {actualizaciones} =1 {actualización} other {actualizaciones} } versus un promedio comunitario de {communityUpdates} {communityUpdates, plural, =0 {actualizaciones} =1 {actualización} other {actualizaciones} }.", - "participationSummaryCoverageBetterStats": "Tu cobertura fue del {userCoverage}%, mejor que el promedio de pronosticadores en esta pregunta ({averageCoverage}%).", - "participationSummaryCoverageWorseStats": "Tu cobertura fue del {userCoverage}%, peor que el promedio de pronosticadores en esta pregunta ({averageCoverage}%).", + "participationSummaryCoverageBetterStats": "Tu cobertura fue del {userCoverage}%, mejor que el promedio de pronosticadores en esta pregunta ({averageCoverage}%).", + "participationSummaryCoverageWorseStats": "Tu cobertura fue del {userCoverage}%, peor que el promedio de pronosticadores en esta pregunta ({averageCoverage}%).", "peer": "Pares", "bothPeerAndBaseline": "tanto en Pares como en Referencia", "scores": "Puntuaciones", @@ -2081,5 +2081,6 @@ "tournamentUnfollowModalSubmit": "Dejar de Seguir", "switchBackToSlidersHint": "vuelve a los deslizadores para un ajuste suave", "view": "Ver", + "maxAttainableCoverageExplanation": "Las preguntas que se resuelven pronto tienen sus puntuaciones y cobertura truncadas para preservar la propiedad adecuada de la puntuación. Aprende más aquí.", "thousandsOfOpenQuestions": "20,000+ preguntas abiertas" } diff --git a/front_end/messages/pt.json b/front_end/messages/pt.json index 86c24baee1..b9ec186834 100644 --- a/front_end/messages/pt.json +++ b/front_end/messages/pt.json @@ -1681,8 +1681,8 @@ "resolutionScores": "Pontuações de Resolução", "participationSummary": "Resumo de Participação", "participationSummaryPredictionNrStats": "Você fez {userUpdates} {userUpdates, plural, =0 {atualizações} =1 {atualização} other {atualizações} } vs. uma média da comunidade de {communityUpdates} {communityUpdates, plural, =0 {atualizações} =1 {atualização} other {atualizações} }.", - "participationSummaryCoverageBetterStats": "Sua cobertura foi de {userCoverage}%, melhor que a do previsor médio nesta pergunta ({averageCoverage}%).", - "participationSummaryCoverageWorseStats": "Sua cobertura foi de {userCoverage}%, pior que a do previsor médio nesta pergunta ({averageCoverage}%).", + "participationSummaryCoverageBetterStats": "Sua cobertura foi de {userCoverage}%, melhor que a do previsor médio nesta pergunta ({averageCoverage}%).", + "participationSummaryCoverageWorseStats": "Sua cobertura foi de {userCoverage}%, pior que a do previsor médio nesta pergunta ({averageCoverage}%).", "peer": "Par", "bothPeerAndBaseline": "tanto Par quanto Referência", "scores": "Pontuações", @@ -2079,5 +2079,6 @@ "tournamentUnfollowModalSubmit": "Deixar de Seguir", "switchBackToSlidersHint": "volte para os controles deslizantes para um ajuste suave", "view": "Visualizar", + "maxAttainableCoverageExplanation": "Questões que são resolvidas cedo têm suas pontuações e coberturas truncadas para preservar a correção da pontuação. Saiba mais aqui.", "thousandsOfOpenQuestions": "20.000+ perguntas abertas" } diff --git a/front_end/messages/zh-TW.json b/front_end/messages/zh-TW.json index 904b23721d..25e6793b1d 100644 --- a/front_end/messages/zh-TW.json +++ b/front_end/messages/zh-TW.json @@ -1680,8 +1680,8 @@ "resolutionScores": "決議分數", "participationSummary": "參與總結", "participationSummaryPredictionNrStats": "您做出了 {userUpdates} {userUpdates, plural, =0 {次更新} =1 {次更新} other {次更新} },而社群平均為 {communityUpdates} {communityUpdates, plural, =0 {次更新} =1 {次更新} other {次更新} }。", - "participationSummaryCoverageBetterStats": "您的涵蓋率是 {userCoverage}%,比該問題的平均預測者更好({averageCoverage}%)。", - "participationSummaryCoverageWorseStats": "您的涵蓋率是 {userCoverage}%,比該問題的平均預測者更差({averageCoverage}%)。", + "participationSummaryCoverageBetterStats": "您的涵蓋率是 {userCoverage}%,比該問題的平均預測者更好({averageCoverage}%)。", + "participationSummaryCoverageWorseStats": "您的涵蓋率是 {userCoverage}%,比該問題的平均預測者更差({averageCoverage}%)。", "peer": "同行", "bothPeerAndBaseline": "同行和基準", "scores": "分數", @@ -2078,5 +2078,6 @@ "tournamentUnfollowModalSubmit": "取消關注", "switchBackToSlidersHint": "切換回滑桿以平滑調整", "view": "檢視", + "maxAttainableCoverageExplanation": "為了保持分數的正確性,早期解決的問題其分數和覆蓋率會被截斷。在這裡了解更多。", "thousandsOfOpenQuestions": "20,000+ 開放問題" } diff --git a/front_end/messages/zh.json b/front_end/messages/zh.json index 1713c9e194..816ad4ac58 100644 --- a/front_end/messages/zh.json +++ b/front_end/messages/zh.json @@ -1685,8 +1685,8 @@ "resolutionScores": "解答分数", "participationSummary": "参与摘要", "participationSummaryPredictionNrStats": "您做出了 {userUpdates} {userUpdates, plural, =0 {次更新} =1 {次更新} other {次更新} },社区平均为 {communityUpdates} {communityUpdates, plural, =0 {次更新} =1 {次更新} other {次更新} }。", - "participationSummaryCoverageBetterStats": "您的覆盖率为 {userCoverage}%,优于该问题上的平均预测者({averageCoverage}%)。", - "participationSummaryCoverageWorseStats": "您的覆盖率为 {userCoverage}%,逊于该问题上的平均预测者({averageCoverage}%)。", + "participationSummaryCoverageBetterStats": "您的覆盖率为 {userCoverage}%,优于该问题上的平均预测者({averageCoverage}%)。", + "participationSummaryCoverageWorseStats": "您的覆盖率为 {userCoverage}%,逊于该问题上的平均预测者({averageCoverage}%)。", "peer": "同行", "bothPeerAndBaseline": "同行和基准", "scores": "得分", @@ -2083,5 +2083,6 @@ "tournamentUnfollowModalSubmit": "取消关注", "switchBackToSlidersHint": "切回滑块以进行更精细的调整", "view": "查看", + "maxAttainableCoverageExplanation": "为确保分数的准确性,提前解决的问题,其分数和覆盖率会被截断。在此了解更多信息。", "thousandsOfOpenQuestions": "20,000+ 开放问题" } diff --git a/front_end/src/app/(main)/questions/[id]/components/post_score_data/additional_scores_table.tsx b/front_end/src/app/(main)/questions/[id]/components/post_score_data/additional_scores_table.tsx index 0882500957..86e7e6621f 100644 --- a/front_end/src/app/(main)/questions/[id]/components/post_score_data/additional_scores_table.tsx +++ b/front_end/src/app/(main)/questions/[id]/components/post_score_data/additional_scores_table.tsx @@ -1,8 +1,11 @@ +import { faCircleInfo } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { isNil } from "lodash"; import { useTranslations } from "next-intl"; -import React, { FC } from "react"; +import React, { FC, ReactNode } from "react"; import SectionToggle from "@/components/ui/section_toggle"; +import Tooltip from "@/components/ui/tooltip"; import { QuestionWithForecasts, ScoreData } from "@/types/question"; import { TranslationKey } from "@/types/translations"; import cn from "@/utils/core/cn"; @@ -14,14 +17,16 @@ type Props = { variant?: Variant; }; +type ScoreRow = { label: string; value: string; valueSuffix?: ReactNode }; + const ScoreTable: FC<{ - rows: { label: string; value: string }[]; + rows: ScoreRow[]; className?: string; variant?: Variant; -}> = ({ rows, className, variant = "auto" }) => ( +}> = ({ rows, className }) => (
@@ -30,23 +35,12 @@ const ScoreTable: FC<{ key={index} className="flex items-center border-b border-gray-300 px-4 py-3 last:border-b-0 dark:border-gray-300-dark" > - + {row.label} - + {row.value} + {row.valueSuffix}
))} @@ -77,6 +71,21 @@ const buildScoreLabelKey = ( return (prefix + toCamel(key) + suffix) as TranslationKey; }; +/** + * Returns the max attainable peer coverage (0–1) for a question that resolved + * before its scheduled close time, or null if not applicable. + */ +const getMaxCoverage = (question: QuestionWithForecasts): number | null => { + const { open_time, actual_close_time, scheduled_close_time } = question; + if (!open_time || !actual_close_time || !scheduled_close_time) return null; + const open = new Date(open_time).getTime(); + const actualClose = new Date(actual_close_time).getTime(); + const scheduledClose = new Date(scheduled_close_time).getTime(); + const totalDuration = scheduledClose - open; + if (totalDuration <= 0) return null; + return (actualClose - open) / totalDuration; +}; + export const AdditionalScoresTable: FC = ({ question, separateCoverage, @@ -107,8 +116,39 @@ export const AdditionalScoresTable: FC = ({ "weighted_coverage", ]; - const coverageRows: { label: string; value: string }[] = []; - const otherRows: { label: string; value: string }[] = []; + // Peer coverage uses scheduled_close_time as total duration, so early + // resolution reduces the max attainable coverage. + const maxCoverage = getMaxCoverage(question); + const tooltipContent = t.rich("maxAttainableCoverageExplanation", { + link: (chunks) => ( + + {chunks} + + ), + }); + let maxCoverageValueSuffix: ReactNode; + if (maxCoverage !== null) { + maxCoverageValueSuffix = ( + + + (max. {(maxCoverage * 100).toFixed(1)}% + + ) + + + ); + } + + const coverageRows: ScoreRow[] = []; + const otherRows: ScoreRow[] = []; for (const key of scoreKeys) { if (key === peerKey || key === baselineKey) continue; @@ -126,6 +166,8 @@ export const AdditionalScoresTable: FC = ({ targetRows.push({ label: t(buildScoreLabelKey(key, "user")), value: formattedValue, + // Only peer coverage (key === "coverage") is affected by early resolution + ...(key === "coverage" ? { valueSuffix: maxCoverageValueSuffix } : {}), }); } diff --git a/front_end/src/app/(main)/questions/[id]/components/post_score_data/participation_summary.tsx b/front_end/src/app/(main)/questions/[id]/components/post_score_data/participation_summary.tsx index a858fdf0d3..096e4948b3 100644 --- a/front_end/src/app/(main)/questions/[id]/components/post_score_data/participation_summary.tsx +++ b/front_end/src/app/(main)/questions/[id]/components/post_score_data/participation_summary.tsx @@ -1,10 +1,15 @@ import { faClock } from "@fortawesome/free-regular-svg-icons"; -import { faFire, faRepeat } from "@fortawesome/free-solid-svg-icons"; +import { + faCircleInfo, + faFire, + faRepeat, +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { isNil } from "lodash"; import { useTranslations } from "next-intl"; import React, { PropsWithChildren, ReactNode } from "react"; +import Tooltip from "@/components/ui/tooltip"; import { QuestionWithForecasts } from "@/types/question"; import cn from "@/utils/core/cn"; @@ -34,6 +39,21 @@ type Props = { itemClassName?: string; }; +/** + * Returns the max attainable peer coverage (0–1) for a question that resolved + * before its scheduled close time, or null if not applicable. + */ +const getMaxCoverage = (question: QuestionWithForecasts): number | null => { + const { open_time, actual_close_time, scheduled_close_time } = question; + if (!open_time || !actual_close_time || !scheduled_close_time) return null; + const open = new Date(open_time).getTime(); + const actualClose = new Date(actual_close_time).getTime(); + const scheduledClose = new Date(scheduled_close_time).getTime(); + const totalDuration = scheduledClose - open; + if (totalDuration <= 0) return null; + return (actualClose - open) / totalDuration; +}; + export const ParticipationSummary: React.FC = ({ question, forecastsCount, @@ -91,6 +111,37 @@ export const ParticipationSummary: React.FC = ({ ); + // Only peer coverage (non-spot) is affected by early resolution. + const maxCoverage = isSpot ? null : getMaxCoverage(question); + const coverageTooltipContent = t.rich("maxAttainableCoverageExplanation", { + link: (chunks) => ( + + {chunks} + + ), + }); + const richMaxCoverageDisplay = (_chunks: ReactNode) => { + if (maxCoverage === null) return null; + return ( + + + {" (max. "} + {Math.round(maxCoverage * 100)}% + + {")"} + + + ); + }; + return (
= ({ strong: richStrong, userCoverage: Math.round(userCoverage * 100), averageCoverage: Math.round(averageCoverage * 100), + maxCoverageDisplay: richMaxCoverageDisplay, } )} diff --git a/front_end/src/components/question/score_card.tsx b/front_end/src/components/question/score_card.tsx index 5c9bfee036..49208678e0 100644 --- a/front_end/src/components/question/score_card.tsx +++ b/front_end/src/components/question/score_card.tsx @@ -466,8 +466,6 @@ export default function ScoreCard({ const t = useTranslations(); const isSpot = type.includes("spot"); - console.log("isSpot", isSpot, type); - if (type.includes("peer")) { return (