Fix filtering config save, auto-save, streamed results, improved AI prompt
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m7s
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m7s
- Add missing fields to FilteringConfigSchema (aiParseFiles, startupAdvanceCount, conceptAdvanceCount, notifyOnEntry, notifyOnAdvance) — Zod was silently stripping them on save - Restore auto-save with 800ms debounce on config changes - Add staggered animations for filtering results (stream in one-by-one) - Improve AI screening prompt: file type label mappings, soft cap handling, missing documents = fail, better user prompt structure Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useMemo, useCallback, useRef } from 'react'
|
||||
import { useState, useMemo, useCallback, useRef, useEffect } from 'react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import type { Route } from 'next'
|
||||
@@ -397,6 +397,20 @@ export default function RoundDetailPage() {
|
||||
updateMutation.mutate({ id: roundId, configJson: config })
|
||||
}, [config, roundId, updateMutation])
|
||||
|
||||
// ── Auto-save: debounce config changes and save automatically ────────
|
||||
useEffect(() => {
|
||||
if (!configInitialized.current) return
|
||||
if (JSON.stringify(config) === JSON.stringify(serverConfig)) return
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setAutosaveStatus('saving')
|
||||
updateMutation.mutate({ id: roundId, configJson: config })
|
||||
}, 800)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [config])
|
||||
|
||||
// ── Computed values ────────────────────────────────────────────────────
|
||||
const projectCount = round?._count?.projectRoundStates ?? 0
|
||||
const stateCounts = useMemo(() =>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { useState, useEffect, useCallback, useRef, useMemo } from 'react'
|
||||
import { trpc } from '@/lib/trpc/client'
|
||||
import { toast } from 'sonner'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -70,6 +70,7 @@ import {
|
||||
ListFilter,
|
||||
Settings2,
|
||||
} from 'lucide-react'
|
||||
import { motion, AnimatePresence } from 'motion/react'
|
||||
import Link from 'next/link'
|
||||
import type { Route } from 'next'
|
||||
import { AwardShortlist } from './award-shortlist'
|
||||
@@ -401,6 +402,30 @@ export function FilteringDashboard({ competitionId, roundId }: FilteringDashboar
|
||||
)
|
||||
}) ?? []
|
||||
|
||||
// ── Staggered reveal: animate new results in one by one ─────────────
|
||||
const prevResultIdsRef = useRef<Set<string>>(new Set())
|
||||
const isFirstLoad = useRef(true)
|
||||
|
||||
const resultStagger = useMemo(() => {
|
||||
const stagger: Record<string, number> = {}
|
||||
if (isFirstLoad.current && displayResults.length > 0) {
|
||||
isFirstLoad.current = false
|
||||
return stagger // No stagger on first load — show immediately
|
||||
}
|
||||
let newIdx = 0
|
||||
for (const r of displayResults) {
|
||||
if (!prevResultIdsRef.current.has((r as any).id)) {
|
||||
stagger[(r as any).id] = newIdx++
|
||||
}
|
||||
}
|
||||
return stagger
|
||||
}, [displayResults])
|
||||
|
||||
// Update known IDs after render
|
||||
useEffect(() => {
|
||||
prevResultIdsRef.current = new Set(displayResults.map((r: any) => r.id))
|
||||
}, [displayResults])
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Main Card: AI Screening Criteria + Controls */}
|
||||
@@ -766,13 +791,26 @@ export function FilteringDashboard({ competitionId, roundId }: FilteringDashboar
|
||||
</div>
|
||||
|
||||
{/* Rows */}
|
||||
<AnimatePresence initial={false}>
|
||||
{displayResults.map((result: any) => {
|
||||
const ai = parseAIData(result.aiScreeningJson)
|
||||
const effectiveOutcome = result.finalOutcome || result.outcome
|
||||
const isExpanded = expandedId === result.id
|
||||
const staggerIdx = resultStagger[result.id]
|
||||
const isNew = staggerIdx !== undefined
|
||||
|
||||
return (
|
||||
<div key={result.id} className="border-b last:border-b-0 animate-in fade-in-0 duration-300">
|
||||
<motion.div
|
||||
key={result.id}
|
||||
initial={isNew ? { opacity: 0, y: 16, scale: 0.98 } : false}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
transition={{
|
||||
duration: 0.35,
|
||||
delay: isNew ? staggerIdx * 0.12 : 0,
|
||||
ease: [0.25, 0.46, 0.45, 0.94],
|
||||
}}
|
||||
className="border-b last:border-b-0"
|
||||
>
|
||||
{/* Main Row */}
|
||||
<div
|
||||
className="grid grid-cols-[40px_1fr_120px_100px_70px_70px_120px] gap-2 px-3 py-2.5 items-center hover:bg-muted/50 text-sm cursor-pointer"
|
||||
@@ -980,9 +1018,10 @@ export function FilteringDashboard({ competitionId, roundId }: FilteringDashboar
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Pagination */}
|
||||
{resultsPage && resultsPage.totalPages > 1 && (
|
||||
|
||||
@@ -149,7 +149,7 @@ const MAX_PARALLEL_BATCHES = 10
|
||||
const AI_SCREENING_SYSTEM_PROMPT = `You are an expert project screening assistant for an ocean conservation competition.
|
||||
|
||||
## Your Role
|
||||
Evaluate each project against the provided screening criteria. Be objective and base evaluation only on provided data.
|
||||
Evaluate each project against the provided screening criteria. Be objective and base evaluation only on provided data. Your goal is to identify projects that clearly FAIL the criteria — not to nitpick minor issues.
|
||||
|
||||
## Output Format
|
||||
Return a JSON object with this exact structure:
|
||||
@@ -179,12 +179,43 @@ Return a JSON object with this exact structure:
|
||||
- founded_year: when the company/initiative was founded (use for age checks)
|
||||
- ocean_issue: the ocean conservation area
|
||||
- file_count, file_types: uploaded documents summary
|
||||
- files[]: per-file details with file_type, page_count (if known), size_kb, detected_lang (ISO 639-3 language code like 'eng', 'fra'), lang_confidence (0-1), round_name (which round the file was submitted for), and is_current_round flag
|
||||
- files[]: per-file details (see File Type Reference below) with file_type, page_count (if known), size_kb, detected_lang (ISO 639-3 language code like 'eng', 'fra'), lang_confidence (0-1), round_name (which round the file was submitted for), and is_current_round flag
|
||||
- description: project summary text
|
||||
- tags: topic tags
|
||||
- If document content is provided (text_content field in files), use it for deeper analysis. Pay SPECIAL ATTENTION to files from the current round (is_current_round=true) as they are the most recent and relevant submissions.
|
||||
- If detected_lang is provided, use it to evaluate language requirements (e.g. 'eng' = English, 'fra' = French). lang_confidence indicates detection reliability.
|
||||
|
||||
## File Type Reference
|
||||
The file_type field uses these codes. When criteria mention document names, match them to the correct code:
|
||||
- EXEC_SUMMARY = Executive Summary
|
||||
- PRESENTATION = Pitch Deck / Presentation
|
||||
- BUSINESS_PLAN = Business Plan
|
||||
- VIDEO = Video
|
||||
- VIDEO_PITCH = Video Pitch
|
||||
- SUPPORTING_DOC = Supporting Document
|
||||
- OTHER = Other / miscellaneous document
|
||||
|
||||
IMPORTANT: Only evaluate page limits against the CORRECT document type. For example, a page limit on "executive summary" applies ONLY to files with file_type=EXEC_SUMMARY, not to pitch decks or other documents.
|
||||
|
||||
## How to Interpret Criteria
|
||||
- Treat criteria as reasonable guidelines written by a human, not as rigid legal rules.
|
||||
- When criteria say "soft cap", "approximately", "around", "about", or "reasonable amount", be LENIENT. A document that is 1-3 pages over a soft cap still meets criteria. Only flag if it is egregiously over (e.g. double the stated limit).
|
||||
- When criteria have NO softening language, treat them as firm but still use judgment — a 1-page overage on a hard limit is borderline, not an automatic fail.
|
||||
- Page count data may be unavailable (null) for some files. If page_count is null, do NOT penalize — only evaluate page limits when page_count is actually provided for that file.
|
||||
- Focus on the INTENT behind each criterion, not a hyper-literal reading.
|
||||
|
||||
## Missing Documents
|
||||
- If criteria mention document requirements (executive summary, pitch deck, etc.) and the project has ZERO files (file_count=0), this is a CLEAR FAIL. Every competition applicant is expected to upload documents.
|
||||
- "Where applicable" in criteria refers to edge cases within specific document types — it does NOT mean documents are optional.
|
||||
- A project with no uploaded documents at all should always have meets_criteria=FALSE unless the criteria explicitly say documents are optional.
|
||||
|
||||
## Decision Framework
|
||||
- Set meets_criteria=TRUE if the project satisfies the core intent of the criteria, even with minor imperfections (e.g. a few pages over a soft cap).
|
||||
- Set meets_criteria=FALSE when there is a clear, material violation: missing required documents entirely, startup far exceeding age limit, documents entirely in the wrong language, no visible ocean impact, etc.
|
||||
- When in doubt on a SOFT criterion (page lengths, formatting), set meets_criteria=TRUE with a lower confidence score and note the concern in reasoning.
|
||||
- When in doubt on a HARD criterion (age limits, language, having documents at all), set meets_criteria=FALSE.
|
||||
- Mention specific concerns in the reasoning field so the admin can review.
|
||||
|
||||
## Guidelines
|
||||
- Evaluate ONLY against the provided criteria, not your own standards
|
||||
- A confidence of 1.0 means absolute certainty; 0.5 means borderline
|
||||
@@ -192,7 +223,7 @@ Return a JSON object with this exact structure:
|
||||
- When criteria differ by category (e.g. stricter for STARTUP vs BUSINESS_CONCEPT), apply the appropriate threshold
|
||||
- When criteria mention regional considerations (e.g. African projects), use the country/region fields
|
||||
- Do not include any personal identifiers in reasoning
|
||||
- If project data is insufficient to evaluate, set confidence below 0.3`
|
||||
- If project data is insufficient to evaluate, set confidence below 0.3 and default meets_criteria to TRUE`
|
||||
|
||||
// ─── Field-Based Rule Evaluation ────────────────────────────────────────────
|
||||
|
||||
@@ -371,10 +402,16 @@ async function processAIBatch(
|
||||
// Sanitize user-supplied criteria
|
||||
const { sanitized: safeCriteria } = sanitizeUserInput(criteriaText)
|
||||
|
||||
// Build optimized prompt
|
||||
const userPrompt = `CRITERIA: ${safeCriteria}
|
||||
PROJECTS: ${JSON.stringify(anonymized)}
|
||||
Evaluate and return JSON.`
|
||||
// Build user prompt with clear structure
|
||||
const userPrompt = `## Screening Criteria
|
||||
The admin has defined the following requirements. Evaluate each project against ALL of these criteria:
|
||||
|
||||
${safeCriteria}
|
||||
|
||||
## Projects to Evaluate (${anonymized.length} total)
|
||||
${JSON.stringify(anonymized)}
|
||||
|
||||
Evaluate each project and return JSON with your assessment.`
|
||||
|
||||
const MAX_PARSE_RETRIES = 2
|
||||
let parseAttempts = 0
|
||||
|
||||
@@ -69,6 +69,12 @@ export const FilteringConfigSchema = z.object({
|
||||
autoAdvanceEligible: z.boolean().default(false),
|
||||
duplicateDetectionEnabled: z.boolean().default(true),
|
||||
batchSize: z.number().int().positive().default(20),
|
||||
|
||||
aiParseFiles: z.boolean().default(false),
|
||||
startupAdvanceCount: z.number().int().nonnegative().optional(),
|
||||
conceptAdvanceCount: z.number().int().nonnegative().optional(),
|
||||
notifyOnEntry: z.boolean().default(false),
|
||||
notifyOnAdvance: z.boolean().default(false),
|
||||
})
|
||||
|
||||
export type FilteringConfig = z.infer<typeof FilteringConfigSchema>
|
||||
|
||||
Reference in New Issue
Block a user