Redesign AI Tagging dialog and add edition-wide tagging

- Redesign AI Tagging dialog with scope selection (Round vs Edition)
- Add visual progress indicator during AI processing
- Display result stats (tagged/skipped/failed) after completion
- Add batchTagProgramProjects endpoint for edition-wide tagging
- Fix getFilterOptions to include program.id for filtering
- Improve error handling with toast notifications

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-05 10:27:52 +01:00
parent 7f95f681d6
commit 05862f1e55
4 changed files with 469 additions and 104 deletions

View File

@@ -406,6 +406,48 @@ export async function tagProject(
}
}
/**
* Common validation and setup for batch tagging
*/
async function validateBatchTagging(): Promise<{
valid: boolean
error?: string
availableTags?: AvailableTag[]
}> {
const settings = await getTaggingSettings()
console.log('[AI Tagging] Settings:', settings)
if (!settings.enabled) {
console.log('[AI Tagging] AI tagging is disabled in settings')
return {
valid: false,
error: 'AI tagging is disabled. Enable it in Settings > AI or set ai_enabled to true.',
}
}
// Check if OpenAI is configured
const openai = await getOpenAI()
if (!openai) {
console.log('[AI Tagging] OpenAI is not configured')
return {
valid: false,
error: 'OpenAI API is not configured. Add your API key in Settings > AI.',
}
}
// Check if there are any available tags
const availableTags = await getAvailableTags()
console.log(`[AI Tagging] Found ${availableTags.length} available expertise tags`)
if (availableTags.length === 0) {
return {
valid: false,
error: 'No expertise tags defined. Create tags in Settings > Tags first.',
}
}
return { valid: true, availableTags }
}
/**
* Batch tag all untagged projects in a round
*
@@ -416,42 +458,13 @@ export async function batchTagProjects(
userId?: string,
onProgress?: (processed: number, total: number) => void
): Promise<BatchTaggingResult> {
const settings = await getTaggingSettings()
console.log('[AI Tagging] Settings:', settings)
if (!settings.enabled) {
console.log('[AI Tagging] AI tagging is disabled in settings')
const validation = await validateBatchTagging()
if (!validation.valid) {
return {
processed: 0,
failed: 0,
skipped: 0,
errors: ['AI tagging is disabled. Enable it in Settings > AI or set ai_enabled to true.'],
results: [],
}
}
// Check if OpenAI is configured
const openai = await getOpenAI()
if (!openai) {
console.log('[AI Tagging] OpenAI is not configured')
return {
processed: 0,
failed: 0,
skipped: 0,
errors: ['OpenAI API is not configured. Add your API key in Settings > AI.'],
results: [],
}
}
// Check if there are any available tags
const availableTags = await getAvailableTags()
console.log(`[AI Tagging] Found ${availableTags.length} available expertise tags`)
if (availableTags.length === 0) {
return {
processed: 0,
failed: 0,
skipped: 0,
errors: ['No expertise tags defined. Create tags in Settings > Tags first.'],
errors: [validation.error!],
results: [],
}
}
@@ -462,16 +475,13 @@ export async function batchTagProjects(
include: {
files: { select: { fileType: true } },
_count: { select: { teamMembers: true, files: true } },
projectTags: { select: { tagId: true } },
},
})
console.log(`[AI Tagging] Found ${allProjects.length} total projects in round`)
// Filter to only projects that truly have no tags (empty tags array AND no projectTags)
const untaggedProjects = allProjects.filter(p =>
(p.tags.length === 0) && (p.projectTags.length === 0)
)
// Filter to only projects that truly have no tags (empty tags array)
const untaggedProjects = allProjects.filter(p => p.tags.length === 0)
const alreadyTaggedCount = allProjects.length - untaggedProjects.length
console.log(`[AI Tagging] ${untaggedProjects.length} untagged projects, ${alreadyTaggedCount} already have tags`)
@@ -513,7 +523,90 @@ export async function batchTagProjects(
return {
processed,
failed,
skipped: 0,
skipped: alreadyTaggedCount,
errors,
results,
}
}
/**
* Batch tag all untagged projects in an entire program (edition)
*
* Processes all projects across all rounds in the program.
*/
export async function batchTagProgramProjects(
programId: string,
userId?: string,
onProgress?: (processed: number, total: number) => void
): Promise<BatchTaggingResult> {
const validation = await validateBatchTagging()
if (!validation.valid) {
return {
processed: 0,
failed: 0,
skipped: 0,
errors: [validation.error!],
results: [],
}
}
// Get ALL projects in the program (across all rounds)
const allProjects = await prisma.project.findMany({
where: {
round: { programId },
},
include: {
files: { select: { fileType: true } },
_count: { select: { teamMembers: true, files: true } },
},
})
console.log(`[AI Tagging] Found ${allProjects.length} total projects in program`)
// Filter to only projects that truly have no tags (empty tags array)
const untaggedProjects = allProjects.filter(p => p.tags.length === 0)
const alreadyTaggedCount = allProjects.length - untaggedProjects.length
console.log(`[AI Tagging] ${untaggedProjects.length} untagged projects, ${alreadyTaggedCount} already have tags`)
if (untaggedProjects.length === 0) {
return {
processed: 0,
failed: 0,
skipped: alreadyTaggedCount,
errors: alreadyTaggedCount > 0
? []
: ['No projects found in this program'],
results: [],
}
}
const results: TaggingResult[] = []
let processed = 0
let failed = 0
const errors: string[] = []
for (let i = 0; i < untaggedProjects.length; i++) {
const project = untaggedProjects[i]
try {
const result = await tagProject(project.id, userId)
results.push(result)
processed++
} catch (error) {
failed++
errors.push(`${project.title}: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
// Report progress
if (onProgress) {
onProgress(i + 1, untaggedProjects.length)
}
}
return {
processed,
failed,
skipped: alreadyTaggedCount,
errors,
results,
}