Pipeline UX: clickable cards, wizard edit, routing rules redesign, category quotas
All checks were successful
Build and Push Docker Image / build (push) Successful in 18s

- Simplify pipeline list cards: whole card is clickable, remove clutter
- Add wizard edit page for existing pipelines with full state pre-population
- Extract toWizardTrackConfig to shared utility for reuse
- Rewrite predicate builder with 3 modes: Simple (sentence-style), AI (NLP), Advanced (JSON)
- Fix routing operators to match backend (eq/neq/in/contains/gt/lt)
- Rewrite routing rules editor with collapsible cards and natural language summaries
- Add parseNaturalLanguageRule AI procedure for routing rules
- Add per-category quotas to SelectionConfig and EvaluationConfig
- Add category quota UI toggles to selection and assignment sections
- Add category breakdown display to selection panel
- Add category-aware scoring to smart assignment (penalty/bonus)
- Add category-aware filtering targets with excess demotion

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt
2026-02-14 20:10:24 +01:00
parent c634982835
commit 382570cebd
17 changed files with 2577 additions and 1095 deletions

View File

@@ -20,6 +20,7 @@ import {
} from '@/components/ui/dropdown-menu'
import { toast } from 'sonner'
import { cn } from '@/lib/utils'
import type { Route } from 'next'
import {
ArrowLeft,
MoreHorizontal,
@@ -30,6 +31,7 @@ import {
Loader2,
ChevronDown,
Save,
Wand2,
} from 'lucide-react'
import { InlineEditableText } from '@/components/ui/inline-editable-text'
@@ -41,8 +43,8 @@ import { AwardsSection } from '@/components/admin/pipeline/sections/awards-secti
import { NotificationsSection } from '@/components/admin/pipeline/sections/notifications-section'
import { RoutingRulesEditor } from '@/components/admin/pipeline/routing-rules-editor'
import { AwardGovernanceEditor } from '@/components/admin/pipeline/award-governance-editor'
import { normalizeStageConfig } from '@/lib/stage-config-schema'
import { defaultNotificationConfig } from '@/lib/pipeline-defaults'
import { toWizardTrackConfig } from '@/lib/pipeline-conversions'
import type { WizardTrackConfig } from '@/types/pipeline-wizard'
const statusColors: Record<string, string> = {
@@ -52,71 +54,6 @@ const statusColors: Record<string, string> = {
CLOSED: 'bg-blue-100 text-blue-700',
}
function toWizardTrackConfig(
track: {
id: string
name: string
slug: string
kind: 'MAIN' | 'AWARD' | 'SHOWCASE'
sortOrder: number
routingMode: 'PARALLEL' | 'EXCLUSIVE' | 'POST_MAIN' | null
decisionMode:
| 'JURY_VOTE'
| 'AWARD_MASTER_DECISION'
| 'ADMIN_DECISION'
| null
stages: Array<{
id: string
name: string
slug: string
stageType:
| 'INTAKE'
| 'FILTER'
| 'EVALUATION'
| 'SELECTION'
| 'LIVE_FINAL'
| 'RESULTS'
sortOrder: number
configJson: unknown
}>
specialAward?: {
name: string
description: string | null
scoringMode: 'PICK_WINNER' | 'RANKED' | 'SCORED'
} | null
}
): WizardTrackConfig {
return {
id: track.id,
name: track.name,
slug: track.slug,
kind: track.kind,
sortOrder: track.sortOrder,
routingModeDefault: track.routingMode ?? undefined,
decisionMode: track.decisionMode ?? undefined,
stages: track.stages
.map((stage) => ({
id: stage.id,
name: stage.name,
slug: stage.slug,
stageType: stage.stageType,
sortOrder: stage.sortOrder,
configJson: normalizeStageConfig(
stage.stageType,
stage.configJson as Record<string, unknown> | null
),
}))
.sort((a, b) => a.sortOrder - b.sortOrder),
awardConfig: track.specialAward
? {
name: track.specialAward.name,
description: track.specialAward.description ?? undefined,
scoringMode: track.specialAward.scoringMode,
}
: undefined,
}
}
export default function PipelineDetailPage() {
const params = useParams()
const pipelineId = params.id as string
@@ -450,6 +387,13 @@ export default function PipelineDetailPage() {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem asChild>
<Link href={`/admin/rounds/pipeline/${pipelineId}/wizard` as Route}>
<Wand2 className="h-4 w-4 mr-2" />
Edit in Wizard
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
{pipeline.status === 'DRAFT' && (
<DropdownMenuItem
disabled={publishMutation.isPending}
@@ -660,10 +604,6 @@ export default function PipelineDetailPage() {
{/* Routing Rules (only if multiple tracks) */}
{hasMultipleTracks && (
<div>
<h2 className="text-lg font-semibold border-b pb-2 mb-4">Routing Rules</h2>
<p className="text-sm text-muted-foreground mb-4">
Define conditions for routing projects between tracks.
</p>
<RoutingRulesEditor
pipelineId={pipelineId}
tracks={trackOptionsForEditors}