AI category-aware evaluation: per-round config, file parsing, shortlist, advance flow
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled

- Per-juror cap mode (HARD/SOFT/NONE) in add-member dialog and members table
- Jury invite flow: create user + add to group + send invitation from dialog
- Per-round config: notifyOnAdvance, aiParseFiles, startupAdvanceCount, conceptAdvanceCount
- Moved notify-on-advance from competition-level to per-round setting
- AI filtering: round-tagged files with newest-first sorting, optional file content extraction
- File content extractor service (pdf-parse for PDF, utf-8 for text files)
- AI shortlist runs independently per category (STARTUP / BUSINESS_CONCEPT)
- generateAIRecommendations tRPC endpoint with per-round config integration
- AI recommendations UI: trigger button, confirmation dialog, per-category results display
- Category-aware advance dialog: select/deselect projects by category with target caps
- STAGE_ACTIVE bug fix in assignment router

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-16 10:09:52 +01:00
parent 93f4ad4b31
commit 80c9e35971
21 changed files with 1886 additions and 1381 deletions

View File

@@ -4,6 +4,7 @@ import { Prisma } from '@prisma/client'
import { router, adminProcedure, protectedProcedure } from '../trpc'
import { logAudit } from '@/server/utils/audit'
import { validateRoundConfig, defaultRoundConfig } from '@/types/competition-configs'
import { generateShortlist } from '../services/ai-shortlist'
import {
openWindow,
closeWindow,
@@ -358,6 +359,74 @@ export const roundRouter = router({
}
}),
// =========================================================================
// AI Shortlist Recommendations
// =========================================================================
/**
* Generate AI-powered shortlist recommendations for a round.
* Runs independently for STARTUP and BUSINESS_CONCEPT categories.
* Uses per-round config for advancement targets and file parsing.
*/
generateAIRecommendations: adminProcedure
.input(
z.object({
roundId: z.string(),
rubric: z.string().optional(),
})
)
.mutation(async ({ ctx, input }) => {
const round = await ctx.prisma.round.findUniqueOrThrow({
where: { id: input.roundId },
select: {
id: true,
name: true,
competitionId: true,
configJson: true,
},
})
const config = (round.configJson as Record<string, unknown>) ?? {}
const startupTopN = (config.startupAdvanceCount as number) || 10
const conceptTopN = (config.conceptAdvanceCount as number) || 10
const aiParseFiles = !!config.aiParseFiles
const result = await generateShortlist(
{
roundId: input.roundId,
competitionId: round.competitionId,
startupTopN,
conceptTopN,
rubric: input.rubric,
aiParseFiles,
},
ctx.prisma,
)
await logAudit({
prisma: ctx.prisma,
userId: ctx.user.id,
action: 'AI_SHORTLIST',
entityType: 'Round',
entityId: input.roundId,
detailsJson: {
roundName: round.name,
startupTopN,
conceptTopN,
aiParseFiles,
success: result.success,
startupCount: result.recommendations.STARTUP.length,
conceptCount: result.recommendations.BUSINESS_CONCEPT.length,
tokensUsed: result.tokensUsed,
errors: result.errors,
},
ipAddress: ctx.ip,
userAgent: ctx.userAgent,
})
return result
}),
// =========================================================================
// Submission Window Management
// =========================================================================