Add AI Assignment toggle and Tags tab to settings
Build and Push Docker Image / build (push) Successful in 9m25s
Details
Build and Push Docker Image / build (push) Successful in 9m25s
Details
- Add "Use AI" button to assignments page to switch between algorithmic and GPT-powered suggestions - Normalize AI suggestions format to match algorithmic format for consistent UI - Add Tags tab to Settings page with link to expertise tags management - AI assignment mode shows GPT-analyzed suggestions with confidence scores Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
c0f318a867
commit
c45a428d8b
|
|
@ -76,15 +76,40 @@ function AssignmentManagementContent({ roundId }: { roundId: string }) {
|
||||||
const [manualDialogOpen, setManualDialogOpen] = useState(false)
|
const [manualDialogOpen, setManualDialogOpen] = useState(false)
|
||||||
const [selectedJuror, setSelectedJuror] = useState<string>('')
|
const [selectedJuror, setSelectedJuror] = useState<string>('')
|
||||||
const [selectedProject, setSelectedProject] = useState<string>('')
|
const [selectedProject, setSelectedProject] = useState<string>('')
|
||||||
|
const [useAI, setUseAI] = useState(false)
|
||||||
|
|
||||||
const { data: round, isLoading: loadingRound } = trpc.round.get.useQuery({ id: roundId })
|
const { data: round, isLoading: loadingRound } = trpc.round.get.useQuery({ id: roundId })
|
||||||
const { data: assignments, isLoading: loadingAssignments } = trpc.assignment.listByRound.useQuery({ roundId })
|
const { data: assignments, isLoading: loadingAssignments } = trpc.assignment.listByRound.useQuery({ roundId })
|
||||||
const { data: stats, isLoading: loadingStats } = trpc.assignment.getStats.useQuery({ roundId })
|
const { data: stats, isLoading: loadingStats } = trpc.assignment.getStats.useQuery({ roundId })
|
||||||
const { data: suggestions, isLoading: loadingSuggestions, refetch: refetchSuggestions } = trpc.assignment.getSuggestions.useQuery(
|
const { data: isAIAvailable } = trpc.assignment.isAIAvailable.useQuery()
|
||||||
|
|
||||||
|
// Algorithmic suggestions (default)
|
||||||
|
const { data: algorithmicSuggestions, isLoading: loadingAlgorithmic, refetch: refetchAlgorithmic } = trpc.assignment.getSuggestions.useQuery(
|
||||||
{ roundId },
|
{ roundId },
|
||||||
{ enabled: !!round }
|
{ enabled: !!round && !useAI }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// AI-powered suggestions
|
||||||
|
const { data: aiSuggestionsRaw, isLoading: loadingAI, refetch: refetchAI } = trpc.assignment.getAISuggestions.useQuery(
|
||||||
|
{ roundId, useAI: true },
|
||||||
|
{ enabled: !!round && useAI }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Normalize AI suggestions to match algorithmic format
|
||||||
|
const aiSuggestions = aiSuggestionsRaw?.suggestions?.map((s) => ({
|
||||||
|
userId: s.jurorId,
|
||||||
|
jurorName: s.jurorName,
|
||||||
|
projectId: s.projectId,
|
||||||
|
projectTitle: s.projectTitle,
|
||||||
|
score: Math.round(s.confidenceScore * 100),
|
||||||
|
reasoning: [s.reasoning],
|
||||||
|
})) ?? []
|
||||||
|
|
||||||
|
// Use the appropriate suggestions based on mode
|
||||||
|
const suggestions = useAI ? aiSuggestions : (algorithmicSuggestions ?? [])
|
||||||
|
const loadingSuggestions = useAI ? loadingAI : loadingAlgorithmic
|
||||||
|
const refetchSuggestions = useAI ? refetchAI : refetchAlgorithmic
|
||||||
|
|
||||||
// Get available jurors for manual assignment
|
// Get available jurors for manual assignment
|
||||||
const { data: availableJurors } = trpc.user.getJuryMembers.useQuery(
|
const { data: availableJurors } = trpc.user.getJuryMembers.useQuery(
|
||||||
{ roundId },
|
{ roundId },
|
||||||
|
|
@ -111,6 +136,7 @@ function AssignmentManagementContent({ roundId }: { roundId: string }) {
|
||||||
utils.assignment.listByRound.invalidate({ roundId })
|
utils.assignment.listByRound.invalidate({ roundId })
|
||||||
utils.assignment.getStats.invalidate({ roundId })
|
utils.assignment.getStats.invalidate({ roundId })
|
||||||
utils.assignment.getSuggestions.invalidate({ roundId })
|
utils.assignment.getSuggestions.invalidate({ roundId })
|
||||||
|
utils.assignment.getAISuggestions.invalidate({ roundId })
|
||||||
setSelectedSuggestions(new Set())
|
setSelectedSuggestions(new Set())
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
@ -438,13 +464,28 @@ function AssignmentManagementContent({ roundId }: { roundId: string }) {
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-lg flex items-center gap-2">
|
<CardTitle className="text-lg flex items-center gap-2">
|
||||||
<Sparkles className="h-5 w-5 text-amber-500" />
|
<Sparkles className="h-5 w-5 text-amber-500" />
|
||||||
Smart Assignment Suggestions
|
{useAI ? 'AI Assignment Suggestions' : 'Smart Assignment Suggestions'}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
AI-powered recommendations based on expertise matching and workload
|
{useAI
|
||||||
balance
|
? 'GPT-powered recommendations analyzing project descriptions and judge expertise'
|
||||||
|
: 'Algorithmic recommendations based on tag matching and workload balance'}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant={useAI ? 'default' : 'outline'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setUseAI(!useAI)
|
||||||
|
setSelectedSuggestions(new Set())
|
||||||
|
}}
|
||||||
|
disabled={!isAIAvailable && !useAI}
|
||||||
|
title={!isAIAvailable ? 'OpenAI API key not configured' : undefined}
|
||||||
|
>
|
||||||
|
<Sparkles className={`mr-2 h-4 w-4 ${useAI ? 'text-amber-300' : ''}`} />
|
||||||
|
{useAI ? 'AI Mode' : 'Use AI'}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -457,6 +498,7 @@ function AssignmentManagementContent({ roundId }: { roundId: string }) {
|
||||||
Refresh
|
Refresh
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{loadingSuggestions ? (
|
{loadingSuggestions ? (
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,11 @@ import {
|
||||||
Shield,
|
Shield,
|
||||||
Settings as SettingsIcon,
|
Settings as SettingsIcon,
|
||||||
Bell,
|
Bell,
|
||||||
|
Tags,
|
||||||
|
ExternalLink,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
import { AISettingsForm } from './ai-settings-form'
|
import { AISettingsForm } from './ai-settings-form'
|
||||||
import { AIUsageCard } from './ai-usage-card'
|
import { AIUsageCard } from './ai-usage-card'
|
||||||
import { BrandingSettingsForm } from './branding-settings-form'
|
import { BrandingSettingsForm } from './branding-settings-form'
|
||||||
|
|
@ -110,11 +114,15 @@ export function SettingsContent({ initialSettings }: SettingsContentProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs defaultValue="ai" className="space-y-6">
|
<Tabs defaultValue="ai" className="space-y-6">
|
||||||
<TabsList className="grid w-full grid-cols-3 lg:grid-cols-7">
|
<TabsList className="grid w-full grid-cols-4 lg:grid-cols-8">
|
||||||
<TabsTrigger value="ai" className="gap-2">
|
<TabsTrigger value="ai" className="gap-2">
|
||||||
<Bot className="h-4 w-4" />
|
<Bot className="h-4 w-4" />
|
||||||
<span className="hidden sm:inline">AI</span>
|
<span className="hidden sm:inline">AI</span>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="tags" className="gap-2">
|
||||||
|
<Tags className="h-4 w-4" />
|
||||||
|
<span className="hidden sm:inline">Tags</span>
|
||||||
|
</TabsTrigger>
|
||||||
<TabsTrigger value="branding" className="gap-2">
|
<TabsTrigger value="branding" className="gap-2">
|
||||||
<Palette className="h-4 w-4" />
|
<Palette className="h-4 w-4" />
|
||||||
<span className="hidden sm:inline">Branding</span>
|
<span className="hidden sm:inline">Branding</span>
|
||||||
|
|
@ -156,6 +164,38 @@ export function SettingsContent({ initialSettings }: SettingsContentProps) {
|
||||||
<AIUsageCard />
|
<AIUsageCard />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="tags">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Tags className="h-5 w-5" />
|
||||||
|
Expertise Tags
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Manage tags used for jury expertise, project categorization, and AI-powered matching
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Expertise tags are used across the platform to:
|
||||||
|
</p>
|
||||||
|
<ul className="text-sm text-muted-foreground list-disc list-inside space-y-1">
|
||||||
|
<li>Categorize jury members by their areas of expertise</li>
|
||||||
|
<li>Tag projects for better organization and filtering</li>
|
||||||
|
<li>Power AI-based project tagging</li>
|
||||||
|
<li>Enable smart jury-project matching</li>
|
||||||
|
</ul>
|
||||||
|
<Button asChild>
|
||||||
|
<Link href="/admin/settings/tags">
|
||||||
|
<Tags className="mr-2 h-4 w-4" />
|
||||||
|
Manage Expertise Tags
|
||||||
|
<ExternalLink className="ml-2 h-3 w-3" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="branding">
|
<TabsContent value="branding">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue