From 6f45c03a29cd5c199c03add256ac57b217dc41ca Mon Sep 17 00:00:00 2001 From: MartinBraquet Date: Mon, 9 Mar 2026 19:24:32 +0100 Subject: [PATCH] Show number of answers and community importance on prompts --- .../api/src/get-compatibililty-questions.ts | 34 ++++++------ .../get-compatibility-questions.unit.test.ts | 2 +- common/src/api/schema.ts | 9 ++-- common/src/api/types.ts | 7 +++ .../add-compatibility-question-button.tsx | 4 +- .../answer-compatibility-question-button.tsx | 10 ++-- .../answer-compatibility-question-content.tsx | 42 +++++++++------ .../compatibility-question-preferred-list.tsx | 6 +-- .../compatibility-questions-display.tsx | 54 +++++++++++++++---- .../answers/free-response-add-question.tsx | 10 ++-- .../answers/free-response-display.tsx | 9 ++-- .../answers/other-profile-answers.tsx | 4 +- web/components/answers/profile-answers.tsx | 1 + web/components/compatibility/sort-widget.tsx | 4 +- web/hooks/use-questions.ts | 11 ++-- web/lib/supabase/questions.ts | 2 +- 16 files changed, 124 insertions(+), 85 deletions(-) create mode 100644 common/src/api/types.ts diff --git a/backend/api/src/get-compatibililty-questions.ts b/backend/api/src/get-compatibililty-questions.ts index d558e7b9..2345523e 100644 --- a/backend/api/src/get-compatibililty-questions.ts +++ b/backend/api/src/get-compatibililty-questions.ts @@ -1,5 +1,5 @@ import {type APIHandler} from 'api/helpers/endpoint' -import {Row} from 'common/supabase/utils' +import {QuestionWithStats} from 'common/api/types' import {createSupabaseDirectClient} from 'shared/supabase/init' export const getCompatibilityQuestions: APIHandler<'get-compatibility-questions'> = async ( @@ -31,7 +31,7 @@ export const getCompatibilityQuestions: APIHandler<'get-compatibility-questions' params.push(`%${keyword}%`) } - const questions = await pg.manyOrNone & {score: number}>( + const questions = await pg.manyOrNone( ` SELECT cp.id, cp.answer_type, @@ -39,25 +39,23 @@ export const getCompatibilityQuestions: APIHandler<'get-compatibility-questions' cp.created_time, cp.creator_id, cp.category, - cp.community_importance_score * (cp.answer_count::float / (cp.answer_count + 20)) AS community_importance_score, - cp.answer_count, - - -- locale-aware fields - COALESCE(cpt.question, cp.question) AS question, + COALESCE(cpt.question, cp.question) AS question, COALESCE(cpt.multiple_choice_options, cp.multiple_choice_options) AS multiple_choice_options, - AVG( - POWER( - ca.importance + 1 + - CASE WHEN ca.explanation IS NULL THEN 1 ELSE 0 END, - 2 - ) - ) AS score + cp.answer_count, + CASE + WHEN cp.answer_count IS NULL OR cp.answer_count = 0 THEN 0 + --- community_importance_score is a weighted sum: max val is 2 * answer_count if everyone marks at the highest level of importance + --- So we divide by 2 * answer_count to ensure it's between and 0 and 1 + --- We damp by 20 to ensure questions with few responders don't get a high score + --- The square root is to spread the percent of all questions, since in the early days they don't get higher than 50%. + --- It does not impact ranking though. + --- TODO: remove the square root when we get more answers + ELSE SQRT(cp.community_importance_score::float / (cp.answer_count + 20) / 2) * 100 + END AS community_importance_percent, + 0 AS score --- update later if needed FROM compatibility_prompts cp - LEFT JOIN compatibility_answers ca - ON cp.id = ca.question_id - LEFT JOIN compatibility_prompts_translations cpt ON cp.id = cpt.question_id AND cpt.locale = $1 @@ -75,7 +73,7 @@ export const getCompatibilityQuestions: APIHandler<'get-compatibility-questions' params, ) - // debug({questions}) + // console.debug(questions.find((q) => q.id === 275)) return { status: 'success', diff --git a/backend/api/tests/unit/get-compatibility-questions.unit.test.ts b/backend/api/tests/unit/get-compatibility-questions.unit.test.ts index 48aff953..fd311f64 100644 --- a/backend/api/tests/unit/get-compatibility-questions.unit.test.ts +++ b/backend/api/tests/unit/get-compatibility-questions.unit.test.ts @@ -50,7 +50,7 @@ describe('getCompatibilityQuestions', () => { expect(sql).toEqual(expect.stringContaining('cp.answer_count')) expect(sql).toEqual( expect.stringContaining( - 'cp.community_importance_score * (cp.answer_count::float / (cp.answer_count + 20)) AS community_importance_score', + 'cp.community_importance_score * (cp.answer_count::float / (cp.answer_count + 20)) AS community_importance_percent', ), ) }) diff --git a/common/src/api/schema.ts b/common/src/api/schema.ts index c9d0565e..7fa87d3d 100644 --- a/common/src/api/schema.ts +++ b/common/src/api/schema.ts @@ -1,3 +1,4 @@ +import {QuestionWithStats} from 'common/api/types' // mqp: very unscientific, just balancing our willingness to accept load import { arraybeSchema, baseProfilesSchema, @@ -19,7 +20,7 @@ import {arrify} from 'common/util/array' import {z} from 'zod' import {LikeData, ShipData} from './profile-types' -import {FullUser, HiddenProfile} from './user-types' // mqp: very unscientific, just balancing our willingness to accept load +import {FullUser, HiddenProfile} from './user-types' // mqp: very unscientific, just balancing our willingness to accept load // with user willingness to put up with stale data @@ -574,11 +575,7 @@ export const API = (_apiTypeCheck = { }), returns: {} as { status: 'success' - questions: (Row<'compatibility_prompts'> & { - answer_count: number - score: number - community_importance_score?: number - })[] + questions: QuestionWithStats[] }, summary: 'Retrieve compatibility questions and stats', tag: 'Compatibility', diff --git a/common/src/api/types.ts b/common/src/api/types.ts new file mode 100644 index 00000000..94b35a04 --- /dev/null +++ b/common/src/api/types.ts @@ -0,0 +1,7 @@ +import {Row} from 'common/supabase/utils' + +export type QuestionWithStats = Omit, 'community_importance_score'> & { + answer_count: number + score: number + community_importance_percent: number +} diff --git a/web/components/answers/add-compatibility-question-button.tsx b/web/components/answers/add-compatibility-question-button.tsx index 407ff3b5..c50e2d71 100644 --- a/web/components/answers/add-compatibility-question-button.tsx +++ b/web/components/answers/add-compatibility-question-button.tsx @@ -1,4 +1,5 @@ import {PlusIcon, XMarkIcon} from '@heroicons/react/24/outline' +import {QuestionWithStats} from 'common/api/types' import {MAX_ANSWER_LENGTH} from 'common/envs/constants' import {debug} from 'common/logger' import {MAX_COMPATIBILITY_QUESTION_LENGTH} from 'common/profiles/constants' @@ -13,7 +14,6 @@ import {Modal, MODAL_CLASS} from 'web/components/layout/modal' import {Row} from 'web/components/layout/row' import {ExpandingInput} from 'web/components/widgets/expanding-input' import {useEvent} from 'web/hooks/use-event' -import {QuestionWithCountType} from 'web/hooks/use-questions' import {useUser} from 'web/hooks/use-user' import {api} from 'web/lib/api' import {useT} from 'web/lib/locale' @@ -64,7 +64,7 @@ function AddCompatibilityQuestionModal(props: { ) : ( { // setOpen(false) diff --git a/web/components/answers/answer-compatibility-question-button.tsx b/web/components/answers/answer-compatibility-question-button.tsx index fd2b0fc2..513547db 100644 --- a/web/components/answers/answer-compatibility-question-button.tsx +++ b/web/components/answers/answer-compatibility-question-button.tsx @@ -1,4 +1,5 @@ import clsx from 'clsx' +import {QuestionWithStats} from 'common/api/types' import {User} from 'common/user' import Link from 'next/link' import router from 'next/router' @@ -8,14 +9,13 @@ import {Button} from 'web/components/buttons/button' import {compareBySort, CompatibilitySort} from 'web/components/compatibility/sort-widget' import {Col} from 'web/components/layout/col' import {Modal, MODAL_CLASS, SCROLLABLE_MODAL_CLASS} from 'web/components/layout/modal' -import {QuestionWithCountType} from 'web/hooks/use-questions' import {useT} from 'web/lib/locale' import {AnswerCompatibilityQuestionContent} from './answer-compatibility-question-content' export function AnswerCompatibilityQuestionButton(props: { user: User | null | undefined - otherQuestions: QuestionWithCountType[] + otherQuestions: QuestionWithStats[] refreshCompatibilityAll: () => void fromSignup?: boolean size?: 'sm' | 'md' @@ -81,7 +81,7 @@ export function CompatibilityPageButton() { export function AnswerSkippedCompatibilityQuestionsButton(props: { user: User | null | undefined - skippedQuestions: QuestionWithCountType[] + skippedQuestions: QuestionWithStats[] refreshCompatibilityAll: () => void fromSignup?: boolean }) { @@ -157,7 +157,7 @@ function AnswerCompatibilityQuestionModal(props: { open: boolean setOpen: (open: boolean) => void user: User - otherQuestions: QuestionWithCountType[] + otherQuestions: QuestionWithStats[] refreshCompatibilityAll: () => void onClose?: () => void fromSignup?: boolean @@ -175,7 +175,7 @@ function AnswerCompatibilityQuestionModal(props: { const sortedQuestions = useMemo(() => { return [...otherQuestions].sort((a, b) => { return compareBySort(a, b, sort) - }) as QuestionWithCountType[] + }) as QuestionWithStats[] }, [otherQuestions, sort]) const handleStartQuestions = () => { diff --git a/web/components/answers/answer-compatibility-question-content.tsx b/web/components/answers/answer-compatibility-question-content.tsx index d3a150fe..d1ce9d03 100644 --- a/web/components/answers/answer-compatibility-question-content.tsx +++ b/web/components/answers/answer-compatibility-question-content.tsx @@ -1,6 +1,7 @@ import {RadioGroup} from '@headlessui/react' import {UserIcon} from '@heroicons/react/24/solid' import clsx from 'clsx' +import {QuestionWithStats} from 'common/api/types' import {Row as rowFor} from 'common/supabase/utils' import {User} from 'common/user' import {shortenNumber} from 'common/util/format' @@ -15,7 +16,6 @@ import {Row} from 'web/components/layout/row' import {ExpandingInput} from 'web/components/widgets/expanding-input' import {RadioToggleGroup} from 'web/components/widgets/radio-toggle-group' import {Tooltip} from 'web/components/widgets/tooltip' -import {QuestionWithCountType} from 'web/hooks/use-questions' import {api} from 'web/lib/api' import {useT} from 'web/lib/locale' import {track} from 'web/lib/service/analytics' @@ -102,7 +102,7 @@ export function getEmptyAnswer(userId: string, questionId: number) { } export function AnswerCompatibilityQuestionContent(props: { - compatibilityQuestion: QuestionWithCountType + compatibilityQuestion: QuestionWithStats user: User index?: number total?: number @@ -158,22 +158,8 @@ export function AnswerCompatibilityQuestionContent(props: { - {shortenedPopularity && ( - - - {shortenedPopularity} - - - - )} {isFinite(index!) && isFinite(total!) && ( - + {index! + 1} / {total} )} @@ -187,6 +173,28 @@ export function AnswerCompatibilityQuestionContent(props: { /> )} + + {shortenedPopularity && ( + + + {shortenedPopularity} + + + + )} + {isFinite(compatibilityQuestion.community_importance_percent) && ( + + Community Importance: {Math.round(compatibilityQuestion.community_importance_percent)} + % + + )} +
{compatibilityQuestion.question}
diff --git a/web/components/answers/compatibility-question-preferred-list.tsx b/web/components/answers/compatibility-question-preferred-list.tsx index 9b1b5a80..34f74722 100644 --- a/web/components/answers/compatibility-question-preferred-list.tsx +++ b/web/components/answers/compatibility-question-preferred-list.tsx @@ -1,15 +1,15 @@ import {CheckCircleIcon, XCircleIcon} from '@heroicons/react/24/outline' import clsx from 'clsx' +import {QuestionWithStats} from 'common/api/types' import {Row as rowFor} from 'common/supabase/utils' import {User} from 'common/user' import {Col} from 'web/components/layout/col' import {Row} from 'web/components/layout/row' import {shortenName} from 'web/components/widgets/user-link' -import {QuestionWithCountType} from 'web/hooks/use-questions' import {useT} from 'web/lib/locale' export function PreferredList(props: { - question: QuestionWithCountType + question: QuestionWithStats answer: rowFor<'compatibility_answers'> comparedAnswer: rowFor<'compatibility_answers'> comparedUser: User @@ -65,7 +65,7 @@ export function PreferredList(props: { } export function PreferredListNoComparison(props: { - question: QuestionWithCountType + question: QuestionWithStats answer: rowFor<'compatibility_answers'> }) { const {question, answer} = props diff --git a/web/components/answers/compatibility-questions-display.tsx b/web/components/answers/compatibility-questions-display.tsx index 472fe8f8..cde08e4a 100644 --- a/web/components/answers/compatibility-questions-display.tsx +++ b/web/components/answers/compatibility-questions-display.tsx @@ -1,5 +1,7 @@ import {PencilIcon, TrashIcon} from '@heroicons/react/24/outline' +import {UserIcon} from '@heroicons/react/24/solid' import clsx from 'clsx' +import {QuestionWithStats} from 'common/api/types' import { getAnswerCompatibility, getScoredAnswerCompatibility, @@ -7,6 +9,7 @@ import { import {Profile} from 'common/profiles/profile' import {Row as rowFor} from 'common/supabase/utils' import {User} from 'common/user' +import {shortenNumber} from 'common/util/format' import {keyBy, partition, sortBy} from 'lodash' import {useEffect, useState} from 'react' import toast from 'react-hot-toast' @@ -24,13 +27,13 @@ import {CompatibleBadge} from 'web/components/widgets/compatible-badge' import {Input} from 'web/components/widgets/input' import {Linkify} from 'web/components/widgets/linkify' import {Pagination} from 'web/components/widgets/pagination' +import {Tooltip} from 'web/components/widgets/tooltip' import {shortenName} from 'web/components/widgets/user-link' import {useIsLooking} from 'web/hooks/use-is-looking' import {usePersistentInMemoryState} from 'web/hooks/use-persistent-in-memory-state' import {useProfile} from 'web/hooks/use-profile' import {useCompatibleProfiles} from 'web/hooks/use-profiles' import { - QuestionWithCountType, useCompatibilityQuestionsWithAnswerCount, useUserCompatibilityAnswers, } from 'web/hooks/use-questions' @@ -59,13 +62,13 @@ import {PreferredList, PreferredListNoComparison} from './compatibility-question const NUM_QUESTIONS_TO_SHOW = 8 export function separateQuestionsArray( - questions: QuestionWithCountType[], + questions: QuestionWithStats[], skippedAnswerQuestionIds: Set, answeredQuestionIds: Set, ) { - const skippedQuestions: QuestionWithCountType[] = [] - const answeredQuestions: QuestionWithCountType[] = [] - const otherQuestions: QuestionWithCountType[] = [] + const skippedQuestions: QuestionWithStats[] = [] + const answeredQuestions: QuestionWithStats[] = [] + const otherQuestions: QuestionWithStats[] = [] questions.forEach((q) => { if (skippedAnswerQuestionIds.has(q.id)) { @@ -86,8 +89,10 @@ export function CompatibilityQuestionsDisplay(props: { profile: Profile fromSignup?: boolean fromProfilePage?: Profile + showCommunityInfo?: boolean }) { - const {isCurrentUser, user, fromSignup, fromProfilePage, profile} = props + const {isCurrentUser, user, fromSignup, fromProfilePage, profile, showCommunityInfo} = props + const t = useT() const currentUser = useUser() @@ -274,6 +279,7 @@ export function CompatibilityQuestionsDisplay(props: { refreshCompatibilityAll={refreshCompatibilityAll} profile={profile} fromProfilePage={fromProfilePage} + showCommunityInfo={showCommunityInfo} /> ) })} @@ -318,13 +324,14 @@ export function CompatibilityQuestionsDisplay(props: { export function CompatibilityAnswerBlock(props: { answer?: rowFor<'compatibility_answers'> - yourQuestions: QuestionWithCountType[] - question?: QuestionWithCountType + yourQuestions: QuestionWithStats[] + question?: QuestionWithStats user: User isCurrentUser: boolean profile?: Profile refreshCompatibilityAll: () => void fromProfilePage?: Profile + showCommunityInfo?: boolean }) { const { answer, @@ -335,6 +342,9 @@ export function CompatibilityAnswerBlock(props: { refreshCompatibilityAll, fromProfilePage, } = props + + const showCommunityInfo = props.showCommunityInfo === undefined ? true : props.showCommunityInfo + const question = props.question || yourQuestions.find((q) => q.id === answer?.question_id) const [editOpen, setEditOpen] = useState(false) const currentUser = useUser() @@ -377,6 +387,9 @@ export function CompatibilityAnswerBlock(props: { const isAnswered = answer && answer.multiple_choice > -1 const isSkipped = answer && answer.importance == -1 + + const shortenedPopularity = question.answer_count ? shortenNumber(question.answer_count) : null + return ( Core Question}*/} + {showCommunityInfo && ( + + {shortenedPopularity && ( + + + {shortenedPopularity} + + + + )} + {isFinite(question.community_importance_percent) && ( + + Community Importance: {Math.round(question.community_importance_percent)}% + + )} + + )} diff --git a/web/components/answers/free-response-add-question.tsx b/web/components/answers/free-response-add-question.tsx index f901eaed..7c4d74a9 100644 --- a/web/components/answers/free-response-add-question.tsx +++ b/web/components/answers/free-response-add-question.tsx @@ -1,5 +1,6 @@ import {ArrowLeftIcon, PlusIcon} from '@heroicons/react/24/outline' import clsx from 'clsx' +import {QuestionWithStats} from 'common/api/types' import {User} from 'common/user' import {TbMessage} from 'react-icons/tb' import {Button} from 'web/components/buttons/button' @@ -7,7 +8,6 @@ import {Col} from 'web/components/layout/col' import {Modal, MODAL_CLASS, SCROLLABLE_MODAL_CLASS} from 'web/components/layout/modal' import {Row} from 'web/components/layout/row' import {usePersistentInMemoryState} from 'web/hooks/use-persistent-in-memory-state' -import {QuestionWithCountType} from 'web/hooks/use-questions' import {useT} from 'web/lib/locale' import {IndividualQuestionRow} from '../questions-form' @@ -15,7 +15,7 @@ import {OtherProfileAnswers} from './other-profile-answers' export function AddQuestionButton(props: { isFirstQuestion?: boolean - questions: QuestionWithCountType[] + questions: QuestionWithStats[] user: User refreshAnswers: () => void }) { @@ -44,20 +44,20 @@ export function AddQuestionButton(props: { function AddQuestionModal(props: { open: boolean setOpen: (open: boolean) => void - questions: QuestionWithCountType[] + questions: QuestionWithStats[] user: User refreshAnswers: () => void }) { const {open, setOpen, questions, user, refreshAnswers} = props const addableQuestions = questions.filter((q) => q.answer_type === 'free_response') const [selectedQuestion, setSelectedQuestion] = - usePersistentInMemoryState( + usePersistentInMemoryState( null, `selected-added-question-${user.id}}`, ) const [expandedQuestion, setExpandedQuestion] = - usePersistentInMemoryState( + usePersistentInMemoryState( null, `selected-expanded-question-${user.id}}`, ) diff --git a/web/components/answers/free-response-display.tsx b/web/components/answers/free-response-display.tsx index 96425f75..6f4c69bf 100644 --- a/web/components/answers/free-response-display.tsx +++ b/web/components/answers/free-response-display.tsx @@ -1,4 +1,5 @@ import {PencilIcon, XMarkIcon} from '@heroicons/react/24/outline' +import {QuestionWithStats} from 'common/api/types' import {Profile} from 'common/profiles/profile' import {Row as rowFor} from 'common/supabase/utils' import {User} from 'common/user' @@ -11,11 +12,7 @@ import {Modal, MODAL_CLASS, SCROLLABLE_MODAL_CLASS} from 'web/components/layout/ import {Row} from 'web/components/layout/row' import {Linkify} from 'web/components/widgets/linkify' import {shortenName} from 'web/components/widgets/user-link' -import { - QuestionWithCountType, - useFRQuestionsWithAnswerCount, - useUserAnswers, -} from 'web/hooks/use-questions' +import {useFRQuestionsWithAnswerCount, useUserAnswers} from 'web/hooks/use-questions' import {useT} from 'web/lib/locale' import {deleteAnswer} from 'web/lib/supabase/answers' @@ -91,7 +88,7 @@ export function FreeResponseDisplay(props: { function AnswerBlock(props: { answer: rowFor<'compatibility_answers_free'> - questions: QuestionWithCountType[] + questions: QuestionWithStats[] isCurrentUser: boolean user: User refreshAnswers: () => void diff --git a/web/components/answers/other-profile-answers.tsx b/web/components/answers/other-profile-answers.tsx index 97d8f33e..1ce6bc19 100644 --- a/web/components/answers/other-profile-answers.tsx +++ b/web/components/answers/other-profile-answers.tsx @@ -1,4 +1,5 @@ import clsx from 'clsx' +import {QuestionWithStats} from 'common/api/types' import {convertGender, Gender} from 'common/gender' import {User} from 'common/user' import {capitalize} from 'lodash' @@ -9,12 +10,11 @@ import {Linkify} from 'web/components/widgets/linkify' import {CompassLoadingIndicator} from 'web/components/widgets/loading-indicator' import {UserLink} from 'web/components/widgets/user-link' import {useOtherAnswers} from 'web/hooks/use-other-answers' -import {QuestionWithCountType} from 'web/hooks/use-questions' import {useT} from 'web/lib/locale' import {shortenedFromNow} from 'web/lib/util/shortenedFromNow' export function OtherProfileAnswers(props: { - question: QuestionWithCountType + question: QuestionWithStats user?: User className?: string }) { diff --git a/web/components/answers/profile-answers.tsx b/web/components/answers/profile-answers.tsx index 1cabb500..fac394fa 100644 --- a/web/components/answers/profile-answers.tsx +++ b/web/components/answers/profile-answers.tsx @@ -21,6 +21,7 @@ export function ProfileAnswers(props: { profile={profile} fromSignup={fromSignup} fromProfilePage={fromProfilePage} + showCommunityInfo={false} /> {/* { return {refreshCompatibilityAnswers, compatibilityAnswers} } -export type QuestionWithCountType = Row<'compatibility_prompts'> & { - answer_count: number - score: number - community_importance_score?: number -} - export const useFRQuestionsWithAnswerCount = () => { const [FRquestionsWithCount, setFRQuestionsWithCount] = usePersistentInMemoryState( [], @@ -93,14 +88,14 @@ export const useFRQuestionsWithAnswerCount = () => { }) }, []) - return FRquestionsWithCount as QuestionWithCountType[] + return FRquestionsWithCount as QuestionWithStats[] } export const useCompatibilityQuestionsWithAnswerCount = () => { const {locale} = useLocale() const firebaseUser = useFirebaseUser() const [compatibilityQuestions, setCompatibilityQuestions] = usePersistentInMemoryState< - QuestionWithCountType[] + QuestionWithStats[] >([], `compatibility-questions-with-count`) const [isLoading, setIsLoading] = useState(true) diff --git a/web/lib/supabase/questions.ts b/web/lib/supabase/questions.ts index f0d13b53..399eda48 100644 --- a/web/lib/supabase/questions.ts +++ b/web/lib/supabase/questions.ts @@ -7,7 +7,7 @@ export type QuestionWithAnswer = Question & { answer?: Row<'compatibility_answers'> answer_count: number score: number - community_importance_score?: number + community_importance_percent: number } export const getAllQuestions = async () => {