Show number of answers and community importance on prompts

This commit is contained in:
MartinBraquet
2026-03-09 19:24:32 +01:00
parent 5819f08aec
commit 6f45c03a29
16 changed files with 124 additions and 85 deletions

View File

@@ -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<Row<'compatibility_prompts'> & {score: number}>(
const questions = await pg.manyOrNone<QuestionWithStats>(
`
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',

View File

@@ -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',
),
)
})

View File

@@ -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',

7
common/src/api/types.ts Normal file
View File

@@ -0,0 +1,7 @@
import {Row} from 'common/supabase/utils'
export type QuestionWithStats = Omit<Row<'compatibility_prompts'>, 'community_importance_score'> & {
answer_count: number
score: number
community_importance_percent: number
}

View File

@@ -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: {
<CreateCompatibilityModalContent afterAddQuestion={afterAddQuestion} setOpen={setOpen} />
) : (
<AnswerCompatibilityQuestionContent
compatibilityQuestion={dbQuestion as QuestionWithCountType}
compatibilityQuestion={dbQuestion as QuestionWithStats}
user={user}
onSubmit={() => {
// setOpen(false)

View File

@@ -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 = () => {

View File

@@ -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: {
<Col className="min-h-0 w-full gap-4">
<Col className="gap-1 shrink-0">
<Row>
{shortenedPopularity && (
<Tooltip
text={t(
'answers.content.people_answered',
'{count} people have answered this question',
{count: String(shortenedPopularity)},
)}
>
<Row className="text-ink-500 select-none items-center text-sm">
{shortenedPopularity}
<UserIcon className="h-4 w-4" />
</Row>
</Tooltip>
)}
{isFinite(index!) && isFinite(total!) && (
<span className={'ml-16 text-sm'}>
<span className={'text-sm'}>
<span className="text-ink-600 font-semibold">{index! + 1}</span> / {total}
</span>
)}
@@ -187,6 +173,28 @@ export function AnswerCompatibilityQuestionContent(props: {
/>
)}
</Row>
<Row className={''}>
{shortenedPopularity && (
<Tooltip
text={t(
'answers.content.people_answered',
'{count} people have answered this question',
{count: String(shortenedPopularity)},
)}
>
<Row className="select-none items-center text-sm guidance">
{shortenedPopularity}
<UserIcon className="h-4 w-4" />
</Row>
</Tooltip>
)}
{isFinite(compatibilityQuestion.community_importance_percent) && (
<span className={'text-sm ml-auto guidance'}>
Community Importance: {Math.round(compatibilityQuestion.community_importance_percent)}
%
</span>
)}
</Row>
<div data-testid="compatibility-question">{compatibilityQuestion.question}</div>
</Col>
<Col className={clsx(SCROLLABLE_MODAL_CLASS, 'w-full gap-4 flex-1 min-h-0 pr-2')}>

View File

@@ -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

View File

@@ -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<number>,
answeredQuestionIds: Set<number>,
) {
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<boolean>(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 (
<Col
data-testid="profile-compatibility-section"
@@ -530,6 +543,29 @@ export function CompatibilityAnswerBlock(props: {
)}
{/*{question.importance_score == 0 && <div className="text-ink-500 text-sm">Core Question</div>}*/}
</Col>
{showCommunityInfo && (
<Row className={'mt-[-20px]'}>
{shortenedPopularity && (
<Tooltip
text={t(
'answers.content.people_answered',
'{count} people have answered this question',
{count: String(shortenedPopularity)},
)}
>
<Row className="select-none items-center text-sm guidance">
{shortenedPopularity}
<UserIcon className="h-4 w-4" />
</Row>
</Tooltip>
)}
{isFinite(question.community_importance_percent) && (
<span className={'text-sm ml-auto guidance'}>
Community Importance: {Math.round(question.community_importance_percent)}%
</span>
)}
</Row>
)}
<Modal open={editOpen} setOpen={setEditOpen}>
<Col className={MODAL_CLASS}>
<AnswerCompatibilityQuestionContent
@@ -551,7 +587,7 @@ export function CompatibilityAnswerBlock(props: {
}
function CompatibilityDisplay(props: {
question: QuestionWithCountType
question: QuestionWithStats
profile1?: Profile
profile2: Profile
answer1: rowFor<'compatibility_answers'>

View File

@@ -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<QuestionWithCountType | null>(
usePersistentInMemoryState<QuestionWithStats | null>(
null,
`selected-added-question-${user.id}}`,
)
const [expandedQuestion, setExpandedQuestion] =
usePersistentInMemoryState<QuestionWithCountType | null>(
usePersistentInMemoryState<QuestionWithStats | null>(
null,
`selected-expanded-question-${user.id}}`,
)

View File

@@ -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

View File

@@ -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
}) {

View File

@@ -21,6 +21,7 @@ export function ProfileAnswers(props: {
profile={profile}
fromSignup={fromSignup}
fromProfilePage={fromProfilePage}
showCommunityInfo={false}
/>
{/*<FreeResponseDisplay*/}
{/* isCurrentUser={isCurrentUser}*/}

View File

@@ -80,8 +80,8 @@ export function compareBySort(a: any, b: any, sort: CompatibilitySort) {
if (sort === 'random') {
return Math.random() - 0.5
} else if (sort === 'community_importance') {
const rateA = (a?.community_importance_score ?? 0) / Math.max(a?.answer_count ?? 1, 1)
const rateB = (b?.community_importance_score ?? 0) / Math.max(b?.answer_count ?? 1, 1)
const rateA = a?.community_importance_percent ?? 0
const rateB = b?.community_importance_percent ?? 0
return rateB - rateA
} else if (sort === 'most_answered') {
return (b?.answer_count ?? 0) - (a?.answer_count ?? 0)

View File

@@ -1,3 +1,4 @@
import {QuestionWithStats} from 'common/api/types'
import {Row} from 'common/supabase/utils'
import {sortBy} from 'lodash'
import {useEffect, useState} from 'react'
@@ -75,12 +76,6 @@ export const useUserCompatibilityAnswers = (userId: string | undefined) => {
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<any>(
[],
@@ -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)

View File

@@ -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 () => {