feat: add challenge and red-blue competitions across API and web
This commit is contained in:
parent
f5161d9add
commit
8fd3c4bb64
77 changed files with 5355 additions and 24 deletions
42
web/app/components/workflow/nodes/team-challenge/default.ts
Normal file
42
web/app/components/workflow/nodes/team-challenge/default.ts
Normal 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
|
||||
25
web/app/components/workflow/nodes/team-challenge/node.tsx
Normal file
25
web/app/components/workflow/nodes/team-challenge/node.tsx
Normal 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)
|
||||
106
web/app/components/workflow/nodes/team-challenge/panel.tsx
Normal file
106
web/app/components/workflow/nodes/team-challenge/panel.tsx
Normal 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)
|
||||
16
web/app/components/workflow/nodes/team-challenge/types.ts
Normal file
16
web/app/components/workflow/nodes/team-challenge/types.ts
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue