284 lines
12 KiB
TypeScript
284 lines
12 KiB
TypeScript
|
|
'use client'
|
||
|
|
|
||
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||
|
|
import { Input } from '@/components/ui/input'
|
||
|
|
import { Label } from '@/components/ui/label'
|
||
|
|
import { Switch } from '@/components/ui/switch'
|
||
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||
|
|
|
||
|
|
type EvaluationConfigProps = {
|
||
|
|
config: Record<string, unknown>
|
||
|
|
onChange: (config: Record<string, unknown>) => void
|
||
|
|
}
|
||
|
|
|
||
|
|
export function EvaluationConfig({ config, onChange }: EvaluationConfigProps) {
|
||
|
|
const update = (key: string, value: unknown) => {
|
||
|
|
onChange({ ...config, [key]: value })
|
||
|
|
}
|
||
|
|
|
||
|
|
const advancementMode = (config.advancementMode as string) ?? 'admin_selection'
|
||
|
|
const advancementConfig = (config.advancementConfig as {
|
||
|
|
perCategory?: boolean; startupCount?: number; conceptCount?: number; tieBreaker?: string
|
||
|
|
}) ?? {}
|
||
|
|
|
||
|
|
const updateAdvancement = (key: string, value: unknown) => {
|
||
|
|
update('advancementConfig', { ...advancementConfig, [key]: value })
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="space-y-6">
|
||
|
|
{/* Scoring */}
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="text-base">Scoring & Reviews</CardTitle>
|
||
|
|
<CardDescription>How jury members evaluate and score projects</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-4">
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="requiredReviews">Required Reviews per Project</Label>
|
||
|
|
<p className="text-xs text-muted-foreground">Minimum number of jury evaluations needed</p>
|
||
|
|
<Input
|
||
|
|
id="requiredReviews"
|
||
|
|
type="number"
|
||
|
|
min={1}
|
||
|
|
className="w-32"
|
||
|
|
value={(config.requiredReviewsPerProject as number) ?? 3}
|
||
|
|
onChange={(e) => update('requiredReviewsPerProject', parseInt(e.target.value, 10) || 3)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="scoringMode">Scoring Mode</Label>
|
||
|
|
<p className="text-xs text-muted-foreground">How jurors assign scores to projects</p>
|
||
|
|
<Select
|
||
|
|
value={(config.scoringMode as string) ?? 'criteria'}
|
||
|
|
onValueChange={(v) => update('scoringMode', v)}
|
||
|
|
>
|
||
|
|
<SelectTrigger id="scoringMode" className="w-64">
|
||
|
|
<SelectValue />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
<SelectItem value="criteria">Criteria-based (multiple criteria with weights)</SelectItem>
|
||
|
|
<SelectItem value="global">Global score (single overall score)</SelectItem>
|
||
|
|
<SelectItem value="binary">Binary (pass/fail)</SelectItem>
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="anonymizationLevel">Anonymization Level</Label>
|
||
|
|
<p className="text-xs text-muted-foreground">How much of other jurors' identities are revealed</p>
|
||
|
|
<Select
|
||
|
|
value={(config.anonymizationLevel as string) ?? 'fully_anonymous'}
|
||
|
|
onValueChange={(v) => update('anonymizationLevel', v)}
|
||
|
|
>
|
||
|
|
<SelectTrigger id="anonymizationLevel" className="w-64">
|
||
|
|
<SelectValue />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
<SelectItem value="fully_anonymous">Fully Anonymous</SelectItem>
|
||
|
|
<SelectItem value="show_initials">Show Initials</SelectItem>
|
||
|
|
<SelectItem value="named">Named (full names visible)</SelectItem>
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
{/* Feedback */}
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="text-base">Feedback Requirements</CardTitle>
|
||
|
|
<CardDescription>What jurors must provide alongside scores</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-4">
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<div>
|
||
|
|
<Label htmlFor="requireFeedback">Require Written Feedback</Label>
|
||
|
|
<p className="text-xs text-muted-foreground">Jurors must write feedback text</p>
|
||
|
|
</div>
|
||
|
|
<Switch
|
||
|
|
id="requireFeedback"
|
||
|
|
checked={(config.requireFeedback as boolean) ?? true}
|
||
|
|
onCheckedChange={(v) => update('requireFeedback', v)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{(config.requireFeedback as boolean) !== false && (
|
||
|
|
<div className="pl-6 border-l-2 border-muted space-y-2">
|
||
|
|
<Label htmlFor="feedbackMinLength">Minimum Feedback Length</Label>
|
||
|
|
<p className="text-xs text-muted-foreground">Minimum characters (0 = no minimum)</p>
|
||
|
|
<Input
|
||
|
|
id="feedbackMinLength"
|
||
|
|
type="number"
|
||
|
|
min={0}
|
||
|
|
className="w-32"
|
||
|
|
value={(config.feedbackMinLength as number) ?? 0}
|
||
|
|
onChange={(e) => update('feedbackMinLength', parseInt(e.target.value, 10) || 0)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<div>
|
||
|
|
<Label htmlFor="requireAllCriteriaScored">Require All Criteria Scored</Label>
|
||
|
|
<p className="text-xs text-muted-foreground">Jurors must score every criterion before submitting</p>
|
||
|
|
</div>
|
||
|
|
<Switch
|
||
|
|
id="requireAllCriteriaScored"
|
||
|
|
checked={(config.requireAllCriteriaScored as boolean) ?? true}
|
||
|
|
onCheckedChange={(v) => update('requireAllCriteriaScored', v)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<div>
|
||
|
|
<Label htmlFor="coiRequired">COI Declaration Required</Label>
|
||
|
|
<p className="text-xs text-muted-foreground">Jurors must declare conflicts of interest</p>
|
||
|
|
</div>
|
||
|
|
<Switch
|
||
|
|
id="coiRequired"
|
||
|
|
checked={(config.coiRequired as boolean) ?? true}
|
||
|
|
onCheckedChange={(v) => update('coiRequired', v)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<div>
|
||
|
|
<Label htmlFor="peerReviewEnabled">Peer Review</Label>
|
||
|
|
<p className="text-xs text-muted-foreground">Allow jurors to see and comment on other evaluations</p>
|
||
|
|
</div>
|
||
|
|
<Switch
|
||
|
|
id="peerReviewEnabled"
|
||
|
|
checked={(config.peerReviewEnabled as boolean) ?? false}
|
||
|
|
onCheckedChange={(v) => update('peerReviewEnabled', v)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
{/* AI Features */}
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="text-base">AI Features</CardTitle>
|
||
|
|
<CardDescription>AI-powered evaluation assistance</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-4">
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<div>
|
||
|
|
<Label htmlFor="aiSummaryEnabled">AI Evaluation Summary</Label>
|
||
|
|
<p className="text-xs text-muted-foreground">Generate AI synthesis of all jury evaluations</p>
|
||
|
|
</div>
|
||
|
|
<Switch
|
||
|
|
id="aiSummaryEnabled"
|
||
|
|
checked={(config.aiSummaryEnabled as boolean) ?? false}
|
||
|
|
onCheckedChange={(v) => update('aiSummaryEnabled', v)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<div>
|
||
|
|
<Label htmlFor="generateAiShortlist">AI Shortlist Recommendations</Label>
|
||
|
|
<p className="text-xs text-muted-foreground">AI suggests which projects should advance</p>
|
||
|
|
</div>
|
||
|
|
<Switch
|
||
|
|
id="generateAiShortlist"
|
||
|
|
checked={(config.generateAiShortlist as boolean) ?? false}
|
||
|
|
onCheckedChange={(v) => update('generateAiShortlist', v)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
{/* Advancement */}
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="text-base">Advancement Rules</CardTitle>
|
||
|
|
<CardDescription>How projects move to the next round</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-4">
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="advancementMode">Advancement Mode</Label>
|
||
|
|
<Select
|
||
|
|
value={advancementMode}
|
||
|
|
onValueChange={(v) => update('advancementMode', v)}
|
||
|
|
>
|
||
|
|
<SelectTrigger id="advancementMode" className="w-64">
|
||
|
|
<SelectValue />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
<SelectItem value="admin_selection">Admin Selection (manual)</SelectItem>
|
||
|
|
<SelectItem value="auto_top_n">Auto Top-N (by score)</SelectItem>
|
||
|
|
<SelectItem value="ai_recommended">AI Recommended</SelectItem>
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{advancementMode === 'auto_top_n' && (
|
||
|
|
<div className="pl-6 border-l-2 border-muted space-y-4">
|
||
|
|
<Label className="text-sm font-medium">Auto Top-N Settings</Label>
|
||
|
|
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<div>
|
||
|
|
<Label htmlFor="perCategory">Per Category</Label>
|
||
|
|
<p className="text-xs text-muted-foreground">Apply limits separately for each category</p>
|
||
|
|
</div>
|
||
|
|
<Switch
|
||
|
|
id="perCategory"
|
||
|
|
checked={advancementConfig.perCategory ?? true}
|
||
|
|
onCheckedChange={(v) => updateAdvancement('perCategory', v)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="startupCount">Startup Advancement Count</Label>
|
||
|
|
<p className="text-xs text-muted-foreground">Number of startups to advance</p>
|
||
|
|
<Input
|
||
|
|
id="startupCount"
|
||
|
|
type="number"
|
||
|
|
min={0}
|
||
|
|
className="w-32"
|
||
|
|
value={advancementConfig.startupCount ?? 10}
|
||
|
|
onChange={(e) => updateAdvancement('startupCount', parseInt(e.target.value, 10) || 0)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="conceptCount">Business Concept Advancement Count</Label>
|
||
|
|
<p className="text-xs text-muted-foreground">Number of business concepts to advance</p>
|
||
|
|
<Input
|
||
|
|
id="conceptCount"
|
||
|
|
type="number"
|
||
|
|
min={0}
|
||
|
|
className="w-32"
|
||
|
|
value={advancementConfig.conceptCount ?? 10}
|
||
|
|
onChange={(e) => updateAdvancement('conceptCount', parseInt(e.target.value, 10) || 0)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="tieBreaker">Tie Breaker</Label>
|
||
|
|
<p className="text-xs text-muted-foreground">How to handle tied scores</p>
|
||
|
|
<Select
|
||
|
|
value={advancementConfig.tieBreaker ?? 'admin_decides'}
|
||
|
|
onValueChange={(v) => updateAdvancement('tieBreaker', v)}
|
||
|
|
>
|
||
|
|
<SelectTrigger id="tieBreaker" className="w-64">
|
||
|
|
<SelectValue />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
<SelectItem value="admin_decides">Admin Decides</SelectItem>
|
||
|
|
<SelectItem value="highest_individual">Highest Individual Score</SelectItem>
|
||
|
|
<SelectItem value="revote">Re-vote</SelectItem>
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|