Multiple choice markets (#698)
* multipe choice answers * create multiple choice cloud function * multi choice market page * show outcome '0' * stats: multi choice type * update place bet * answer doc id = outcome * update resolve market * prettier * fix * fix resolution
This commit is contained in:
parent
b1c4f018f9
commit
1aaae93113
|
@ -5,12 +5,14 @@ import {
|
|||
CPMMBinaryContract,
|
||||
DPMBinaryContract,
|
||||
FreeResponseContract,
|
||||
MultipleChoiceContract,
|
||||
NumericContract,
|
||||
} from './contract'
|
||||
import { User } from './user'
|
||||
import { LiquidityProvision } from './liquidity-provision'
|
||||
import { noFees } from './fees'
|
||||
import { ENV_CONFIG } from './envs/constants'
|
||||
import { Answer } from './answer'
|
||||
|
||||
export const FIXED_ANTE = ENV_CONFIG.fixedAnte ?? 100
|
||||
|
||||
|
@ -111,6 +113,50 @@ export function getFreeAnswerAnte(
|
|||
return anteBet
|
||||
}
|
||||
|
||||
export function getMultipleChoiceAntes(
|
||||
creator: User,
|
||||
contract: MultipleChoiceContract,
|
||||
answers: string[],
|
||||
betDocIds: string[]
|
||||
) {
|
||||
const { totalBets, totalShares } = contract
|
||||
const amount = totalBets['0']
|
||||
const shares = totalShares['0']
|
||||
const p = 1 / answers.length
|
||||
|
||||
const { createdTime } = contract
|
||||
|
||||
const bets: Bet[] = answers.map((answer, i) => ({
|
||||
id: betDocIds[i],
|
||||
userId: creator.id,
|
||||
contractId: contract.id,
|
||||
amount,
|
||||
shares,
|
||||
outcome: i.toString(),
|
||||
probBefore: p,
|
||||
probAfter: p,
|
||||
createdTime,
|
||||
isAnte: true,
|
||||
fees: noFees,
|
||||
}))
|
||||
|
||||
const { username, name, avatarUrl } = creator
|
||||
|
||||
const answerObjects: Answer[] = answers.map((answer, i) => ({
|
||||
id: i.toString(),
|
||||
number: i,
|
||||
contractId: contract.id,
|
||||
createdTime,
|
||||
userId: creator.id,
|
||||
username,
|
||||
name,
|
||||
avatarUrl,
|
||||
text: answer,
|
||||
}))
|
||||
|
||||
return { bets, answerObjects }
|
||||
}
|
||||
|
||||
export function getNumericAnte(
|
||||
anteBettorId: string,
|
||||
contract: NumericContract,
|
||||
|
|
|
@ -23,6 +23,7 @@ import {
|
|||
BinaryContract,
|
||||
FreeResponseContract,
|
||||
PseudoNumericContract,
|
||||
MultipleChoiceContract,
|
||||
} from './contract'
|
||||
import { floatingEqual } from './util/math'
|
||||
|
||||
|
@ -200,7 +201,9 @@ export function getContractBetNullMetrics() {
|
|||
}
|
||||
}
|
||||
|
||||
export function getTopAnswer(contract: FreeResponseContract) {
|
||||
export function getTopAnswer(
|
||||
contract: FreeResponseContract | MultipleChoiceContract
|
||||
) {
|
||||
const { answers } = contract
|
||||
const top = maxBy(
|
||||
answers?.map((answer) => ({
|
||||
|
|
|
@ -4,13 +4,19 @@ import { JSONContent } from '@tiptap/core'
|
|||
import { GroupLink } from 'common/group'
|
||||
|
||||
export type AnyMechanism = DPM | CPMM
|
||||
export type AnyOutcomeType = Binary | PseudoNumeric | FreeResponse | Numeric
|
||||
export type AnyOutcomeType =
|
||||
| Binary
|
||||
| MultipleChoice
|
||||
| PseudoNumeric
|
||||
| FreeResponse
|
||||
| Numeric
|
||||
export type AnyContractType =
|
||||
| (CPMM & Binary)
|
||||
| (CPMM & PseudoNumeric)
|
||||
| (DPM & Binary)
|
||||
| (DPM & FreeResponse)
|
||||
| (DPM & Numeric)
|
||||
| (DPM & MultipleChoice)
|
||||
|
||||
export type Contract<T extends AnyContractType = AnyContractType> = {
|
||||
id: string
|
||||
|
@ -57,6 +63,7 @@ export type BinaryContract = Contract & Binary
|
|||
export type PseudoNumericContract = Contract & PseudoNumeric
|
||||
export type NumericContract = Contract & Numeric
|
||||
export type FreeResponseContract = Contract & FreeResponse
|
||||
export type MultipleChoiceContract = Contract & MultipleChoice
|
||||
export type DPMContract = Contract & DPM
|
||||
export type CPMMContract = Contract & CPMM
|
||||
export type DPMBinaryContract = BinaryContract & DPM
|
||||
|
@ -104,6 +111,13 @@ export type FreeResponse = {
|
|||
resolutions?: { [outcome: string]: number } // Used for MKT resolution.
|
||||
}
|
||||
|
||||
export type MultipleChoice = {
|
||||
outcomeType: 'MULTIPLE_CHOICE'
|
||||
answers: Answer[]
|
||||
resolution?: string | 'MKT' | 'CANCEL'
|
||||
resolutions?: { [outcome: string]: number } // Used for MKT resolution.
|
||||
}
|
||||
|
||||
export type Numeric = {
|
||||
outcomeType: 'NUMERIC'
|
||||
bucketCount: number
|
||||
|
@ -118,6 +132,7 @@ export type resolution = 'YES' | 'NO' | 'MKT' | 'CANCEL'
|
|||
export const RESOLUTIONS = ['YES', 'NO', 'MKT', 'CANCEL'] as const
|
||||
export const OUTCOME_TYPES = [
|
||||
'BINARY',
|
||||
'MULTIPLE_CHOICE',
|
||||
'FREE_RESPONSE',
|
||||
'PSEUDO_NUMERIC',
|
||||
'NUMERIC',
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
CPMMBinaryContract,
|
||||
DPMBinaryContract,
|
||||
FreeResponseContract,
|
||||
MultipleChoiceContract,
|
||||
NumericContract,
|
||||
PseudoNumericContract,
|
||||
} from './contract'
|
||||
|
@ -322,7 +323,7 @@ export const getNewBinaryDpmBetInfo = (
|
|||
export const getNewMultiBetInfo = (
|
||||
outcome: string,
|
||||
amount: number,
|
||||
contract: FreeResponseContract,
|
||||
contract: FreeResponseContract | MultipleChoiceContract,
|
||||
loanAmount: number
|
||||
) => {
|
||||
const { pool, totalShares, totalBets } = contract
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
CPMM,
|
||||
DPM,
|
||||
FreeResponse,
|
||||
MultipleChoice,
|
||||
Numeric,
|
||||
outcomeType,
|
||||
PseudoNumeric,
|
||||
|
@ -30,7 +31,10 @@ export function getNewContract(
|
|||
bucketCount: number,
|
||||
min: number,
|
||||
max: number,
|
||||
isLogScale: boolean
|
||||
isLogScale: boolean,
|
||||
|
||||
// for multiple choice
|
||||
answers: string[]
|
||||
) {
|
||||
const tags = parseTags(
|
||||
[
|
||||
|
@ -48,6 +52,8 @@ export function getNewContract(
|
|||
? getPseudoNumericCpmmProps(initialProb, ante, min, max, isLogScale)
|
||||
: outcomeType === 'NUMERIC'
|
||||
? getNumericProps(ante, bucketCount, min, max)
|
||||
: outcomeType === 'MULTIPLE_CHOICE'
|
||||
? getMultipleChoiceProps(ante, answers)
|
||||
: getFreeAnswerProps(ante)
|
||||
|
||||
const contract: Contract = removeUndefinedProps({
|
||||
|
@ -151,6 +157,26 @@ const getFreeAnswerProps = (ante: number) => {
|
|||
return system
|
||||
}
|
||||
|
||||
const getMultipleChoiceProps = (ante: number, answers: string[]) => {
|
||||
const numAnswers = answers.length
|
||||
const betAnte = ante / numAnswers
|
||||
const betShares = Math.sqrt(ante ** 2 / numAnswers)
|
||||
|
||||
const defaultValues = (x: any) =>
|
||||
Object.fromEntries(range(0, numAnswers).map((k) => [k, x]))
|
||||
|
||||
const system: DPM & MultipleChoice = {
|
||||
mechanism: 'dpm-2',
|
||||
outcomeType: 'MULTIPLE_CHOICE',
|
||||
pool: defaultValues(betAnte),
|
||||
totalShares: defaultValues(betShares),
|
||||
totalBets: defaultValues(betAnte),
|
||||
answers: [],
|
||||
}
|
||||
|
||||
return system
|
||||
}
|
||||
|
||||
const getNumericProps = (
|
||||
ante: number,
|
||||
bucketCount: number,
|
||||
|
|
|
@ -2,7 +2,11 @@ import { sum, groupBy, sumBy, mapValues } from 'lodash'
|
|||
|
||||
import { Bet, NumericBet } from './bet'
|
||||
import { deductDpmFees, getDpmProbability } from './calculate-dpm'
|
||||
import { DPMContract, FreeResponseContract } from './contract'
|
||||
import {
|
||||
DPMContract,
|
||||
FreeResponseContract,
|
||||
MultipleChoiceContract,
|
||||
} from './contract'
|
||||
import { DPM_CREATOR_FEE, DPM_FEES, DPM_PLATFORM_FEE } from './fees'
|
||||
import { addObjects } from './util/object'
|
||||
|
||||
|
@ -180,7 +184,7 @@ export const getDpmMktPayouts = (
|
|||
|
||||
export const getPayoutsMultiOutcome = (
|
||||
resolutions: { [outcome: string]: number },
|
||||
contract: FreeResponseContract,
|
||||
contract: FreeResponseContract | MultipleChoiceContract,
|
||||
bets: Bet[]
|
||||
) => {
|
||||
const poolTotal = sum(Object.values(contract.pool))
|
||||
|
|
|
@ -117,6 +117,7 @@ export const getDpmPayouts = (
|
|||
resolutionProbability?: number
|
||||
): PayoutInfo => {
|
||||
const openBets = bets.filter((b) => !b.isSold && !b.sale)
|
||||
const { outcomeType } = contract
|
||||
|
||||
switch (outcome) {
|
||||
case 'YES':
|
||||
|
@ -124,7 +125,8 @@ export const getDpmPayouts = (
|
|||
return getDpmStandardPayouts(outcome, contract, openBets)
|
||||
|
||||
case 'MKT':
|
||||
return contract.outcomeType === 'FREE_RESPONSE' // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return outcomeType === 'FREE_RESPONSE' ||
|
||||
outcomeType === 'MULTIPLE_CHOICE' // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
? getPayoutsMultiOutcome(resolutions!, contract, openBets)
|
||||
: getDpmMktPayouts(contract, openBets, resolutionProbability)
|
||||
case 'CANCEL':
|
||||
|
@ -132,7 +134,7 @@ export const getDpmPayouts = (
|
|||
return getDpmCancelPayouts(contract, openBets)
|
||||
|
||||
default:
|
||||
if (contract.outcomeType === 'NUMERIC')
|
||||
if (outcomeType === 'NUMERIC')
|
||||
return getNumericDpmPayouts(outcome, contract, openBets as NumericBet[])
|
||||
|
||||
// Outcome is a free response answer id.
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
FreeResponseContract,
|
||||
MAX_QUESTION_LENGTH,
|
||||
MAX_TAG_LENGTH,
|
||||
MultipleChoiceContract,
|
||||
NumericContract,
|
||||
OUTCOME_TYPES,
|
||||
} from '../../common/contract'
|
||||
|
@ -20,15 +21,18 @@ import {
|
|||
FIXED_ANTE,
|
||||
getCpmmInitialLiquidity,
|
||||
getFreeAnswerAnte,
|
||||
getMultipleChoiceAntes,
|
||||
getNumericAnte,
|
||||
} from '../../common/antes'
|
||||
import { getNoneAnswer } from '../../common/answer'
|
||||
import { Answer, getNoneAnswer } from '../../common/answer'
|
||||
import { getNewContract } from '../../common/new-contract'
|
||||
import { NUMERIC_BUCKET_COUNT } from '../../common/numeric-constants'
|
||||
import { User } from '../../common/user'
|
||||
import { Group, MAX_ID_LENGTH } from '../../common/group'
|
||||
import { getPseudoProbability } from '../../common/pseudo-numeric'
|
||||
import { JSONContent } from '@tiptap/core'
|
||||
import { zip } from 'lodash'
|
||||
import { Bet } from 'common/bet'
|
||||
|
||||
const descScehma: z.ZodType<JSONContent> = z.lazy(() =>
|
||||
z.intersection(
|
||||
|
@ -79,11 +83,15 @@ const numericSchema = z.object({
|
|||
isLogScale: z.boolean().optional(),
|
||||
})
|
||||
|
||||
const multipleChoiceSchema = z.object({
|
||||
answers: z.string().trim().min(1).array().min(2),
|
||||
})
|
||||
|
||||
export const createmarket = newEndpoint({}, async (req, auth) => {
|
||||
const { question, description, tags, closeTime, outcomeType, groupId } =
|
||||
validate(bodySchema, req.body)
|
||||
|
||||
let min, max, initialProb, isLogScale
|
||||
let min, max, initialProb, isLogScale, answers
|
||||
|
||||
if (outcomeType === 'PSEUDO_NUMERIC' || outcomeType === 'NUMERIC') {
|
||||
let initialValue
|
||||
|
@ -104,10 +112,15 @@ export const createmarket = newEndpoint({}, async (req, auth) => {
|
|||
)
|
||||
else throw new APIError(400, 'Invalid initial probability.')
|
||||
}
|
||||
|
||||
if (outcomeType === 'BINARY') {
|
||||
;({ initialProb } = validate(binarySchema, req.body))
|
||||
}
|
||||
|
||||
if (outcomeType === 'MULTIPLE_CHOICE') {
|
||||
;({ answers } = validate(multipleChoiceSchema, req.body))
|
||||
}
|
||||
|
||||
const userDoc = await firestore.collection('users').doc(auth.uid).get()
|
||||
if (!userDoc.exists) {
|
||||
throw new APIError(400, 'No user exists with the authenticated user ID.')
|
||||
|
@ -167,7 +180,8 @@ export const createmarket = newEndpoint({}, async (req, auth) => {
|
|||
NUMERIC_BUCKET_COUNT,
|
||||
min ?? 0,
|
||||
max ?? 0,
|
||||
isLogScale ?? false
|
||||
isLogScale ?? false,
|
||||
answers ?? []
|
||||
)
|
||||
|
||||
if (ante) await chargeUser(user.id, ante, true)
|
||||
|
@ -189,6 +203,31 @@ export const createmarket = newEndpoint({}, async (req, auth) => {
|
|||
)
|
||||
|
||||
await liquidityDoc.set(lp)
|
||||
} else if (outcomeType === 'MULTIPLE_CHOICE') {
|
||||
const betCol = firestore.collection(`contracts/${contract.id}/bets`)
|
||||
const betDocs = (answers ?? []).map(() => betCol.doc())
|
||||
|
||||
const answerCol = firestore.collection(`contracts/${contract.id}/answers`)
|
||||
const answerDocs = (answers ?? []).map((_, i) =>
|
||||
answerCol.doc(i.toString())
|
||||
)
|
||||
|
||||
const { bets, answerObjects } = getMultipleChoiceAntes(
|
||||
user,
|
||||
contract as MultipleChoiceContract,
|
||||
answers ?? [],
|
||||
betDocs.map((bd) => bd.id)
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
zip(bets, betDocs).map(([bet, doc]) => doc?.create(bet as Bet))
|
||||
)
|
||||
await Promise.all(
|
||||
zip(answerObjects, answerDocs).map(([answer, doc]) =>
|
||||
doc?.create(answer as Answer)
|
||||
)
|
||||
)
|
||||
await contractRef.update({ answers: answerObjects })
|
||||
} else if (outcomeType === 'FREE_RESPONSE') {
|
||||
const noneAnswerDoc = firestore
|
||||
.collection(`contracts/${contract.id}/answers`)
|
||||
|
|
|
@ -96,7 +96,10 @@ export const placebet = newEndpoint({}, async (req, auth) => {
|
|||
limitProb,
|
||||
unfilledBets
|
||||
)
|
||||
} else if (outcomeType == 'FREE_RESPONSE' && mechanism == 'dpm-2') {
|
||||
} else if (
|
||||
(outcomeType == 'FREE_RESPONSE' || outcomeType === 'MULTIPLE_CHOICE') &&
|
||||
mechanism == 'dpm-2'
|
||||
) {
|
||||
const { outcome } = validate(freeResponseSchema, req.body)
|
||||
const answerDoc = contractDoc.collection('answers').doc(outcome)
|
||||
const answerSnap = await trans.get(answerDoc)
|
||||
|
|
|
@ -5,6 +5,7 @@ import { difference, uniq, mapValues, groupBy, sumBy } from 'lodash'
|
|||
import {
|
||||
Contract,
|
||||
FreeResponseContract,
|
||||
MultipleChoiceContract,
|
||||
RESOLUTIONS,
|
||||
} from '../../common/contract'
|
||||
import { User } from '../../common/user'
|
||||
|
@ -245,7 +246,10 @@ function getResolutionParams(contract: Contract, body: string) {
|
|||
...validate(pseudoNumericSchema, body),
|
||||
resolutions: undefined,
|
||||
}
|
||||
} else if (outcomeType === 'FREE_RESPONSE') {
|
||||
} else if (
|
||||
outcomeType === 'FREE_RESPONSE' ||
|
||||
outcomeType === 'MULTIPLE_CHOICE'
|
||||
) {
|
||||
const freeResponseParams = validate(freeResponseSchema, body)
|
||||
const { outcome } = freeResponseParams
|
||||
switch (outcome) {
|
||||
|
@ -292,7 +296,10 @@ function getResolutionParams(contract: Contract, body: string) {
|
|||
throw new APIError(500, `Invalid outcome type: ${outcomeType}`)
|
||||
}
|
||||
|
||||
function validateAnswer(contract: FreeResponseContract, answer: number) {
|
||||
function validateAnswer(
|
||||
contract: FreeResponseContract | MultipleChoiceContract,
|
||||
answer: number
|
||||
) {
|
||||
const validIds = contract.answers.map((a) => a.id)
|
||||
if (!validIds.includes(answer.toString())) {
|
||||
throw new APIError(400, `${answer} is not a valid answer ID`)
|
||||
|
|
|
@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from 'react'
|
|||
import { XIcon } from '@heroicons/react/solid'
|
||||
|
||||
import { Answer } from 'common/answer'
|
||||
import { FreeResponseContract } from 'common/contract'
|
||||
import { FreeResponseContract, MultipleChoiceContract } from 'common/contract'
|
||||
import { BuyAmountInput } from '../amount-input'
|
||||
import { Col } from '../layout/col'
|
||||
import { APIError, placeBet } from 'web/lib/firebase/api'
|
||||
|
@ -29,7 +29,7 @@ import { isIOS } from 'web/lib/util/device'
|
|||
|
||||
export function AnswerBetPanel(props: {
|
||||
answer: Answer
|
||||
contract: FreeResponseContract
|
||||
contract: FreeResponseContract | MultipleChoiceContract
|
||||
closePanel: () => void
|
||||
className?: string
|
||||
isModal?: boolean
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import clsx from 'clsx'
|
||||
|
||||
import { Answer } from 'common/answer'
|
||||
import { FreeResponseContract } from 'common/contract'
|
||||
import { FreeResponseContract, MultipleChoiceContract } from 'common/contract'
|
||||
import { Col } from '../layout/col'
|
||||
import { Row } from '../layout/row'
|
||||
import { Avatar } from '../avatar'
|
||||
|
@ -13,7 +13,7 @@ import { Linkify } from '../linkify'
|
|||
|
||||
export function AnswerItem(props: {
|
||||
answer: Answer
|
||||
contract: FreeResponseContract
|
||||
contract: FreeResponseContract | MultipleChoiceContract
|
||||
showChoice: 'radio' | 'checkbox' | undefined
|
||||
chosenProb: number | undefined
|
||||
totalChosenProb?: number
|
||||
|
|
|
@ -2,7 +2,7 @@ import clsx from 'clsx'
|
|||
import { sum } from 'lodash'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { Contract, FreeResponse } from 'common/contract'
|
||||
import { FreeResponseContract, MultipleChoiceContract } from 'common/contract'
|
||||
import { Col } from '../layout/col'
|
||||
import { APIError, resolveMarket } from 'web/lib/firebase/api'
|
||||
import { Row } from '../layout/row'
|
||||
|
@ -11,7 +11,7 @@ import { ResolveConfirmationButton } from '../confirmation-button'
|
|||
import { removeUndefinedProps } from 'common/util/object'
|
||||
|
||||
export function AnswerResolvePanel(props: {
|
||||
contract: Contract & FreeResponse
|
||||
contract: FreeResponseContract | MultipleChoiceContract
|
||||
resolveOption: 'CHOOSE' | 'CHOOSE_MULTIPLE' | 'CANCEL' | undefined
|
||||
setResolveOption: (
|
||||
option: 'CHOOSE' | 'CHOOSE_MULTIPLE' | 'CANCEL' | undefined
|
||||
|
|
|
@ -5,14 +5,14 @@ import { groupBy, sortBy, sumBy } from 'lodash'
|
|||
import { memo } from 'react'
|
||||
|
||||
import { Bet } from 'common/bet'
|
||||
import { FreeResponseContract } from 'common/contract'
|
||||
import { FreeResponseContract, MultipleChoiceContract } from 'common/contract'
|
||||
import { getOutcomeProbability } from 'common/calculate'
|
||||
import { useWindowSize } from 'web/hooks/use-window-size'
|
||||
|
||||
const NUM_LINES = 6
|
||||
|
||||
export const AnswersGraph = memo(function AnswersGraph(props: {
|
||||
contract: FreeResponseContract
|
||||
contract: FreeResponseContract | MultipleChoiceContract
|
||||
bets: Bet[]
|
||||
height?: number
|
||||
}) {
|
||||
|
@ -178,15 +178,22 @@ function formatTime(
|
|||
return d.format(format)
|
||||
}
|
||||
|
||||
const computeProbsByOutcome = (bets: Bet[], contract: FreeResponseContract) => {
|
||||
const { totalBets } = contract
|
||||
const computeProbsByOutcome = (
|
||||
bets: Bet[],
|
||||
contract: FreeResponseContract | MultipleChoiceContract
|
||||
) => {
|
||||
const { totalBets, outcomeType } = contract
|
||||
|
||||
const betsByOutcome = groupBy(bets, (bet) => bet.outcome)
|
||||
const outcomes = Object.keys(betsByOutcome).filter((outcome) => {
|
||||
const maxProb = Math.max(
|
||||
...betsByOutcome[outcome].map((bet) => bet.probAfter)
|
||||
)
|
||||
return outcome !== '0' && maxProb > 0.02 && totalBets[outcome] > 0.000000001
|
||||
return (
|
||||
(outcome !== '0' || outcomeType === 'MULTIPLE_CHOICE') &&
|
||||
maxProb > 0.02 &&
|
||||
totalBets[outcome] > 0.000000001
|
||||
)
|
||||
})
|
||||
|
||||
const trackedOutcomes = sortBy(
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { sortBy, partition, sum, uniq } from 'lodash'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { FreeResponseContract } from 'common/contract'
|
||||
import { FreeResponseContract, MultipleChoiceContract } from 'common/contract'
|
||||
import { Col } from '../layout/col'
|
||||
import { useUser } from 'web/hooks/use-user'
|
||||
import { getDpmOutcomeProbability } from 'common/calculate-dpm'
|
||||
|
@ -25,14 +25,19 @@ import { UserLink } from 'web/components/user-page'
|
|||
import { Linkify } from 'web/components/linkify'
|
||||
import { BuyButton } from 'web/components/yes-no-selector'
|
||||
|
||||
export function AnswersPanel(props: { contract: FreeResponseContract }) {
|
||||
export function AnswersPanel(props: {
|
||||
contract: FreeResponseContract | MultipleChoiceContract
|
||||
}) {
|
||||
const { contract } = props
|
||||
const { creatorId, resolution, resolutions, totalBets } = contract
|
||||
const { creatorId, resolution, resolutions, totalBets, outcomeType } =
|
||||
contract
|
||||
|
||||
const answers = useAnswers(contract.id) ?? contract.answers
|
||||
const [winningAnswers, losingAnswers] = partition(
|
||||
answers.filter(
|
||||
(answer) => answer.id !== '0' && totalBets[answer.id] > 0.000000001
|
||||
(answer) =>
|
||||
(answer.id !== '0' || outcomeType === 'MULTIPLE_CHOICE') &&
|
||||
totalBets[answer.id] > 0.000000001
|
||||
),
|
||||
(answer) =>
|
||||
answer.id === resolution || (resolutions && resolutions[answer.id])
|
||||
|
@ -131,7 +136,8 @@ export function AnswersPanel(props: { contract: FreeResponseContract }) {
|
|||
<div className="pb-4 text-gray-500">No answers yet...</div>
|
||||
)}
|
||||
|
||||
{tradingAllowed(contract) &&
|
||||
{outcomeType === 'FREE_RESPONSE' &&
|
||||
tradingAllowed(contract) &&
|
||||
(!resolveOption || resolveOption === 'CANCEL') && (
|
||||
<CreateAnswerPanel contract={contract} />
|
||||
)}
|
||||
|
@ -152,7 +158,7 @@ export function AnswersPanel(props: { contract: FreeResponseContract }) {
|
|||
}
|
||||
|
||||
function getAnswerItems(
|
||||
contract: FreeResponseContract,
|
||||
contract: FreeResponseContract | MultipleChoiceContract,
|
||||
answers: Answer[],
|
||||
user: User | undefined | null
|
||||
) {
|
||||
|
@ -178,7 +184,7 @@ function getAnswerItems(
|
|||
}
|
||||
|
||||
function OpenAnswer(props: {
|
||||
contract: FreeResponseContract
|
||||
contract: FreeResponseContract | MultipleChoiceContract
|
||||
answer: Answer
|
||||
items: ActivityItem[]
|
||||
type: string
|
||||
|
|
65
web/components/answers/multiple-choice-answers.tsx
Normal file
65
web/components/answers/multiple-choice-answers.tsx
Normal file
|
@ -0,0 +1,65 @@
|
|||
import { MAX_ANSWER_LENGTH } from 'common/answer'
|
||||
import { useState } from 'react'
|
||||
import Textarea from 'react-expanding-textarea'
|
||||
import { XIcon } from '@heroicons/react/solid'
|
||||
|
||||
import { Col } from '../layout/col'
|
||||
import { Row } from '../layout/row'
|
||||
|
||||
export function MultipleChoiceAnswers(props: {
|
||||
setAnswers: (answers: string[]) => void
|
||||
}) {
|
||||
const [answers, setInternalAnswers] = useState(['', '', ''])
|
||||
|
||||
const setAnswer = (i: number, answer: string) => {
|
||||
const newAnswers = setElement(answers, i, answer)
|
||||
setInternalAnswers(newAnswers)
|
||||
props.setAnswers(newAnswers)
|
||||
}
|
||||
|
||||
const removeAnswer = (i: number) => {
|
||||
const newAnswers = answers.slice(0, i).concat(answers.slice(i + 1))
|
||||
setInternalAnswers(newAnswers)
|
||||
props.setAnswers(newAnswers)
|
||||
}
|
||||
|
||||
const addAnswer = () => setAnswer(answers.length, '')
|
||||
|
||||
return (
|
||||
<Col>
|
||||
{answers.map((answer, i) => (
|
||||
<Row className="mb-2 items-center align-middle">
|
||||
{i + 1}.{' '}
|
||||
<Textarea
|
||||
value={answer}
|
||||
onChange={(e) => setAnswer(i, e.target.value)}
|
||||
className="textarea textarea-bordered ml-2 w-full resize-none"
|
||||
placeholder="Type your answer..."
|
||||
rows={1}
|
||||
maxLength={MAX_ANSWER_LENGTH}
|
||||
/>
|
||||
{answers.length > 2 && (
|
||||
<button
|
||||
className="btn btn-xs btn-outline ml-2"
|
||||
onClick={() => removeAnswer(i)}
|
||||
>
|
||||
<XIcon className="h-4 w-4 flex-shrink-0" />
|
||||
</button>
|
||||
)}
|
||||
</Row>
|
||||
))}
|
||||
|
||||
<Row className="justify-end">
|
||||
<button className="btn btn-outline btn-xs" onClick={addAnswer}>
|
||||
Add answer
|
||||
</button>
|
||||
</Row>
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
||||
const setElement = <T,>(array: T[], i: number, elem: T) => {
|
||||
const newArray = array.concat()
|
||||
newArray[i] = elem
|
||||
return newArray
|
||||
}
|
|
@ -8,6 +8,7 @@ import {
|
|||
BinaryContract,
|
||||
Contract,
|
||||
FreeResponseContract,
|
||||
MultipleChoiceContract,
|
||||
NumericContract,
|
||||
PseudoNumericContract,
|
||||
} from 'common/contract'
|
||||
|
@ -227,7 +228,7 @@ function FreeResponseTopAnswer(props: {
|
|||
}
|
||||
|
||||
export function FreeResponseResolutionOrChance(props: {
|
||||
contract: FreeResponseContract
|
||||
contract: FreeResponseContract | MultipleChoiceContract
|
||||
truncate: 'short' | 'long' | 'none'
|
||||
className?: string
|
||||
}) {
|
||||
|
|
|
@ -41,6 +41,8 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
|||
? 'YES / NO'
|
||||
: outcomeType === 'FREE_RESPONSE'
|
||||
? 'Free response'
|
||||
: outcomeType === 'MULTIPLE_CHOICE'
|
||||
? 'Multiple choice'
|
||||
: 'Numeric'
|
||||
|
||||
return (
|
||||
|
|
|
@ -85,7 +85,8 @@ export const ContractOverview = (props: {
|
|||
{tradingAllowed(contract) && <BetRow contract={contract} />}
|
||||
</Row>
|
||||
) : (
|
||||
outcomeType === 'FREE_RESPONSE' &&
|
||||
(outcomeType === 'FREE_RESPONSE' ||
|
||||
outcomeType === 'MULTIPLE_CHOICE') &&
|
||||
resolution && (
|
||||
<FreeResponseResolutionOrChance
|
||||
contract={contract}
|
||||
|
@ -110,7 +111,8 @@ export const ContractOverview = (props: {
|
|||
{(isBinary || isPseudoNumeric) && (
|
||||
<ContractProbGraph contract={contract} bets={bets} />
|
||||
)}{' '}
|
||||
{outcomeType === 'FREE_RESPONSE' && (
|
||||
{(outcomeType === 'FREE_RESPONSE' ||
|
||||
outcomeType === 'MULTIPLE_CHOICE') && (
|
||||
<AnswersGraph contract={contract} bets={bets} />
|
||||
)}
|
||||
{outcomeType === 'NUMERIC' && <NumericGraph contract={contract} />}
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
BinaryContract,
|
||||
Contract,
|
||||
FreeResponseContract,
|
||||
MultipleChoiceContract,
|
||||
resolution,
|
||||
} from 'common/contract'
|
||||
import { formatLargeNumber, formatPercent } from 'common/util/format'
|
||||
|
@ -77,7 +78,7 @@ export function BinaryContractOutcomeLabel(props: {
|
|||
}
|
||||
|
||||
export function FreeResponseOutcomeLabel(props: {
|
||||
contract: FreeResponseContract
|
||||
contract: FreeResponseContract | MultipleChoiceContract
|
||||
resolution: string | 'CANCEL' | 'MKT'
|
||||
truncate: 'short' | 'long' | 'none'
|
||||
answerClassName?: string
|
||||
|
|
|
@ -217,7 +217,8 @@ export function ContractPageContent(
|
|||
/>
|
||||
)}
|
||||
|
||||
{outcomeType === 'FREE_RESPONSE' && (
|
||||
{(outcomeType === 'FREE_RESPONSE' ||
|
||||
outcomeType === 'MULTIPLE_CHOICE') && (
|
||||
<>
|
||||
<Spacer h={4} />
|
||||
<AnswersPanel contract={contract} />
|
||||
|
|
|
@ -31,6 +31,7 @@ import { Checkbox } from 'web/components/checkbox'
|
|||
import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth'
|
||||
import { Title } from 'web/components/title'
|
||||
import { SEO } from 'web/components/SEO'
|
||||
import { MultipleChoiceAnswers } from 'web/components/answers/multiple-choice-answers'
|
||||
|
||||
export const getServerSideProps = redirectIfLoggedOut('/')
|
||||
|
||||
|
@ -116,6 +117,8 @@ export function NewContract(props: {
|
|||
const [isLogScale, setIsLogScale] = useState<boolean>(!!params?.isLogScale)
|
||||
const [initialValueString, setInitialValueString] = useState(initValue)
|
||||
|
||||
const [answers, setAnswers] = useState<string[]>([]) // for multiple choice
|
||||
|
||||
useEffect(() => {
|
||||
if (groupId && creator)
|
||||
getGroup(groupId).then((group) => {
|
||||
|
@ -160,6 +163,10 @@ export function NewContract(props: {
|
|||
// get days from today until the end of this year:
|
||||
const daysLeftInTheYear = dayjs().endOf('year').diff(dayjs(), 'day')
|
||||
|
||||
const isValidMultipleChoice = answers.every(
|
||||
(answer) => answer.trim().length > 0
|
||||
)
|
||||
|
||||
const isValid =
|
||||
(outcomeType === 'BINARY' ? initialProb >= 5 && initialProb <= 95 : true) &&
|
||||
question.length > 0 &&
|
||||
|
@ -178,7 +185,8 @@ export function NewContract(props: {
|
|||
min < max &&
|
||||
max - min > 0.01 &&
|
||||
min < initialValue &&
|
||||
initialValue < max))
|
||||
initialValue < max)) &&
|
||||
(outcomeType !== 'MULTIPLE_CHOICE' || isValidMultipleChoice)
|
||||
|
||||
const [errorText, setErrorText] = useState<string>('')
|
||||
useEffect(() => {
|
||||
|
@ -221,6 +229,7 @@ export function NewContract(props: {
|
|||
max,
|
||||
initialValue,
|
||||
isLogScale,
|
||||
answers,
|
||||
groupId: selectedGroup?.id,
|
||||
})
|
||||
)
|
||||
|
@ -259,10 +268,11 @@ export function NewContract(props: {
|
|||
'Users can submit their own answers to this market.'
|
||||
)
|
||||
else setMarketInfoText('')
|
||||
setOutcomeType(choice as 'BINARY' | 'FREE_RESPONSE')
|
||||
setOutcomeType(choice as outcomeType)
|
||||
}}
|
||||
choicesMap={{
|
||||
'Yes / No': 'BINARY',
|
||||
'Multiple choice': 'MULTIPLE_CHOICE',
|
||||
'Free response': 'FREE_RESPONSE',
|
||||
Numeric: 'PSEUDO_NUMERIC',
|
||||
}}
|
||||
|
@ -277,6 +287,10 @@ export function NewContract(props: {
|
|||
|
||||
<Spacer h={6} />
|
||||
|
||||
{outcomeType === 'MULTIPLE_CHOICE' && (
|
||||
<MultipleChoiceAnswers setAnswers={setAnswers} />
|
||||
)}
|
||||
|
||||
{outcomeType === 'PSEUDO_NUMERIC' && (
|
||||
<>
|
||||
<div className="form-control mb-2 items-start">
|
||||
|
|
Loading…
Reference in New Issue
Block a user