Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 3 additions & 19 deletions src/apps/admin/src/ai/topscout-rag/IndexedChallengesPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
Button,
IconOutline,
InputSelect,
InputSelectOption,
InputWrapper,
Table,
TableColumn,
Expand All @@ -32,28 +31,13 @@ import {
IndexedChallenge,
} from '../../lib/services/rag-index.service'

import { INDEXED_TRACK_OPTIONS, INDEXED_TYPE_OPTIONS } from './ingest-options'
import styles from './IndexedChallengesPanel.module.scss'

const stopPropagation: MouseEventHandler = ev => ev.stopPropagation()

const PER_PAGE = 10

const TRACK_OPTIONS: InputSelectOption[] = [
{ label: 'All tracks', value: '' },
{ label: 'Development', value: 'Development' },
{ label: 'Design', value: 'Design' },
{ label: 'Data Science', value: 'Data Science' },
{ label: 'Quality Assurance', value: 'Quality Assurance' },
]

const TYPE_OPTIONS: InputSelectOption[] = [
{ label: 'All types', value: '' },
{ label: 'Challenge', value: 'Challenge' },
{ label: 'First2Finish', value: 'First2Finish' },
{ label: 'Marathon Match', value: 'Marathon Match' },
{ label: 'Task', value: 'Task' },
]

interface Filters {
search: string
projectId: string
Expand Down Expand Up @@ -390,15 +374,15 @@ export const IndexedChallengesPanel: FC<IndexedChallengesPanelProps> = props =>
<InputSelect
name='filterTrack'
label=''
options={TRACK_OPTIONS}
options={INDEXED_TRACK_OPTIONS}
value={filters.track}
onChange={handleFilterChange('track')}
tabIndex={0}
/>
<InputSelect
name='filterType'
label=''
options={TYPE_OPTIONS}
options={INDEXED_TYPE_OPTIONS}
value={filters.type}
onChange={handleFilterChange('type')}
tabIndex={0}
Expand Down
41 changes: 9 additions & 32 deletions src/apps/admin/src/ai/topscout-rag/IngestChallengesPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,41 +13,18 @@ import {
ingestChallengeInRag,
WorkflowPollTimeoutError,
} from '~/libs/shared'
import { Button, InputSelect, InputSelectOption } from '~/libs/ui'
import { Button, InputSelect } from '~/libs/ui'
import FormToggleSwitch from '~/libs/ui/lib/components/form/form-groups/form-toggle-switch'

import { Field, TextField } from './FormFields'
import {
BULK_STATUS_OPTIONS,
BULK_TRACK_OPTIONS,
BULK_TYPE_OPTIONS,
} from './ingest-options'
import { IngestionRun, IngestionRunSummary } from './IngestionRunSummary'
import styles from './IngestChallengesPanel.module.scss'

/**
* tc-ai-api treats tracks and types as free-form strings (its own config
* documents them as informational, not enums), so these are conveniences for
* the common cases — never validation.
*/
const TRACK_OPTIONS: InputSelectOption[] = [
{ label: 'Any track', value: '' },
{ label: 'Development', value: 'Development' },
{ label: 'Design', value: 'Design' },
{ label: 'Data Science', value: 'Data Science' },
{ label: 'Quality Assurance', value: 'Quality Assurance' },
]

const TYPE_OPTIONS: InputSelectOption[] = [
{ label: 'Any type', value: '' },
{ label: 'Challenge', value: 'Challenge' },
{ label: 'First2Finish', value: 'First2Finish' },
{ label: 'Marathon Match', value: 'Marathon Match' },
{ label: 'Task', value: 'Task' },
]

/** Mirrors the workflow's own default status set. */
const STATUS_OPTIONS: InputSelectOption[] = [
{ label: 'Active + Completed', value: '' },
{ label: 'Active only', value: 'ACTIVE' },
{ label: 'Completed only', value: 'COMPLETED' },
]

interface BulkFormState {
projectId: string
track: string
Expand Down Expand Up @@ -221,7 +198,7 @@ export const IngestChallengesPanel: FC<IngestChallengesPanelProps> = props => {
<InputSelect
name='track'
label=''
options={TRACK_OPTIONS}
options={BULK_TRACK_OPTIONS}
value={bulk.track}
onChange={handleBulkChange('track')}
disabled={bulkDisabled}
Expand All @@ -232,7 +209,7 @@ export const IngestChallengesPanel: FC<IngestChallengesPanelProps> = props => {
<InputSelect
name='type'
label=''
options={TYPE_OPTIONS}
options={BULK_TYPE_OPTIONS}
value={bulk.type}
onChange={handleBulkChange('type')}
disabled={bulkDisabled}
Expand All @@ -243,7 +220,7 @@ export const IngestChallengesPanel: FC<IngestChallengesPanelProps> = props => {
<InputSelect
name='status'
label=''
options={STATUS_OPTIONS}
options={BULK_STATUS_OPTIONS}
value={bulk.status}
onChange={handleBulkChange('status')}
disabled={bulkDisabled}
Expand Down
51 changes: 51 additions & 0 deletions src/apps/admin/src/ai/topscout-rag/ingest-options.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/* eslint-disable import/no-extraneous-dependencies */
import {
BULK_TRACK_OPTIONS,
INDEXED_TRACK_OPTIONS,
} from './ingest-options'

jest.mock('~/libs/ui', () => ({}), { virtual: true })

describe('ingest-options', () => {
describe('BULK_TRACK_OPTIONS', () => {
// The v6 Challenges API resolves `tracks` against challengeTrack.abbreviation,
// and an unmatched value silently widens the filter to every track rather
// than erroring — so these are pinned rather than left to drift.
it.each([
['Development', 'Dev'],
['Data Science', 'DS'],
['Design', 'Des'],
['Quality Assurance', 'QA'],
])('sends the %s abbreviation %s to the search API', (label, abbreviation) => {
expect(BULK_TRACK_OPTIONS.find(option => option.label === label)?.value)
.toBe(abbreviation)
})

it('keeps an unfiltered option that sends nothing', () => {
expect(BULK_TRACK_OPTIONS[0])
.toEqual({ label: 'Any track', value: '' })
})
})

describe('INDEXED_TRACK_OPTIONS', () => {
// These match stored chunk metadata, which holds the track's full name.
it.each([
'Development',
'Data Science',
'Design',
'Quality Assurance',
])('filters stored metadata by the full name %s', name => {
expect(INDEXED_TRACK_OPTIONS.find(option => option.label === name)?.value)
.toBe(name)
})
})

it('deliberately uses different values for the two surfaces', () => {
// Guards against someone "aligning" the two lists: one talks to the
// search API (abbreviations), the other to the vector index (names).
const bulk = BULK_TRACK_OPTIONS.map(option => option.value)
const indexed = INDEXED_TRACK_OPTIONS.map(option => option.value)

expect(bulk).not.toEqual(indexed)
})
})
68 changes: 68 additions & 0 deletions src/apps/admin/src/ai/topscout-rag/ingest-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { InputSelectOption } from '~/libs/ui'

/**
* Track/type vocabularies for this page.
*
* These deliberately differ between the two panels, and the difference is not
* cosmetic:
*
* - **Ingest** filters reach the v6 Challenges API (bulk ingestion paginates
* `searchChallengesTool`), which resolves `tracks`/`types` against the
* `challengeTrack`/`challengeType` tables' **abbreviation** column. Sending
* "Development" there matches no row, so the filter silently widens to
* "every track" instead of erroring.
* - **Indexed Challenges** filters query stored chunk metadata, where
* ingestion recorded the track's full **name** (`toFreeFormName(challenge.track)`
* in challenge-ingestion-workflow.ts). Sending "Dev" there matches nothing.
*
* So: abbreviations going out to the search API, full names coming back from
* the index. Do not "align" the two lists.
*/
export const BULK_TRACK_OPTIONS: InputSelectOption[] = [
{ label: 'Any track', value: '' },
{ label: 'Development', value: 'Dev' },
{ label: 'Design', value: 'Des' },
{ label: 'Data Science', value: 'DS' },
{ label: 'Quality Assurance', value: 'QA' },
]

/**
* Challenge types are resolved by abbreviation too, but unlike tracks the
* abbreviations here are unconfirmed — they are rows in `challengeType`, not
* constants in any repo. These values are the type *names*; if bulk ingestion
* ignores a type filter, this list is the first place to look.
*/
export const BULK_TYPE_OPTIONS: InputSelectOption[] = [
{ label: 'Any type', value: '' },
{ label: 'Challenge', value: 'Challenge' },
{ label: 'First2Finish', value: 'First2Finish' },
{ label: 'Marathon Match', value: 'Marathon Match' },
{ label: 'Task', value: 'Task' },
]

/** Mirrors the workflow's own default status set. */
export const BULK_STATUS_OPTIONS: InputSelectOption[] = [
{ label: 'Active + Completed', value: '' },
{ label: 'Active only', value: 'ACTIVE' },
{ label: 'Completed only', value: 'COMPLETED' },
]

/**
* Indexed-challenge filters match stored metadata, so these carry the track's
* full name — see the note on BULK_TRACK_OPTIONS.
*/
export const INDEXED_TRACK_OPTIONS: InputSelectOption[] = [
{ label: 'All tracks', value: '' },
{ label: 'Development', value: 'Development' },
{ label: 'Design', value: 'Design' },
{ label: 'Data Science', value: 'Data Science' },
{ label: 'Quality Assurance', value: 'Quality Assurance' },
]

export const INDEXED_TYPE_OPTIONS: InputSelectOption[] = [
{ label: 'All types', value: '' },
{ label: 'Challenge', value: 'Challenge' },
{ label: 'First2Finish', value: 'First2Finish' },
{ label: 'Marathon Match', value: 'Marathon Match' },
{ label: 'Task', value: 'Task' },
]
Loading