Prevent duplicate Free Response answers (#581)
* Prevent duplicate Free Response answers * Address review comments
This commit is contained in:
parent
b7cbd2a431
commit
5e768aa57c
|
@ -1,6 +1,7 @@
|
|||
import clsx from 'clsx'
|
||||
import { useState } from 'react'
|
||||
import Textarea from 'react-expanding-textarea'
|
||||
import { findBestMatch } from 'string-similarity'
|
||||
|
||||
import { FreeResponseContract } from 'common/contract'
|
||||
import { BuyAmountInput } from '../amount-input'
|
||||
|
@ -23,6 +24,7 @@ import { firebaseLogin } from 'web/lib/firebase/users'
|
|||
import { Bet } from 'common/bet'
|
||||
import { MAX_ANSWER_LENGTH } from 'common/answer'
|
||||
import { withTracking } from 'web/lib/service/analytics'
|
||||
import { lowerCase } from 'lodash'
|
||||
|
||||
export function CreateAnswerPanel(props: { contract: FreeResponseContract }) {
|
||||
const { contract } = props
|
||||
|
@ -30,9 +32,15 @@ export function CreateAnswerPanel(props: { contract: FreeResponseContract }) {
|
|||
const [text, setText] = useState('')
|
||||
const [betAmount, setBetAmount] = useState<number | undefined>(10)
|
||||
const [amountError, setAmountError] = useState<string | undefined>()
|
||||
const [answerError, setAnswerError] = useState<string | undefined>()
|
||||
const [possibleDuplicateAnswer, setPossibleDuplicateAnswer] = useState<
|
||||
string | undefined
|
||||
>()
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const { answers } = contract
|
||||
|
||||
const canSubmit = text && betAmount && !amountError && !isSubmitting
|
||||
const canSubmit =
|
||||
text && betAmount && !amountError && !isSubmitting && !answerError
|
||||
|
||||
const submitAnswer = async () => {
|
||||
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(
|
||||
contract.totalShares,
|
||||
'new',
|
||||
|
@ -79,12 +117,21 @@ export function CreateAnswerPanel(props: { contract: FreeResponseContract }) {
|
|||
<div className="mb-1">Add your answer</div>
|
||||
<Textarea
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
onChange={(e) => changeAnswer(e.target.value)}
|
||||
className="textarea textarea-bordered w-full resize-none"
|
||||
placeholder="Type your answer..."
|
||||
rows={1}
|
||||
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 />
|
||||
<Col
|
||||
className={clsx(
|
||||
|
@ -163,3 +210,21 @@ export function CreateAnswerPanel(props: { contract: FreeResponseContract }) {
|
|||
</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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -41,7 +41,8 @@
|
|||
"react-expanding-textarea": "2.3.5",
|
||||
"react-hot-toast": "2.2.0",
|
||||
"react-instantsearch-hooks-web": "6.24.1",
|
||||
"react-query": "3.39.0"
|
||||
"react-query": "3.39.0",
|
||||
"string-similarity": "^4.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "0.4.0",
|
||||
|
@ -50,6 +51,7 @@
|
|||
"@types/lodash": "4.14.178",
|
||||
"@types/node": "16.11.11",
|
||||
"@types/react": "17.0.43",
|
||||
"@types/string-similarity": "^4.0.0",
|
||||
"autoprefixer": "10.2.6",
|
||||
"concurrently": "6.5.1",
|
||||
"critters": "0.0.16",
|
||||
|
|
15
yarn.lock
15
yarn.lock
|
@ -3148,6 +3148,11 @@
|
|||
dependencies:
|
||||
"@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":
|
||||
version "2.0.6"
|
||||
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"
|
||||
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:
|
||||
version "1.4.0"
|
||||
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"
|
||||
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:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
|
|
Loading…
Reference in New Issue
Block a user