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:
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user