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

@@ -22,7 +22,16 @@ import {
type AnonymizedProjectForAI,
type ProjectAIMapping,
} from './anonymization'
import type { Prisma, FileType, SubmissionSource } from '@prisma/client'
import type { Prisma, FileType, SubmissionSource, PrismaClient } from '@prisma/client'
// ─── Progress Callback Type ─────────────────────────────────────────────────
export type ProgressCallback = (progress: {
currentBatch: number
totalBatches: number
processedCount: number
tokensUsed?: number
}) => Promise<void>
// ─── Types ──────────────────────────────────────────────────────────────────
@@ -410,7 +419,8 @@ export async function executeAIScreening(
config: AIScreeningConfig,
projects: ProjectForFiltering[],
userId?: string,
entityId?: string
entityId?: string,
onProgress?: ProgressCallback
): Promise<Map<string, AIScreeningResult>> {
const results = new Map<string, AIScreeningResult>()
@@ -444,13 +454,15 @@ export async function executeAIScreening(
}
let totalTokens = 0
const totalBatches = Math.ceil(anonymized.length / BATCH_SIZE)
// Process in batches
for (let i = 0; i < anonymized.length; i += BATCH_SIZE) {
const batchAnon = anonymized.slice(i, i + BATCH_SIZE)
const batchMappings = mappings.slice(i, i + BATCH_SIZE)
const currentBatch = Math.floor(i / BATCH_SIZE) + 1
console.log(`[AI Filtering] Processing batch ${Math.floor(i / BATCH_SIZE) + 1}/${Math.ceil(anonymized.length / BATCH_SIZE)}`)
console.log(`[AI Filtering] Processing batch ${currentBatch}/${totalBatches}`)
const { results: batchResults, tokensUsed } = await processAIBatch(
openai,
@@ -468,6 +480,16 @@ export async function executeAIScreening(
for (const [id, result] of batchResults) {
results.set(id, result)
}
// Report progress
if (onProgress) {
await onProgress({
currentBatch,
totalBatches,
processedCount: Math.min((currentBatch) * BATCH_SIZE, anonymized.length),
tokensUsed: totalTokens,
})
}
}
console.log(`[AI Filtering] Completed. Total tokens: ${totalTokens}`)
@@ -513,7 +535,8 @@ export async function executeFilteringRules(
rules: FilteringRuleInput[],
projects: ProjectForFiltering[],
userId?: string,
roundId?: string
roundId?: string,
onProgress?: ProgressCallback
): Promise<ProjectFilteringResult[]> {
const activeRules = rules
.filter((r) => r.isActive)
@@ -528,7 +551,7 @@ export async function executeFilteringRules(
for (const aiRule of aiRules) {
const config = aiRule.configJson as unknown as AIScreeningConfig
const screeningResults = await executeAIScreening(config, projects, userId, roundId)
const screeningResults = await executeAIScreening(config, projects, userId, roundId, onProgress)
aiResults.set(aiRule.id, screeningResults)
}