feat: add challenge and red-blue competitions across API and web

This commit is contained in:
Joey Yakimowich-Payne 2025-10-01 06:49:09 -06:00
commit 8fd3c4bb64
No known key found for this signature in database
GPG key ID: 6BFE655FA5ABD1E1
77 changed files with 5355 additions and 24 deletions

View file

@ -0,0 +1,42 @@
import type { NodeDefault } from '../../types'
import { genNodeMetaData } from '@/app/components/workflow/utils'
import { BlockEnum, VarType } from '@/app/components/workflow/types'
import { BlockClassificationEnum } from '@/app/components/workflow/block-selector/types'
import type { TeamChallengeNodeType } from './types'
const metaData = genNodeMetaData({
classification: BlockClassificationEnum.Utilities,
sort: 5,
type: BlockEnum.TeamChallenge,
helpLinkUri: 'team-challenge',
})
const nodeDefault: NodeDefault<TeamChallengeNodeType> = {
metaData,
defaultValue: {
defense_selection_policy: 'latest_best',
attack_selection_policy: 'latest_best',
scoring_strategy: 'red_blue_ratio',
inputs: {
team_choice: [],
attack_prompt: [],
defense_prompt: [],
},
},
getOutputVars() {
return [
{ variable: 'team', type: VarType.string },
{ variable: 'judge_passed', type: VarType.boolean },
{ variable: 'judge_rating', type: VarType.number },
{ variable: 'judge_feedback', type: VarType.string },
{ variable: 'categories', type: VarType.object },
{ variable: 'team_points', type: VarType.number },
{ variable: 'total_points', type: VarType.number },
]
},
checkValid(_payload: TeamChallengeNodeType) {
return { isValid: true }
},
}
export default nodeDefault

View file

@ -0,0 +1,25 @@
import type { FC } from 'react'
import React from 'react'
import type { NodeProps } from '@/app/components/workflow/types'
import type { TeamChallengeNodeType } from './types'
const Node: FC<NodeProps<TeamChallengeNodeType>> = ({ data }) => {
const { red_blue_challenge_id, defense_selection_policy, attack_selection_policy } = data
return (
<div className='mb-1 px-3 py-1'>
{red_blue_challenge_id ? (
<div className='flex items-center gap-2'>
<div className='rounded bg-components-badge-white-to-dark px-1 py-0.5 text-[10px] font-semibold uppercase text-text-tertiary'>Red/Blue</div>
<div className='truncate text-xs text-text-secondary' title={red_blue_challenge_id}>{red_blue_challenge_id}</div>
</div>
) : (
<div className='flex items-center gap-2'>
<div className='rounded bg-components-badge-white-to-dark px-1 py-0.5 text-[10px] font-semibold uppercase text-text-tertiary'>Defense: {defense_selection_policy}</div>
<div className='rounded bg-components-badge-white-to-dark px-1 py-0.5 text-[10px] font-semibold uppercase text-text-tertiary'>Attack: {attack_selection_policy}</div>
</div>
)}
</div>
)
}
export default React.memo(Node)

View file

@ -0,0 +1,106 @@
import type { FC } from 'react'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import type { NodePanelProps } from '@/app/components/workflow/types'
import Field from '@/app/components/workflow/nodes/_base/components/field'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
import type { TeamChallengeNodeType } from './types'
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
import produce from 'immer'
import useSWR from 'swr'
import { fetchRedBlueChallenges } from '@/service/redBlueChallenges'
import Select from '@/app/components/base/select'
const i18nPrefix = 'workflow.nodes.teamChallenge'
const Panel: FC<NodePanelProps<TeamChallengeNodeType>> = ({ id, data }) => {
const { t } = useTranslation()
const { inputs, setInputs } = useNodeCrud<TeamChallengeNodeType>(id, data)
const { data: redBlue } = useSWR('redBlue:list', fetchRedBlueChallenges)
return (
<div className='pt-2'>
<div className='space-y-4 px-4 pb-4'>
<Field title={t(`${i18nPrefix}.selectedChallenge`)} tooltip={t(`${i18nPrefix}.selectedChallengeTip`)}>
<Select
items={(redBlue || []).map((c: any) => ({ value: c.id, name: c.name }))}
defaultValue={data.red_blue_challenge_id || ''}
onSelect={item => setInputs(produce(inputs, (draft) => { (draft as any).red_blue_challenge_id = (item?.value as string) || undefined }))}
allowSearch={false}
/>
</Field>
<Field title={t(`${i18nPrefix}.defenseSelectionPolicy`)} tooltip={t(`${i18nPrefix}.defenseSelectionPolicyTip`)}>
<Select
items={[
{ value: 'latest_best', name: 'latest_best' },
{ value: 'random_active', name: 'random_active' },
{ value: 'round_robin', name: 'round_robin' },
{ value: 'request_new_if_none', name: 'request_new_if_none' },
]}
defaultValue={data.defense_selection_policy || 'latest_best'}
onSelect={item => setInputs(produce(inputs, (draft) => { (draft as any).defense_selection_policy = item.value as string }))}
allowSearch={false}
/>
</Field>
<Field title={t(`${i18nPrefix}.attackSelectionPolicy`)} tooltip={t(`${i18nPrefix}.attackSelectionPolicyTip`)}>
<Select
items={[
{ value: 'latest_best', name: 'latest_best' },
{ value: 'random_active', name: 'random_active' },
{ value: 'round_robin', name: 'round_robin' },
{ value: 'request_new_if_none', name: 'request_new_if_none' },
]}
defaultValue={data.attack_selection_policy || 'latest_best'}
onSelect={item => setInputs(produce(inputs, (draft) => { (draft as any).attack_selection_policy = item.value as string }))}
allowSearch={false}
/>
</Field>
<Field title={t(`${i18nPrefix}.teamChoiceVar`)} tooltip={t(`${i18nPrefix}.teamChoiceVarTip`)}>
<VarReferencePicker
nodeId={id}
isShowNodeName
readonly={false}
value={data.inputs?.team_choice || []}
onChange={v => setInputs(produce(inputs, (draft) => { (draft as any).inputs = { ...(draft as any).inputs, team_choice: v } }))}
/>
</Field>
<Field title={t(`${i18nPrefix}.attackPromptVar`)} tooltip={t(`${i18nPrefix}.attackPromptVarTip`)}>
<VarReferencePicker
nodeId={id}
isShowNodeName
readonly={false}
value={data.inputs?.attack_prompt || []}
onChange={v => setInputs(produce(inputs, (draft) => { (draft as any).inputs = { ...(draft as any).inputs, attack_prompt: v } }))}
/>
</Field>
<Field title={t(`${i18nPrefix}.defensePromptVar`)} tooltip={t(`${i18nPrefix}.defensePromptVarTip`)}>
<VarReferencePicker
nodeId={id}
isShowNodeName
readonly={false}
value={data.inputs?.defense_prompt || []}
onChange={v => setInputs(produce(inputs, (draft) => { (draft as any).inputs = { ...(draft as any).inputs, defense_prompt: v } }))}
/>
</Field>
</div>
<Split />
<div>
<OutputVars>
<>
<VarItem name='team' type='string' description='Team' />
<VarItem name='judge_passed' type='boolean' description='Judge passed' />
<VarItem name='judge_rating' type='number' description='Judge rating' />
<VarItem name='judge_feedback' type='string' description='Judge feedback' />
<VarItem name='categories' type='object' description='Category outcomes' />
<VarItem name='team_points' type='number' description='Team points' />
<VarItem name='total_points' type='number' description='Total points' />
</>
</OutputVars>
</div>
</div>
)
}
export default memo(Panel)

View file

@ -0,0 +1,16 @@
import type { CommonNodeType, ValueSelector } from '@/app/components/workflow/types'
import { BlockEnum } from '@/app/components/workflow/types'
export type TeamChallengeNodeType = CommonNodeType<{
red_blue_challenge_id?: string
defense_selection_policy?: 'latest_best' | 'random_active' | 'round_robin' | 'request_new_if_none'
attack_selection_policy?: 'latest_best' | 'random_active' | 'round_robin' | 'request_new_if_none'
scoring_strategy?: 'red_blue_ratio' | 'custom'
inputs?: {
team_choice?: ValueSelector
attack_prompt?: ValueSelector
defense_prompt?: ValueSelector
}
}>
export const TEAM_CHALLENGE_BLOCK_TYPE = BlockEnum.TeamChallenge