Prevent duplicate Free Response answers (#581)

* Prevent duplicate Free Response answers

* Address review comments
This commit is contained in:
Ben Congdon 2022-06-25 16:18:49 -07:00 committed by GitHub
parent b7cbd2a431
commit 5e768aa57c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 80 additions and 8 deletions

View File

@ -1,6 +1,7 @@
import clsx from 'clsx' import clsx from 'clsx'
import { useState } from 'react' import { useState } from 'react'
import Textarea from 'react-expanding-textarea' import Textarea from 'react-expanding-textarea'
import { findBestMatch } from 'string-similarity'
import { FreeResponseContract } from 'common/contract' import { FreeResponseContract } from 'common/contract'
import { BuyAmountInput } from '../amount-input' import { BuyAmountInput } from '../amount-input'
@ -23,6 +24,7 @@ import { firebaseLogin } from 'web/lib/firebase/users'
import { Bet } from 'common/bet' import { Bet } from 'common/bet'
import { MAX_ANSWER_LENGTH } from 'common/answer' import { MAX_ANSWER_LENGTH } from 'common/answer'
import { withTracking } from 'web/lib/service/analytics' import { withTracking } from 'web/lib/service/analytics'
import { lowerCase } from 'lodash'
export function CreateAnswerPanel(props: { contract: FreeResponseContract }) { export function CreateAnswerPanel(props: { contract: FreeResponseContract }) {
const { contract } = props const { contract } = props
@ -30,9 +32,15 @@ export function CreateAnswerPanel(props: { contract: FreeResponseContract }) {
const [text, setText] = useState('') const [text, setText] = useState('')
const [betAmount, setBetAmount] = useState<number | undefined>(10) const [betAmount, setBetAmount] = useState<number | undefined>(10)
const [amountError, setAmountError] = useState<string | undefined>() const [amountError, setAmountError] = useState<string | undefined>()
const [answerError, setAnswerError] = useState<string | undefined>()
const [possibleDuplicateAnswer, setPossibleDuplicateAnswer] = useState<
string | undefined
>()
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
const { answers } = contract
const canSubmit = text && betAmount && !amountError && !isSubmitting const canSubmit =
text && betAmount && !amountError && !isSubmitting && !answerError
const submitAnswer = async () => { const submitAnswer = async () => {
if (canSubmit) { if (canSubmit) {
@ -54,6 +62,36 @@ export function CreateAnswerPanel(props: { contract: FreeResponseContract }) {
} }
} }
const changeAnswer = (text: string) => {
setText(text)
const existingAnswer = answers.find(
(a) => lowerCase(a.text) === lowerCase(text)
)
if (existingAnswer) {
setAnswerError(
existingAnswer
? `"${existingAnswer.text}" already exists as an answer`
: ''
)
return
} else {
setAnswerError('')
}
if (answers.length && text) {
const matches = findBestMatch(
lowerCase(text),
answers.map((a) => lowerCase(a.text))
)
setPossibleDuplicateAnswer(
matches.bestMatch.rating > 0.8
? answers[matches.bestMatchIndex].text
: ''
)
}
}
const resultProb = getDpmOutcomeProbabilityAfterBet( const resultProb = getDpmOutcomeProbabilityAfterBet(
contract.totalShares, contract.totalShares,
'new', 'new',
@ -79,12 +117,21 @@ export function CreateAnswerPanel(props: { contract: FreeResponseContract }) {
<div className="mb-1">Add your answer</div> <div className="mb-1">Add your answer</div>
<Textarea <Textarea
value={text} value={text}
onChange={(e) => setText(e.target.value)} onChange={(e) => changeAnswer(e.target.value)}
className="textarea textarea-bordered w-full resize-none" className="textarea textarea-bordered w-full resize-none"
placeholder="Type your answer..." placeholder="Type your answer..."
rows={1} rows={1}
maxLength={MAX_ANSWER_LENGTH} maxLength={MAX_ANSWER_LENGTH}
/> />
{answerError ? (
<AnswerError key={1} level="error" text={answerError} />
) : possibleDuplicateAnswer ? (
<AnswerError
key={2}
level="warning"
text={`Did you mean to bet on "${possibleDuplicateAnswer}"?`}
/>
) : undefined}
<div /> <div />
<Col <Col
className={clsx( className={clsx(
@ -163,3 +210,21 @@ export function CreateAnswerPanel(props: { contract: FreeResponseContract }) {
</Col> </Col>
) )
} }
type answerErrorLevel = 'warning' | 'error'
const AnswerError = (props: { text: string; level: answerErrorLevel }) => {
const { text, level } = props
const colorClass =
{
error: 'text-red-500',
warning: 'text-orange-500',
}[level] ?? ''
return (
<div
className={`${colorClass} mb-2 mr-auto self-center whitespace-nowrap text-xs font-medium tracking-wide`}
>
{text}
</div>
)
}

View File

@ -41,7 +41,8 @@
"react-expanding-textarea": "2.3.5", "react-expanding-textarea": "2.3.5",
"react-hot-toast": "2.2.0", "react-hot-toast": "2.2.0",
"react-instantsearch-hooks-web": "6.24.1", "react-instantsearch-hooks-web": "6.24.1",
"react-query": "3.39.0" "react-query": "3.39.0",
"string-similarity": "^4.0.4"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/forms": "0.4.0", "@tailwindcss/forms": "0.4.0",
@ -50,6 +51,7 @@
"@types/lodash": "4.14.178", "@types/lodash": "4.14.178",
"@types/node": "16.11.11", "@types/node": "16.11.11",
"@types/react": "17.0.43", "@types/react": "17.0.43",
"@types/string-similarity": "^4.0.0",
"autoprefixer": "10.2.6", "autoprefixer": "10.2.6",
"concurrently": "6.5.1", "concurrently": "6.5.1",
"critters": "0.0.16", "critters": "0.0.16",

View File

@ -3148,6 +3148,11 @@
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@types/string-similarity@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/string-similarity/-/string-similarity-4.0.0.tgz#8cc03d5d1baad2b74530fe6c7d849d5768d391ad"
integrity sha512-dMS4S07fbtY1AILG/RhuwmptmzK1Ql8scmAebOTJ/8iBtK/KI17NwGwKzu1uipjj8Kk+3mfPxum56kKZE93mzQ==
"@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2", "@types/unist@^2.0.3": "@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2", "@types/unist@^2.0.3":
version "2.0.6" version "2.0.6"
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
@ -8026,11 +8031,6 @@ nanoid@^3.1.23, nanoid@^3.1.30, nanoid@^3.3.4:
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
nanoid@^3.3.4:
version "3.3.4"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
natural-compare@^1.4.0: natural-compare@^1.4.0:
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@ -10320,6 +10320,11 @@ streamsearch@^1.1.0:
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764"
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
string-similarity@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-4.0.4.tgz#42d01ab0b34660ea8a018da8f56a3309bb8b2a5b"
integrity sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ==
string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2: string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2:
version "4.2.3" version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"