diff --git a/src/app/(admin)/admin/rounds/[roundId]/page.tsx b/src/app/(admin)/admin/rounds/[roundId]/page.tsx index dc06194..f17e641 100644 --- a/src/app/(admin)/admin/rounds/[roundId]/page.tsx +++ b/src/app/(admin)/admin/rounds/[roundId]/page.tsx @@ -1293,7 +1293,13 @@ export default function RoundDetailPage() { {aiRecommendations && ( setAiRecommendations(null)} + onApplied={() => { + setAiRecommendations(null) + utils.roundEngine.getProjectStates.invalidate({ roundId }) + }} /> )} @@ -2079,7 +2085,7 @@ function RoundUnassignedQueue({ roundId, requiredReviews = 3 }: { roundId: strin ? 'bg-red-50 text-red-700 border-red-200' : 'bg-amber-50 text-amber-700 border-amber-200', )}> - {project.assignmentCount || 0} / 3 + {project.assignmentCount || 0} / {requiredReviews} ))} @@ -3033,12 +3039,75 @@ type RecommendationItem = { function AIRecommendationsDisplay({ recommendations, + projectStates, + roundId, onClear, + onApplied, }: { recommendations: { STARTUP: RecommendationItem[]; BUSINESS_CONCEPT: RecommendationItem[] } + projectStates: any[] | undefined + roundId: string onClear: () => void + onApplied: () => void }) { const [expandedId, setExpandedId] = useState(null) + const [applying, setApplying] = useState(false) + + // Initialize selected with all recommended project IDs + const allRecommendedIds = useMemo(() => { + const ids = new Set() + for (const item of recommendations.STARTUP) ids.add(item.projectId) + for (const item of recommendations.BUSINESS_CONCEPT) ids.add(item.projectId) + return ids + }, [recommendations]) + + const [selectedIds, setSelectedIds] = useState>(() => new Set(allRecommendedIds)) + + // Build projectId → title map from projectStates + const projectTitleMap = useMemo(() => { + const map = new Map() + if (projectStates) { + for (const ps of projectStates) { + if (ps.project?.id && ps.project?.title) { + map.set(ps.project.id, ps.project.title) + } + } + } + return map + }, [projectStates]) + + const transitionMutation = trpc.roundEngine.transitionProject.useMutation() + + const toggleProject = (projectId: string) => { + setSelectedIds((prev) => { + const next = new Set(prev) + if (next.has(projectId)) next.delete(projectId) + else next.add(projectId) + return next + }) + } + + const selectedStartups = recommendations.STARTUP.filter((item) => selectedIds.has(item.projectId)).length + const selectedConcepts = recommendations.BUSINESS_CONCEPT.filter((item) => selectedIds.has(item.projectId)).length + + const handleApply = async () => { + setApplying(true) + try { + // Transition all selected projects to PASSED + const promises = Array.from(selectedIds).map((projectId) => + transitionMutation.mutateAsync({ projectId, roundId, newState: 'PASSED' }).catch(() => { + // Project might already be PASSED — that's OK + }) + ) + await Promise.all(promises) + toast.success(`Marked ${selectedIds.size} project(s) as passed`) + onApplied() + } catch (error) { + toast.error('Failed to apply recommendations') + } finally { + setApplying(false) + } + } const renderCategory = (label: string, items: RecommendationItem[], colorClass: string) => { if (items.length === 0) return ( @@ -3051,33 +3120,46 @@ function AIRecommendationsDisplay({
{items.map((item) => { const isExpanded = expandedId === `${item.category}-${item.projectId}` + const isSelected = selectedIds.has(item.projectId) + const projectTitle = projectTitleMap.get(item.projectId) || item.projectId + return (
- +
+ toggleProject(item.projectId)} + className="shrink-0" + /> + +
{isExpanded && (
@@ -3114,7 +3196,7 @@ function AIRecommendationsDisplay({
AI Shortlist Recommendations - Ranked independently per category — {recommendations.STARTUP.length} startups, {recommendations.BUSINESS_CONCEPT.length} concepts + Ranked independently per category — {selectedStartups} of {recommendations.STARTUP.length} startups, {selectedConcepts} of {recommendations.BUSINESS_CONCEPT.length} concepts selected
- +

@@ -3140,6 +3222,24 @@ function AIRecommendationsDisplay({ {renderCategory('Business Concept', recommendations.BUSINESS_CONCEPT, 'bg-purple-500')}

+ + {/* Apply button */} +
+

+ {selectedIds.size} project{selectedIds.size !== 1 ? 's' : ''} will be marked as Passed +

+ +
) diff --git a/src/components/admin/assignment/assignment-preview-sheet.tsx b/src/components/admin/assignment/assignment-preview-sheet.tsx index b0f4ffc..3b5a40b 100644 --- a/src/components/admin/assignment/assignment-preview-sheet.tsx +++ b/src/components/admin/assignment/assignment-preview-sheet.tsx @@ -877,10 +877,26 @@ function AssignmentRow({ const a = assignment return ( -
-
+
+
{a.projectTitle} + {!a.isManual && a.score > 0 && ( + = 50 + ? 'border-green-300 text-green-700' + : a.score >= 25 + ? 'border-amber-300 text-amber-700' + : 'border-red-300 text-red-700', + )} + > + + {Math.round(a.score)} + + )} {a.isManual ? ( - {/* Score + tags + reasoning */} -
- {!a.isManual && a.score > 0 && ( - - - - = 50 - ? 'border-green-300 text-green-700' - : a.score >= 25 - ? 'border-amber-300 text-amber-700' - : 'border-red-300 text-red-700', - )} - > - - {Math.round(a.score)} - - - -

Match Score Breakdown

-
    - {a.reasoning.map((r, i) => ( -
  • • {r}
  • - ))} -
-
-
-
- )} - {a.matchingTags.slice(0, 3).map((tag) => ( - - - {tag} - - ))} - {a.matchingTags.length > 3 && ( - - +{a.matchingTags.length - 3} more - - )} -
+ {/* AI reasoning — displayed directly */} + {!a.isManual && a.reasoning.length > 0 && a.reasoning[0] !== 'Manually added by admin' && ( +

+ {a.reasoning.join(' ')} +

+ )} + + {/* Tags */} + {a.matchingTags.length > 0 && ( +
+ {a.matchingTags.slice(0, 4).map((tag) => ( + + + {tag} + + ))} + {a.matchingTags.length > 4 && ( + + +{a.matchingTags.length - 4} more + + )} +
+ )} {/* Policy violations */} {a.policyViolations.length > 0 && ( diff --git a/src/types/competition-configs.ts b/src/types/competition-configs.ts index 9d7336f..3ff0bcb 100644 --- a/src/types/competition-configs.ts +++ b/src/types/competition-configs.ts @@ -107,6 +107,7 @@ export const EvaluationConfigSchema = z.object({ aiSummaryEnabled: z.boolean().default(false), generateAiShortlist: z.boolean().default(false), + aiParseFiles: z.boolean().default(false), advancementMode: z .enum(['auto_top_n', 'admin_selection', 'ai_recommended'])