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:
@@ -12,6 +12,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Label } from '@/components/ui/label'
|
||||
@@ -147,6 +148,11 @@ export default function RoundDetailPage() {
|
||||
const [previewSheetOpen, setPreviewSheetOpen] = useState(false)
|
||||
const [exportOpen, setExportOpen] = useState(false)
|
||||
const [advanceDialogOpen, setAdvanceDialogOpen] = useState(false)
|
||||
const [aiRecommendations, setAiRecommendations] = useState<{
|
||||
STARTUP: Array<{ projectId: string; rank: number; score: number; category: string; strengths: string[]; concerns: string[]; recommendation: string }>
|
||||
BUSINESS_CONCEPT: Array<{ projectId: string; rank: number; score: number; category: string; strengths: string[]; concerns: string[]; recommendation: string }>
|
||||
} | null>(null)
|
||||
const [shortlistDialogOpen, setShortlistDialogOpen] = useState(false)
|
||||
|
||||
const utils = trpc.useUtils()
|
||||
|
||||
@@ -243,6 +249,25 @@ export default function RoundDetailPage() {
|
||||
onError: (err) => toast.error(err.message),
|
||||
})
|
||||
|
||||
const shortlistMutation = trpc.round.generateAIRecommendations.useMutation({
|
||||
onSuccess: (data) => {
|
||||
if (data.success) {
|
||||
setAiRecommendations(data.recommendations)
|
||||
toast.success(
|
||||
`AI recommendations generated: ${data.recommendations.STARTUP.length} startups, ${data.recommendations.BUSINESS_CONCEPT.length} concepts` +
|
||||
(data.tokensUsed ? ` (${data.tokensUsed} tokens)` : ''),
|
||||
)
|
||||
} else {
|
||||
toast.error(data.errors?.join('; ') || 'AI shortlist failed')
|
||||
}
|
||||
setShortlistDialogOpen(false)
|
||||
},
|
||||
onError: (err) => {
|
||||
toast.error(err.message)
|
||||
setShortlistDialogOpen(false)
|
||||
},
|
||||
})
|
||||
|
||||
const isTransitioning = activateMutation.isPending || closeMutation.isPending || archiveMutation.isPending
|
||||
|
||||
const handleConfigChange = useCallback((newConfig: Record<string, unknown>) => {
|
||||
@@ -828,6 +853,25 @@ export default function RoundDetailPage() {
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* AI Shortlist Recommendations */}
|
||||
{(isEvaluation || isFiltering) && projectCount > 0 && (
|
||||
<button
|
||||
onClick={() => setShortlistDialogOpen(true)}
|
||||
className="flex items-start gap-3 p-4 rounded-lg border hover:bg-muted/50 transition-colors text-left border-purple-200 bg-purple-50/50"
|
||||
disabled={shortlistMutation.isPending}
|
||||
>
|
||||
<BarChart3 className="h-5 w-5 text-purple-600 mt-0.5 shrink-0" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">
|
||||
{shortlistMutation.isPending ? 'Generating...' : 'AI Recommendations'}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Generate ranked shortlist per category using AI analysis
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Advance projects (shown when PASSED > 0) */}
|
||||
{passedCount > 0 && (
|
||||
<button
|
||||
@@ -847,29 +891,62 @@ export default function RoundDetailPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Advance Projects Confirmation Dialog */}
|
||||
<AlertDialog open={advanceDialogOpen} onOpenChange={setAdvanceDialogOpen}>
|
||||
{/* Advance Projects Dialog */}
|
||||
<AdvanceProjectsDialog
|
||||
open={advanceDialogOpen}
|
||||
onOpenChange={setAdvanceDialogOpen}
|
||||
roundId={roundId}
|
||||
projectStates={projectStates}
|
||||
config={config}
|
||||
advanceMutation={advanceMutation}
|
||||
/>
|
||||
|
||||
{/* AI Shortlist Confirmation Dialog */}
|
||||
<AlertDialog open={shortlistDialogOpen} onOpenChange={setShortlistDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Advance {passedCount} project(s)?</AlertDialogTitle>
|
||||
<AlertDialogTitle>Generate AI Recommendations?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
All projects with PASSED status in this round will be moved to the next round.
|
||||
This action creates new entries in the next round and marks current entries as completed.
|
||||
The AI will analyze all project evaluations and generate a ranked shortlist
|
||||
for each category independently.
|
||||
{config.startupAdvanceCount ? (
|
||||
<span className="block mt-1">
|
||||
Startup target: top {String(config.startupAdvanceCount)}
|
||||
</span>
|
||||
) : null}
|
||||
{config.conceptAdvanceCount ? (
|
||||
<span className="block">
|
||||
Business Concept target: top {String(config.conceptAdvanceCount)}
|
||||
</span>
|
||||
) : null}
|
||||
{config.aiParseFiles ? (
|
||||
<span className="block mt-1 text-amber-600">
|
||||
Document parsing is enabled — the AI will read uploaded file contents.
|
||||
</span>
|
||||
) : null}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => advanceMutation.mutate({ roundId })}
|
||||
disabled={advanceMutation.isPending}
|
||||
onClick={() => shortlistMutation.mutate({ roundId })}
|
||||
disabled={shortlistMutation.isPending}
|
||||
>
|
||||
{advanceMutation.isPending && <Loader2 className="h-4 w-4 mr-1.5 animate-spin" />}
|
||||
Advance Projects
|
||||
{shortlistMutation.isPending && <Loader2 className="h-4 w-4 mr-1.5 animate-spin" />}
|
||||
Generate
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* AI Recommendations Display */}
|
||||
{aiRecommendations && (
|
||||
<AIRecommendationsDisplay
|
||||
recommendations={aiRecommendations}
|
||||
onClear={() => setAiRecommendations(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Round Info + Project Breakdown */}
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<Card>
|
||||
@@ -1054,7 +1131,7 @@ export default function RoundDetailPage() {
|
||||
<CardTitle className="text-base">General Settings</CardTitle>
|
||||
<CardDescription>Settings that apply to this round regardless of type</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<CardContent className="space-y-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="notify-on-entry" className="text-sm font-medium">
|
||||
@@ -1072,6 +1149,85 @@ export default function RoundDetailPage() {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="notify-on-advance" className="text-sm font-medium">
|
||||
Notify on advance
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Send an email to project applicants when their project advances from this round to the next
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="notify-on-advance"
|
||||
checked={!!config.notifyOnAdvance}
|
||||
onCheckedChange={(checked) => {
|
||||
handleConfigChange({ ...config, notifyOnAdvance: checked })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="ai-parse-files" className="text-sm font-medium">
|
||||
AI document parsing
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Allow AI to read the contents of uploaded files (PDF/text) for deeper analysis during filtering and evaluation
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="ai-parse-files"
|
||||
checked={!!config.aiParseFiles}
|
||||
onCheckedChange={(checked) => {
|
||||
handleConfigChange({ ...config, aiParseFiles: checked })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-4">
|
||||
<Label className="text-sm font-medium">Advancement Targets</Label>
|
||||
<p className="text-xs text-muted-foreground mb-3">
|
||||
Target number of projects per category to advance from this round
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="startup-advance-count" className="text-xs text-muted-foreground">
|
||||
Startup Projects
|
||||
</Label>
|
||||
<Input
|
||||
id="startup-advance-count"
|
||||
type="number"
|
||||
min={0}
|
||||
className="h-9"
|
||||
placeholder="No limit"
|
||||
value={(config.startupAdvanceCount as number) ?? ''}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value ? parseInt(e.target.value, 10) : undefined
|
||||
handleConfigChange({ ...config, startupAdvanceCount: val })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="concept-advance-count" className="text-xs text-muted-foreground">
|
||||
Concept Projects
|
||||
</Label>
|
||||
<Input
|
||||
id="concept-advance-count"
|
||||
type="number"
|
||||
min={0}
|
||||
className="h-9"
|
||||
placeholder="No limit"
|
||||
value={(config.conceptAdvanceCount as number) ?? ''}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value ? parseInt(e.target.value, 10) : undefined
|
||||
handleConfigChange({ ...config, conceptAdvanceCount: val })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1585,6 +1741,304 @@ function IndividualAssignmentsTable({ roundId }: { roundId: string }) {
|
||||
|
||||
// ── Evaluation Criteria Editor ───────────────────────────────────────────
|
||||
|
||||
// ── Advance Projects Dialog ─────────────────────────────────────────────
|
||||
|
||||
function AdvanceProjectsDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
roundId,
|
||||
projectStates,
|
||||
config,
|
||||
advanceMutation,
|
||||
}: {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
roundId: string
|
||||
projectStates: any[] | undefined
|
||||
config: Record<string, unknown>
|
||||
advanceMutation: { mutate: (input: { roundId: string; projectIds?: string[] }) => void; isPending: boolean }
|
||||
}) {
|
||||
const passedProjects = useMemo(() =>
|
||||
(projectStates ?? []).filter((ps: any) => ps.state === 'PASSED'),
|
||||
[projectStates])
|
||||
|
||||
const startups = useMemo(() =>
|
||||
passedProjects.filter((ps: any) => ps.project?.competitionCategory === 'STARTUP'),
|
||||
[passedProjects])
|
||||
|
||||
const concepts = useMemo(() =>
|
||||
passedProjects.filter((ps: any) => ps.project?.competitionCategory === 'BUSINESS_CONCEPT'),
|
||||
[passedProjects])
|
||||
|
||||
const other = useMemo(() =>
|
||||
passedProjects.filter((ps: any) =>
|
||||
ps.project?.competitionCategory !== 'STARTUP' && ps.project?.competitionCategory !== 'BUSINESS_CONCEPT',
|
||||
),
|
||||
[passedProjects])
|
||||
|
||||
const startupCap = (config.startupAdvanceCount as number) || 0
|
||||
const conceptCap = (config.conceptAdvanceCount as number) || 0
|
||||
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set())
|
||||
|
||||
// Reset selection when dialog opens
|
||||
if (open && selected.size === 0 && passedProjects.length > 0) {
|
||||
const initial = new Set<string>()
|
||||
// Auto-select all (or up to cap if configured)
|
||||
const startupSlice = startupCap > 0 ? startups.slice(0, startupCap) : startups
|
||||
const conceptSlice = conceptCap > 0 ? concepts.slice(0, conceptCap) : concepts
|
||||
for (const ps of startupSlice) initial.add(ps.project?.id)
|
||||
for (const ps of conceptSlice) initial.add(ps.project?.id)
|
||||
for (const ps of other) initial.add(ps.project?.id)
|
||||
setSelected(initial)
|
||||
}
|
||||
|
||||
const toggleProject = (projectId: string) => {
|
||||
setSelected((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(projectId)) next.delete(projectId)
|
||||
else next.add(projectId)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const toggleAll = (projects: any[], on: boolean) => {
|
||||
setSelected((prev) => {
|
||||
const next = new Set(prev)
|
||||
for (const ps of projects) {
|
||||
if (on) next.add(ps.project?.id)
|
||||
else next.delete(ps.project?.id)
|
||||
}
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const handleAdvance = () => {
|
||||
const ids = Array.from(selected)
|
||||
if (ids.length === 0) return
|
||||
advanceMutation.mutate({ roundId, projectIds: ids })
|
||||
onOpenChange(false)
|
||||
setSelected(new Set())
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
onOpenChange(false)
|
||||
setSelected(new Set())
|
||||
}
|
||||
|
||||
const renderCategorySection = (
|
||||
label: string,
|
||||
projects: any[],
|
||||
cap: number,
|
||||
badgeColor: string,
|
||||
) => {
|
||||
const selectedInCategory = projects.filter((ps: any) => selected.has(ps.project?.id)).length
|
||||
const overCap = cap > 0 && selectedInCategory > cap
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={projects.length > 0 && projects.every((ps: any) => selected.has(ps.project?.id))}
|
||||
onCheckedChange={(checked) => toggleAll(projects, !!checked)}
|
||||
/>
|
||||
<span className="text-sm font-medium">{label}</span>
|
||||
<Badge variant="secondary" className={cn('text-[10px]', badgeColor)}>
|
||||
{selectedInCategory}/{projects.length}
|
||||
</Badge>
|
||||
{cap > 0 && (
|
||||
<span className={cn('text-[10px]', overCap ? 'text-red-500 font-medium' : 'text-muted-foreground')}>
|
||||
(target: {cap})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{projects.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground pl-7">No passed projects in this category</p>
|
||||
) : (
|
||||
<div className="space-y-1 pl-7">
|
||||
{projects.map((ps: any) => (
|
||||
<label
|
||||
key={ps.project?.id}
|
||||
className="flex items-center gap-2 p-2 rounded hover:bg-muted/30 cursor-pointer"
|
||||
>
|
||||
<Checkbox
|
||||
checked={selected.has(ps.project?.id)}
|
||||
onCheckedChange={() => toggleProject(ps.project?.id)}
|
||||
/>
|
||||
<span className="text-sm truncate flex-1">{ps.project?.title || 'Untitled'}</span>
|
||||
{ps.project?.teamName && (
|
||||
<span className="text-xs text-muted-foreground shrink-0">{ps.project.teamName}</span>
|
||||
)}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className="max-w-lg max-h-[85vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Advance Projects</DialogTitle>
|
||||
<DialogDescription>
|
||||
Select which passed projects to advance to the next round.
|
||||
{selected.size} of {passedProjects.length} selected.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 overflow-y-auto space-y-4 py-2">
|
||||
{renderCategorySection('Startup', startups, startupCap, 'bg-blue-100 text-blue-700')}
|
||||
{renderCategorySection('Business Concept', concepts, conceptCap, 'bg-purple-100 text-purple-700')}
|
||||
{other.length > 0 && renderCategorySection('Other / Uncategorized', other, 0, 'bg-gray-100 text-gray-700')}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={handleClose}>Cancel</Button>
|
||||
<Button
|
||||
onClick={handleAdvance}
|
||||
disabled={selected.size === 0 || advanceMutation.isPending}
|
||||
>
|
||||
{advanceMutation.isPending && <Loader2 className="h-4 w-4 mr-1.5 animate-spin" />}
|
||||
Advance {selected.size} Project{selected.size !== 1 ? 's' : ''}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
// ── AI Recommendations Display ──────────────────────────────────────────
|
||||
|
||||
type RecommendationItem = {
|
||||
projectId: string
|
||||
rank: number
|
||||
score: number
|
||||
category: string
|
||||
strengths: string[]
|
||||
concerns: string[]
|
||||
recommendation: string
|
||||
}
|
||||
|
||||
function AIRecommendationsDisplay({
|
||||
recommendations,
|
||||
onClear,
|
||||
}: {
|
||||
recommendations: { STARTUP: RecommendationItem[]; BUSINESS_CONCEPT: RecommendationItem[] }
|
||||
onClear: () => void
|
||||
}) {
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null)
|
||||
|
||||
const renderCategory = (label: string, items: RecommendationItem[], colorClass: string) => {
|
||||
if (items.length === 0) return (
|
||||
<div className="text-center py-4 text-muted-foreground text-sm">
|
||||
No {label.toLowerCase()} projects evaluated
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{items.map((item) => {
|
||||
const isExpanded = expandedId === `${item.category}-${item.projectId}`
|
||||
return (
|
||||
<div
|
||||
key={item.projectId}
|
||||
className="border rounded-lg overflow-hidden"
|
||||
>
|
||||
<button
|
||||
onClick={() => setExpandedId(isExpanded ? null : `${item.category}-${item.projectId}`)}
|
||||
className="w-full flex items-center gap-3 p-3 text-left hover:bg-muted/30 transition-colors"
|
||||
>
|
||||
<span className={cn(
|
||||
'h-7 w-7 rounded-full flex items-center justify-center text-xs font-bold text-white shrink-0',
|
||||
colorClass,
|
||||
)}>
|
||||
{item.rank}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium truncate">{item.projectId}</p>
|
||||
<p className="text-xs text-muted-foreground truncate">{item.recommendation}</p>
|
||||
</div>
|
||||
<Badge variant="outline" className="shrink-0 text-xs font-mono">
|
||||
{item.score}/100
|
||||
</Badge>
|
||||
<ChevronDown className={cn(
|
||||
'h-4 w-4 text-muted-foreground transition-transform shrink-0',
|
||||
isExpanded && 'rotate-180',
|
||||
)} />
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="px-3 pb-3 pt-0 space-y-2 border-t bg-muted/10">
|
||||
<div className="pt-2">
|
||||
<p className="text-xs font-medium text-emerald-700 mb-1">Strengths</p>
|
||||
<ul className="text-xs text-muted-foreground space-y-0.5 pl-4 list-disc">
|
||||
{item.strengths.map((s, i) => <li key={i}>{s}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
{item.concerns.length > 0 && (
|
||||
<div>
|
||||
<p className="text-xs font-medium text-amber-700 mb-1">Concerns</p>
|
||||
<ul className="text-xs text-muted-foreground space-y-0.5 pl-4 list-disc">
|
||||
{item.concerns.map((c, i) => <li key={i}>{c}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<p className="text-xs font-medium text-blue-700 mb-1">Recommendation</p>
|
||||
<p className="text-xs text-muted-foreground">{item.recommendation}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-base">AI Shortlist Recommendations</CardTitle>
|
||||
<CardDescription>
|
||||
Ranked independently per category — {recommendations.STARTUP.length} startups, {recommendations.BUSINESS_CONCEPT.length} concepts
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={onClear}>
|
||||
<X className="h-4 w-4 mr-1" />
|
||||
Dismiss
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold mb-3 flex items-center gap-2">
|
||||
<div className="h-2 w-2 rounded-full bg-blue-500" />
|
||||
Startup ({recommendations.STARTUP.length})
|
||||
</h4>
|
||||
{renderCategory('Startup', recommendations.STARTUP, 'bg-blue-500')}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold mb-3 flex items-center gap-2">
|
||||
<div className="h-2 w-2 rounded-full bg-purple-500" />
|
||||
Business Concept ({recommendations.BUSINESS_CONCEPT.length})
|
||||
</h4>
|
||||
{renderCategory('Business Concept', recommendations.BUSINESS_CONCEPT, 'bg-purple-500')}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Evaluation Criteria Editor ───────────────────────────────────────────
|
||||
|
||||
function EvaluationCriteriaEditor({ roundId }: { roundId: string }) {
|
||||
const [editing, setEditing] = useState(false)
|
||||
const [criteria, setCriteria] = useState<Array<{
|
||||
|
||||
@@ -169,7 +169,6 @@ export default function RoundsPage() {
|
||||
categoryMode: comp.categoryMode,
|
||||
startupFinalistCount: comp.startupFinalistCount,
|
||||
conceptFinalistCount: comp.conceptFinalistCount,
|
||||
notifyOnRoundAdvance: comp.notifyOnRoundAdvance,
|
||||
notifyOnDeadlineApproach: comp.notifyOnDeadlineApproach,
|
||||
})
|
||||
}
|
||||
@@ -492,13 +491,6 @@ function CompetitionGroup({
|
||||
onChange={(e) => onEditChange({ ...competitionEdits, conceptFinalistCount: parseInt(e.target.value, 10) || 10 })}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 pt-6">
|
||||
<Switch
|
||||
checked={(competitionEdits.notifyOnRoundAdvance as boolean) ?? false}
|
||||
onCheckedChange={(v) => onEditChange({ ...competitionEdits, notifyOnRoundAdvance: v })}
|
||||
/>
|
||||
<Label className="text-xs font-medium">Notify on Advance</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 pt-6">
|
||||
<Switch
|
||||
checked={(competitionEdits.notifyOnDeadlineApproach as boolean) ?? false}
|
||||
|
||||
Reference in New Issue
Block a user