Add AI Tags button to admin projects page
Build and Push Docker Image / build (push) Has been cancelled Details

Adds a new "AI Tags" button that opens a dialog to batch-generate
expertise tags using AI for all untagged projects in a selected round.

The feature uses the existing tag.batchTagProjects endpoint which:
- Only processes projects without existing tags
- Preserves any manually added tags
- Logs AI usage for cost tracking

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Matt 2026-02-04 15:08:46 +01:00
parent d1f7f0361d
commit 5cbcad28ad
1 changed files with 94 additions and 0 deletions

View File

@ -52,7 +52,16 @@ import {
Search, Search,
Trash2, Trash2,
Loader2, Loader2,
Sparkles,
} from 'lucide-react' } from 'lucide-react'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Label } from '@/components/ui/label'
import { truncate } from '@/lib/utils' import { truncate } from '@/lib/utils'
import { ProjectLogo } from '@/components/shared/project-logo' import { ProjectLogo } from '@/components/shared/project-logo'
import { Pagination } from '@/components/shared/pagination' import { Pagination } from '@/components/shared/pagination'
@ -230,6 +239,30 @@ export default function ProjectsPage() {
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
const [projectToDelete, setProjectToDelete] = useState<{ id: string; title: string } | null>(null) const [projectToDelete, setProjectToDelete] = useState<{ id: string; title: string } | null>(null)
const [aiTagDialogOpen, setAiTagDialogOpen] = useState(false)
const [selectedRoundForTagging, setSelectedRoundForTagging] = useState<string>('')
// Fetch rounds for the AI tagging dialog
const { data: programs } = trpc.program.list.useQuery()
const { data: allRounds } = trpc.round.list.useQuery(
{ programId: programs?.[0]?.id ?? '' },
{ enabled: !!programs?.[0]?.id }
)
// AI batch tagging mutation
const batchTagProjects = trpc.tag.batchTagProjects.useMutation({
onSuccess: (result) => {
toast.success(
`AI Tagging complete: ${result.processed} tagged, ${result.skipped} skipped, ${result.failed} failed`
)
setAiTagDialogOpen(false)
setSelectedRoundForTagging('')
utils.project.list.invalidate()
},
onError: (error) => {
toast.error(error.message || 'Failed to generate AI tags')
},
})
const deleteProject = trpc.project.delete.useMutation({ const deleteProject = trpc.project.delete.useMutation({
onSuccess: () => { onSuccess: () => {
@ -259,6 +292,10 @@ export default function ProjectsPage() {
</p> </p>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Button variant="outline" onClick={() => setAiTagDialogOpen(true)}>
<Sparkles className="mr-2 h-4 w-4" />
AI Tags
</Button>
<Button variant="outline" asChild> <Button variant="outline" asChild>
<Link href="/admin/projects/import"> <Link href="/admin/projects/import">
<FileUp className="mr-2 h-4 w-4" /> <FileUp className="mr-2 h-4 w-4" />
@ -547,6 +584,63 @@ export default function ProjectsPage() {
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
{/* AI Tagging Dialog */}
<AlertDialog open={aiTagDialogOpen} onOpenChange={setAiTagDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2">
<Sparkles className="h-5 w-5" />
Generate AI Tags
</AlertDialogTitle>
<AlertDialogDescription>
Use AI to automatically generate expertise tags for all projects in a round
that don&apos;t have tags yet. This helps with jury matching and filtering.
</AlertDialogDescription>
</AlertDialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="round-select">Select Round</Label>
<Select
value={selectedRoundForTagging}
onValueChange={setSelectedRoundForTagging}
>
<SelectTrigger id="round-select">
<SelectValue placeholder="Choose a round..." />
</SelectTrigger>
<SelectContent>
{filterOptions?.rounds?.map((round) => (
<SelectItem key={round.id} value={round.id}>
{round.name} ({round.program?.name})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<p className="text-sm text-muted-foreground">
Only projects without existing tags will be processed. Existing tags are preserved.
</p>
</div>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
if (selectedRoundForTagging) {
batchTagProjects.mutate({ roundId: selectedRoundForTagging })
}
}}
disabled={!selectedRoundForTagging || batchTagProjects.isPending}
>
{batchTagProjects.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<Sparkles className="mr-2 h-4 w-4" />
)}
{batchTagProjects.isPending ? 'Processing...' : 'Generate Tags'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div> </div>
) )
} }