Competition/Round architecture: full platform rewrite (Phases 1-9)

Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 23:04:15 +01:00
parent 9ab4717f96
commit 6ca39c976b
349 changed files with 69938 additions and 28767 deletions

View File

@@ -0,0 +1,141 @@
'use client'
import { useParams } from 'next/navigation'
import Link from 'next/link'
import type { Route } from 'next'
import { trpc } from '@/lib/trpc/client'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Skeleton } from '@/components/ui/skeleton'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { WorkspaceChat } from '@/components/mentor/workspace-chat'
import { FilePromotionPanel } from '@/components/mentor/file-promotion-panel'
import { ArrowLeft, MessageSquare, FileText, Upload } from 'lucide-react'
import { toast } from 'sonner'
export default function MentorWorkspaceDetailPage() {
const params = useParams()
const projectId = params.projectId as string
// Get mentor assignment for this project
const { data: assignments } = trpc.mentor.getMyProjects.useQuery()
const assignment = assignments?.find(a => a.projectId === projectId)
const { data: project, isLoading } = trpc.project.get.useQuery(
{ id: projectId },
{ enabled: !!projectId }
)
if (isLoading) {
return (
<div className="space-y-6">
<Skeleton className="h-8 w-64" />
<Skeleton className="h-96" />
</div>
)
}
if (!project) {
return (
<div className="space-y-6">
<Button variant="ghost" size="sm" asChild>
<Link href={'/mentor/workspace' as Route}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back
</Link>
</Button>
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<FileText className="h-12 w-12 text-muted-foreground/50 mb-3" />
<p className="font-medium">Project not found</p>
</CardContent>
</Card>
</div>
)
}
return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<Button variant="ghost" size="sm" asChild>
<Link href={'/mentor/workspace' as Route}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back
</Link>
</Button>
<div className="flex-1">
<div className="flex items-center gap-3 flex-wrap">
<h1 className="text-2xl font-bold tracking-tight">{project.title}</h1>
{project.status && (
<Badge variant="secondary">{project.status}</Badge>
)}
</div>
{project.teamName && (
<p className="text-muted-foreground mt-1">{project.teamName}</p>
)}
</div>
</div>
<Tabs defaultValue="chat" className="w-full">
<TabsList className="w-full sm:w-auto grid grid-cols-3 sm:inline-grid">
<TabsTrigger value="chat" className="flex items-center gap-2">
<MessageSquare className="h-4 w-4" />
<span className="hidden sm:inline">Chat</span>
</TabsTrigger>
<TabsTrigger value="files" className="flex items-center gap-2">
<FileText className="h-4 w-4" />
<span className="hidden sm:inline">Files</span>
</TabsTrigger>
<TabsTrigger value="promotion" className="flex items-center gap-2">
<Upload className="h-4 w-4" />
<span className="hidden sm:inline">Promotion</span>
</TabsTrigger>
</TabsList>
<TabsContent value="chat" className="mt-6">
{assignment ? (
<WorkspaceChat mentorAssignmentId={assignment.id} />
) : (
<Card>
<CardContent className="text-center py-8">
<MessageSquare className="h-12 w-12 text-muted-foreground/50 mx-auto mb-3" />
<p className="text-sm text-muted-foreground">Loading workspace...</p>
</CardContent>
</Card>
)}
</TabsContent>
<TabsContent value="files" className="mt-6">
<Card>
<CardHeader>
<CardTitle>Workspace Files</CardTitle>
<CardDescription>
Files shared in the mentor workspace
</CardDescription>
</CardHeader>
<CardContent className="text-center py-8">
<FileText className="h-12 w-12 text-muted-foreground/50 mx-auto mb-3" />
<p className="text-sm text-muted-foreground">
File listing feature coming soon
</p>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="promotion" className="mt-6">
{assignment ? (
<FilePromotionPanel mentorAssignmentId={assignment.id} />
) : (
<Card>
<CardContent className="text-center py-8">
<Upload className="h-12 w-12 text-muted-foreground/50 mx-auto mb-3" />
<p className="text-sm text-muted-foreground">Loading workspace...</p>
</CardContent>
</Card>
)}
</TabsContent>
</Tabs>
</div>
)
}

View File

@@ -0,0 +1,128 @@
'use client'
import Link from 'next/link'
import type { Route } from 'next'
import { trpc } from '@/lib/trpc/client'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import { ArrowLeft, ArrowRight, Users, Briefcase } from 'lucide-react'
import { toast } from 'sonner'
const statusColors: Record<string, 'default' | 'secondary' | 'destructive' | 'outline'> = {
SUBMITTED: 'secondary',
ELIGIBLE: 'default',
SEMIFINALIST: 'default',
FINALIST: 'default',
WINNER: 'default',
REJECTED: 'destructive',
}
export default function MentorWorkspacePage() {
const { data: assignments, isLoading } = trpc.mentor.getMyProjects.useQuery()
if (isLoading) {
return (
<div className="space-y-6">
<Skeleton className="h-8 w-64" />
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{[1, 2, 3].map((i) => (
<Skeleton key={i} className="h-40" />
))}
</div>
</div>
)
}
const projects = assignments || []
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold tracking-tight">Mentor Workspace</h1>
<p className="text-muted-foreground mt-1">
Collaborate with your assigned mentee projects
</p>
</div>
<Button variant="ghost" size="sm" asChild>
<Link href={'/mentor' as Route}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Dashboard
</Link>
</Button>
</div>
{projects.length === 0 ? (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<div className="rounded-2xl bg-brand-teal/10 p-4 mb-4">
<Briefcase className="h-8 w-8 text-brand-teal/60" />
</div>
<h2 className="text-xl font-semibold mb-2">No Projects Assigned</h2>
<p className="text-muted-foreground text-center max-w-md">
You don&apos;t have any mentee projects assigned yet. Check back later.
</p>
</CardContent>
</Card>
) : (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{projects.map((assignment) => {
const project = assignment.project
const teamSize = project.teamMembers?.length || 0
return (
<Card
key={assignment.id}
className="flex flex-col transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md"
>
<CardHeader>
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<CardTitle className="text-lg truncate" title={project.title}>
{project.title}
</CardTitle>
{project.teamName && (
<p className="text-sm text-muted-foreground mt-1 truncate">
{project.teamName}
</p>
)}
</div>
{project.status && (
<Badge variant={statusColors[project.status] || 'secondary'} className="shrink-0 ml-2">
{project.status}
</Badge>
)}
</div>
</CardHeader>
<CardContent className="flex-1 flex flex-col space-y-4">
{project.description && (
<p className="text-sm text-muted-foreground line-clamp-2">
{project.description}
</p>
)}
<div className="flex-1" />
<div className="flex items-center justify-between text-sm pt-3 border-t">
<div className="flex items-center gap-2 text-muted-foreground">
<Users className="h-4 w-4" />
<span>{teamSize} member{teamSize !== 1 ? 's' : ''}</span>
</div>
<Button variant="ghost" size="sm" asChild>
<Link href={`/mentor/workspace/${project.id}` as Route}>
Open
<ArrowRight className="ml-1 h-4 w-4" />
</Link>
</Button>
</div>
</CardContent>
</Card>
)
})}
</div>
)}
</div>
)
}