Add background filtering jobs, improved date picker, AI reasoning display

- Implement background job system for AI filtering to avoid HTTP timeouts
- Add FilteringJob model to track progress of long-running filtering operations
- Add real-time progress polling for filtering operations on round details page
- Create custom DateTimePicker component with calendar popup (no year picker hassle)
- Fix round date persistence bug (refetchOnWindowFocus was resetting form state)
- Integrate filtering controls into round details page for filtering rounds
- Display AI reasoning for flagged/filtered projects in results table
- Add onboarding system scaffolding (schema, routes, basic UI)
- Allow setting round dates in the past for manual overrides

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-03 19:48:41 +01:00
parent 8be740a4fb
commit e2782b2b19
24 changed files with 3692 additions and 443 deletions

View File

@@ -33,7 +33,7 @@ import {
} from '@/components/forms/evaluation-form-builder'
import { RoundTypeSettings } from '@/components/forms/round-type-settings'
import { ArrowLeft, Loader2, AlertCircle, AlertTriangle } from 'lucide-react'
import { format } from 'date-fns'
import { DateTimePicker } from '@/components/ui/datetime-picker'
interface PageProps {
params: Promise<{ id: string }>
@@ -43,13 +43,13 @@ const updateRoundSchema = z
.object({
name: z.string().min(1, 'Name is required').max(255),
requiredReviews: z.number().int().min(1).max(10),
votingStartAt: z.string().optional(),
votingEndAt: z.string().optional(),
votingStartAt: z.date().nullable().optional(),
votingEndAt: z.date().nullable().optional(),
})
.refine(
(data) => {
if (data.votingStartAt && data.votingEndAt) {
return new Date(data.votingEndAt) > new Date(data.votingStartAt)
return data.votingEndAt > data.votingStartAt
}
return true
},
@@ -61,25 +61,19 @@ const updateRoundSchema = z
type UpdateRoundForm = z.infer<typeof updateRoundSchema>
// Convert ISO date to datetime-local format
function toDatetimeLocal(date: Date | string | null | undefined): string {
if (!date) return ''
const d = new Date(date)
// Format: YYYY-MM-DDTHH:mm
return format(d, "yyyy-MM-dd'T'HH:mm")
}
function EditRoundContent({ roundId }: { roundId: string }) {
const router = useRouter()
const [criteria, setCriteria] = useState<Criterion[]>([])
const [criteriaInitialized, setCriteriaInitialized] = useState(false)
const [formInitialized, setFormInitialized] = useState(false)
const [roundType, setRoundType] = useState<'FILTERING' | 'EVALUATION' | 'LIVE_EVENT'>('EVALUATION')
const [roundSettings, setRoundSettings] = useState<Record<string, unknown>>({})
// Fetch round data
const { data: round, isLoading: loadingRound } = trpc.round.get.useQuery({
id: roundId,
})
// Fetch round data - disable refetch on focus to prevent overwriting user's edits
const { data: round, isLoading: loadingRound } = trpc.round.get.useQuery(
{ id: roundId },
{ refetchOnWindowFocus: false }
)
// Fetch evaluation form
const { data: evaluationForm, isLoading: loadingForm } =
@@ -110,25 +104,26 @@ function EditRoundContent({ roundId }: { roundId: string }) {
defaultValues: {
name: '',
requiredReviews: 3,
votingStartAt: '',
votingEndAt: '',
votingStartAt: null,
votingEndAt: null,
},
})
// Update form when round data loads
// Update form when round data loads - only initialize once
useEffect(() => {
if (round) {
if (round && !formInitialized) {
form.reset({
name: round.name,
requiredReviews: round.requiredReviews,
votingStartAt: toDatetimeLocal(round.votingStartAt),
votingEndAt: toDatetimeLocal(round.votingEndAt),
votingStartAt: round.votingStartAt ? new Date(round.votingStartAt) : null,
votingEndAt: round.votingEndAt ? new Date(round.votingEndAt) : null,
})
// Set round type and settings
setRoundType((round.roundType as typeof roundType) || 'EVALUATION')
setRoundSettings((round.settingsJson as Record<string, unknown>) || {})
setFormInitialized(true)
}
}, [round, form])
}, [round, form, formInitialized])
// Initialize criteria from evaluation form
useEffect(() => {
@@ -151,8 +146,8 @@ function EditRoundContent({ roundId }: { roundId: string }) {
requiredReviews: data.requiredReviews,
roundType,
settingsJson: roundSettings,
votingStartAt: data.votingStartAt ? new Date(data.votingStartAt) : null,
votingEndAt: data.votingEndAt ? new Date(data.votingEndAt) : null,
votingStartAt: data.votingStartAt ?? null,
votingEndAt: data.votingEndAt ?? null,
})
// Update evaluation form if criteria changed and no evaluations exist
@@ -303,7 +298,11 @@ function EditRoundContent({ roundId }: { roundId: string }) {
<FormItem>
<FormLabel>Start Date & Time</FormLabel>
<FormControl>
<Input type="datetime-local" {...field} />
<DateTimePicker
value={field.value}
onChange={field.onChange}
placeholder="Select start date & time"
/>
</FormControl>
<FormMessage />
</FormItem>
@@ -317,7 +316,11 @@ function EditRoundContent({ roundId }: { roundId: string }) {
<FormItem>
<FormLabel>End Date & Time</FormLabel>
<FormControl>
<Input type="datetime-local" {...field} />
<DateTimePicker
value={field.value}
onChange={field.onChange}
placeholder="Select end date & time"
/>
</FormControl>
<FormMessage />
</FormItem>
@@ -326,7 +329,7 @@ function EditRoundContent({ roundId }: { roundId: string }) {
</div>
<p className="text-sm text-muted-foreground">
Leave empty to disable the voting window enforcement.
Leave empty to disable the voting window enforcement. Past dates are allowed.
</p>
</CardContent>
</Card>