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
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:
@@ -60,7 +60,7 @@ type UploadState = {
|
||||
type UploadMap = Record<string, UploadState>
|
||||
|
||||
export default function BulkUploadPage() {
|
||||
const [windowId, setWindowId] = useState('')
|
||||
const [roundId, setRoundId] = useState('')
|
||||
const [search, setSearch] = useState('')
|
||||
const [debouncedSearch, setDebouncedSearch] = useState('')
|
||||
const [statusFilter, setStatusFilter] = useState<'all' | 'missing' | 'complete'>('all')
|
||||
@@ -96,20 +96,20 @@ export default function BulkUploadPage() {
|
||||
}, [])
|
||||
|
||||
// Queries
|
||||
const { data: windows, isLoading: windowsLoading } = trpc.file.listSubmissionWindows.useQuery()
|
||||
const { data: rounds, isLoading: roundsLoading } = trpc.file.listRoundsForBulkUpload.useQuery()
|
||||
|
||||
const { data, isLoading, refetch } = trpc.file.listProjectsWithUploadStatus.useQuery(
|
||||
const { data, isLoading, refetch } = trpc.file.listProjectsByRoundRequirements.useQuery(
|
||||
{
|
||||
submissionWindowId: windowId,
|
||||
roundId,
|
||||
search: debouncedSearch || undefined,
|
||||
status: statusFilter,
|
||||
page,
|
||||
pageSize: perPage,
|
||||
},
|
||||
{ enabled: !!windowId }
|
||||
{ enabled: !!roundId }
|
||||
)
|
||||
|
||||
const uploadMutation = trpc.file.adminUploadForRequirement.useMutation()
|
||||
const uploadMutation = trpc.file.adminUploadForRoundRequirement.useMutation()
|
||||
|
||||
// Upload a single file for a project requirement
|
||||
const uploadFileForRequirement = useCallback(
|
||||
@@ -117,7 +117,7 @@ export default function BulkUploadPage() {
|
||||
projectId: string,
|
||||
requirementId: string,
|
||||
file: File,
|
||||
submissionWindowId: string
|
||||
targetRoundId: string
|
||||
) => {
|
||||
const key = `${projectId}:${requirementId}`
|
||||
setUploads((prev) => ({
|
||||
@@ -131,8 +131,8 @@ export default function BulkUploadPage() {
|
||||
fileName: file.name,
|
||||
mimeType: file.type || 'application/octet-stream',
|
||||
size: file.size,
|
||||
submissionWindowId,
|
||||
submissionFileRequirementId: requirementId,
|
||||
roundId: targetRoundId,
|
||||
requirementId,
|
||||
})
|
||||
|
||||
// XHR upload with progress
|
||||
@@ -186,18 +186,18 @@ export default function BulkUploadPage() {
|
||||
}
|
||||
input.onchange = (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0]
|
||||
if (file && windowId) {
|
||||
uploadFileForRequirement(projectId, requirementId, file, windowId)
|
||||
if (file && roundId) {
|
||||
uploadFileForRequirement(projectId, requirementId, file, roundId)
|
||||
}
|
||||
}
|
||||
input.click()
|
||||
},
|
||||
[windowId, uploadFileForRequirement]
|
||||
[roundId, uploadFileForRequirement]
|
||||
)
|
||||
|
||||
// Handle bulk row upload
|
||||
const handleBulkUploadAll = useCallback(async () => {
|
||||
if (!bulkProject || !windowId) return
|
||||
if (!bulkProject || !roundId) return
|
||||
|
||||
const entries = Object.entries(bulkFiles).filter(
|
||||
([, file]) => file !== null
|
||||
@@ -211,14 +211,14 @@ export default function BulkUploadPage() {
|
||||
// Upload all in parallel
|
||||
await Promise.allSettled(
|
||||
entries.map(([reqId, file]) =>
|
||||
uploadFileForRequirement(bulkProject.id, reqId, file, windowId)
|
||||
uploadFileForRequirement(bulkProject.id, reqId, file, roundId)
|
||||
)
|
||||
)
|
||||
|
||||
setBulkProject(null)
|
||||
setBulkFiles({})
|
||||
toast.success('Bulk upload complete')
|
||||
}, [bulkProject, bulkFiles, windowId, uploadFileForRequirement])
|
||||
}, [bulkProject, bulkFiles, roundId, uploadFileForRequirement])
|
||||
|
||||
const progressPercent =
|
||||
data && data.totalProjects > 0
|
||||
@@ -242,32 +242,37 @@ export default function BulkUploadPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Window Selector */}
|
||||
{/* Round Selector */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Submission Window</CardTitle>
|
||||
<CardTitle className="text-base">Round</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{windowsLoading ? (
|
||||
{roundsLoading ? (
|
||||
<Skeleton className="h-10 w-full" />
|
||||
) : !rounds || rounds.length === 0 ? (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<span>No rounds have file requirements configured. Add file requirements to a round first.</span>
|
||||
</div>
|
||||
) : (
|
||||
<Select
|
||||
value={windowId}
|
||||
value={roundId}
|
||||
onValueChange={(v) => {
|
||||
setWindowId(v)
|
||||
setRoundId(v)
|
||||
setPage(1)
|
||||
setUploads({})
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a submission window..." />
|
||||
<SelectValue placeholder="Select a round..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{windows?.map((w) => (
|
||||
<SelectItem key={w.id} value={w.id}>
|
||||
{w.competition.program.name} {w.competition.program.year} — {w.name}{' '}
|
||||
({w.fileRequirements.length} requirement
|
||||
{w.fileRequirements.length !== 1 ? 's' : ''})
|
||||
{rounds.map((r) => (
|
||||
<SelectItem key={r.id} value={r.id}>
|
||||
{r.competition.program.name} {r.competition.program.year} — {r.name}{' '}
|
||||
({r.fileRequirements.length} requirement
|
||||
{r.fileRequirements.length !== 1 ? 's' : ''})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -276,8 +281,8 @@ export default function BulkUploadPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Content (only if window selected) */}
|
||||
{windowId && data && (
|
||||
{/* Content (only if round selected) */}
|
||||
{roundId && data && (
|
||||
<>
|
||||
{/* Progress Summary */}
|
||||
<Card>
|
||||
|
||||
@@ -92,7 +92,7 @@ function ImportPageContent() {
|
||||
Create a competition with rounds before importing projects
|
||||
</p>
|
||||
<Button asChild className="mt-4">
|
||||
<Link href="/admin/competitions">View Competitions</Link>
|
||||
<Link href="/admin/rounds">View Rounds</Link>
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user