diff --git a/backend/igny8_core/ai/engine.py b/backend/igny8_core/ai/engine.py index 987ae8ea..c5d3a663 100644 --- a/backend/igny8_core/ai/engine.py +++ b/backend/igny8_core/ai/engine.py @@ -37,6 +37,29 @@ class AIEngine: return f"{count} task{'s' if count != 1 else ''}" return f"{count} item{'s' if count != 1 else ''}" + def _build_validation_message(self, function_name: str, payload: dict, count: int, input_description: str) -> str: + """Build validation message with item names for better UX""" + if function_name == 'auto_cluster' and count > 0: + try: + from igny8_core.modules.planner.models import Keywords + ids = payload.get('ids', []) + keywords = Keywords.objects.filter(id__in=ids, account=self.account).values_list('keyword', flat=True)[:3] + keyword_list = list(keywords) + + if len(keyword_list) > 0: + remaining = count - len(keyword_list) + if remaining > 0: + keywords_text = ', '.join(keyword_list) + return f"Validating {keywords_text} and {remaining} more keyword{'s' if remaining != 1 else ''}" + else: + keywords_text = ', '.join(keyword_list) + return f"Validating {keywords_text}" + except Exception as e: + logger.warning(f"Failed to load keyword names for validation message: {e}") + + # Fallback to simple count message + return f"Validating {input_description}" + def _get_prep_message(self, function_name: str, count: int, data: Any) -> str: """Get user-friendly prep message""" if function_name == 'auto_cluster': @@ -73,6 +96,18 @@ class AIEngine: return "Processing images" return "Processing results" + def _get_parse_message_with_count(self, function_name: str, count: int) -> str: + """Get user-friendly parse message with count""" + if function_name == 'auto_cluster': + return f"{count} cluster{'s' if count != 1 else ''} created" + elif function_name == 'generate_ideas': + return f"{count} idea{'s' if count != 1 else ''} created" + elif function_name == 'generate_content': + return f"{count} article{'s' if count != 1 else ''} created" + elif function_name == 'generate_images': + return f"{count} image{'s' if count != 1 else ''} created" + return f"{count} item{'s' if count != 1 else ''} processed" + def _get_save_message(self, function_name: str, count: int) -> str: """Get user-friendly save message""" if function_name == 'auto_cluster': @@ -117,7 +152,9 @@ class AIEngine: self.console_tracker.error('ValidationError', validated['error']) return self._handle_error(validated['error'], fn) - validation_message = f"Validating {input_description}" + # Build validation message with keyword names for auto_cluster + validation_message = self._build_validation_message(function_name, payload, input_count, input_description) + self.console_tracker.prep("Validation complete") self.step_tracker.add_request_step("INIT", "success", validation_message) self.tracker.update("INIT", 10, validation_message, meta=self.step_tracker.get_meta()) @@ -253,6 +290,9 @@ class AIEngine: else: parsed_count = 1 + # Update parse message with count for better UX + parse_message = self._get_parse_message_with_count(function_name, parsed_count) + self.console_tracker.parse(f"Successfully parsed {parsed_count} items from response") self.step_tracker.add_response_step("PARSE", "success", parse_message) self.tracker.update("PARSE", 85, parse_message, meta=self.step_tracker.get_meta()) diff --git a/frontend/src/components/common/ProgressModal.tsx b/frontend/src/components/common/ProgressModal.tsx index 278aa61e..6eda9fbd 100644 --- a/frontend/src/components/common/ProgressModal.tsx +++ b/frontend/src/components/common/ProgressModal.tsx @@ -201,15 +201,93 @@ export default function ProgressModal({ const steps = getStepsForFunction(functionId, title); const currentPhase = getCurrentPhase(stepLogs, percentage); + // Format step message with counts and better formatting + const formatStepMessage = (stepPhase: string, stepLog: any, defaultLabel: string, allStepLogs: any[], functionId?: string, title?: string): string => { + const funcName = (functionId || title || '').toLowerCase(); + const message = stepLog?.message || defaultLabel; + + // Extract counts from message + const extractCount = (pattern: RegExp): string => { + const match = message.match(pattern); + return match && match[1] ? match[1] : ''; + }; + + if (funcName.includes('cluster')) { + if (stepPhase === 'INIT') { + // For INIT: Message already includes keyword names from backend (e.g., "Validating keyword1, keyword2, keyword3 and 5 more keywords") + // Just return the message as-is since backend formats it + return message; + } else if (stepPhase === 'PREP') { + // For PREP: Show count of keywords being loaded + const keywordCount = extractCount(/(\d+)\s+keyword/i); + if (keywordCount) { + return `Loading ${keywordCount} keyword${keywordCount !== '1' ? 's' : ''} for clustering`; + } + return message; + } else if (stepPhase === 'AI_CALL') { + // For AI_CALL: Show count of keywords being sent + const keywordCount = extractCount(/(\d+)\s+keyword/i); + if (keywordCount) { + return `Sending ${keywordCount} keyword${keywordCount !== '1' ? 's' : ''} for clustering`; + } + return message; + } else if (stepPhase === 'PARSE') { + // For PARSE: Show count of clusters created + const clusterCount = extractCount(/(\d+)\s+cluster/i); + if (clusterCount) { + return `${clusterCount} cluster${clusterCount !== '1' ? 's' : ''} created`; + } + // Try to find cluster count in any step log + for (const log of allStepLogs) { + const count = log.message?.match(/(\d+)\s+cluster/i); + if (count && count[1]) { + return `${count[1]} cluster${count[1] !== '1' ? 's' : ''} created`; + } + } + return message; + } else if (stepPhase === 'SAVE') { + // For SAVE: Show count of clusters being saved + const clusterCount = extractCount(/(\d+)\s+cluster/i); + if (clusterCount) { + return `Saving ${clusterCount} cluster${clusterCount !== '1' ? 's' : ''}`; + } + return message; + } + } else if (funcName.includes('idea')) { + if (stepPhase === 'PARSE') { + const ideaCount = extractCount(/(\d+)\s+idea/i); + if (ideaCount) { + return `${ideaCount} idea${ideaCount !== '1' ? 's' : ''} created`; + } + } + } else if (funcName.includes('content')) { + if (stepPhase === 'PARSE') { + const articleCount = extractCount(/(\d+)\s+article/i); + if (articleCount) { + return `${articleCount} article${articleCount !== '1' ? 's' : ''} created`; + } + } + } else if (funcName.includes('image')) { + if (stepPhase === 'PARSE') { + const imageCount = extractCount(/(\d+)\s+image/i); + if (imageCount) { + return `${imageCount} image${imageCount !== '1' ? 's' : ''} created`; + } + } + } + + return message; + }; + // Build checklist items with visual completion state (needed for allStepsVisuallyCompleted) const checklistItems = steps.map((step) => { const actuallyCompleted = isStepCompleted(step.phase, currentPhase, stepLogs); const visuallyCompleted = visuallyCompletedSteps.has(step.phase); const inProgress = isStepInProgress(step.phase, currentPhase) && !visuallyCompleted; - // Get user-friendly message from step logs if available (includes counts from backend) + // Get step log and format message const stepLog = stepLogs.find(log => log.stepName === step.phase); - const stepMessage = stepLog?.message || step.label; + const stepMessage = formatStepMessage(step.phase, stepLog, step.label, stepLogs, functionId, title); return { label: stepMessage, @@ -329,9 +407,12 @@ export default function ProgressModal({

{title}

- {!showSuccess && ( + {!showSuccess && status !== 'completed' && (

{message}

)} + {status === 'completed' && !allStepsVisuallyCompleted && ( +

Processing...

+ )}