From 1bd9ebc974aec94aec747dd413023fcfca76d660 Mon Sep 17 00:00:00 2001 From: Desktop Date: Mon, 10 Nov 2025 23:47:36 +0500 Subject: [PATCH] Enhance AIEngine and ProgressModal for improved user feedback - Added user-friendly messages for input description, preparation, AI call, parsing, and saving phases in AIEngine. - Updated ProgressModal to display success messages and checklist-style progress steps based on function type. - Improved handling of step logs and current phase determination for better user experience during asynchronous tasks. --- backend/igny8_core/ai/engine.py | 125 +++++-- .../src/components/common/ProgressModal.tsx | 347 ++++++++++-------- 2 files changed, 291 insertions(+), 181 deletions(-) diff --git a/backend/igny8_core/ai/engine.py b/backend/igny8_core/ai/engine.py index ab8fc7b1..987ae8ea 100644 --- a/backend/igny8_core/ai/engine.py +++ b/backend/igny8_core/ai/engine.py @@ -25,6 +25,66 @@ class AIEngine: self.console_tracker = None # Will be initialized per function self.cost_tracker = CostTracker() + def _get_input_description(self, function_name: str, payload: dict, count: int) -> str: + """Get user-friendly input description""" + if function_name == 'auto_cluster': + return f"{count} keyword{'s' if count != 1 else ''}" + elif function_name == 'generate_ideas': + return f"{count} cluster{'s' if count != 1 else ''}" + elif function_name == 'generate_content': + return f"{count} task{'s' if count != 1 else ''}" + elif function_name == 'generate_images': + return f"{count} task{'s' if count != 1 else ''}" + return f"{count} item{'s' if count != 1 else ''}" + + def _get_prep_message(self, function_name: str, count: int, data: Any) -> str: + """Get user-friendly prep message""" + if function_name == 'auto_cluster': + return f"Loading {count} keyword{'s' if count != 1 else ''}" + elif function_name == 'generate_ideas': + return f"Loading {count} cluster{'s' if count != 1 else ''}" + elif function_name == 'generate_content': + return f"Preparing {count} content idea{'s' if count != 1 else ''}" + elif function_name == 'generate_images': + return f"Extracting image prompts from {count} task{'s' if count != 1 else ''}" + return f"Preparing {count} item{'s' if count != 1 else ''}" + + def _get_ai_call_message(self, function_name: str, count: int) -> str: + """Get user-friendly AI call message""" + if function_name == 'auto_cluster': + return f"Grouping {count} keyword{'s' if count != 1 else ''} into clusters" + elif function_name == 'generate_ideas': + return f"Generating content ideas for {count} cluster{'s' if count != 1 else ''}" + elif function_name == 'generate_content': + return f"Writing article{'s' if count != 1 else ''} with AI" + elif function_name == 'generate_images': + return f"Creating image{'s' if count != 1 else ''} with AI" + return f"Processing with AI" + + def _get_parse_message(self, function_name: str) -> str: + """Get user-friendly parse message""" + if function_name == 'auto_cluster': + return "Organizing clusters" + elif function_name == 'generate_ideas': + return "Structuring outlines" + elif function_name == 'generate_content': + return "Formatting content" + elif function_name == 'generate_images': + return "Processing images" + return "Processing results" + + def _get_save_message(self, function_name: str, count: int) -> str: + """Get user-friendly save message""" + if function_name == 'auto_cluster': + return f"Saving {count} cluster{'s' if count != 1 else ''}" + elif function_name == 'generate_ideas': + return f"Saving {count} idea{'s' if count != 1 else ''}" + elif function_name == 'generate_content': + return f"Saving {count} article{'s' if count != 1 else ''}" + elif function_name == 'generate_images': + return f"Saving {count} image{'s' if count != 1 else ''}" + return f"Saving {count} item{'s' if count != 1 else ''}" + def execute(self, fn: BaseAIFunction, payload: dict) -> dict: """ Unified execution pipeline for all AI functions. @@ -46,18 +106,23 @@ class AIEngine: try: # Phase 1: INIT - Validation & Setup (0-10%) - self.console_tracker.prep("Validating input payload") + # Extract input data for user-friendly messages + ids = payload.get('ids', []) + input_count = len(ids) if ids else 0 + input_description = self._get_input_description(function_name, payload, input_count) + + self.console_tracker.prep(f"Validating {input_description}") validated = fn.validate(payload, self.account) if not validated['valid']: self.console_tracker.error('ValidationError', validated['error']) return self._handle_error(validated['error'], fn) + validation_message = f"Validating {input_description}" self.console_tracker.prep("Validation complete") - self.step_tracker.add_request_step("INIT", "success", "Validation complete") - self.tracker.update("INIT", 10, "Validation complete", meta=self.step_tracker.get_meta()) + self.step_tracker.add_request_step("INIT", "success", validation_message) + self.tracker.update("INIT", 10, validation_message, meta=self.step_tracker.get_meta()) # Phase 2: PREP - Data Loading & Prompt Building (10-25%) - self.console_tracker.prep("Loading data from database") data = fn.prepare(payload, self.account) if isinstance(data, (list, tuple)): data_count = len(data) @@ -68,15 +133,16 @@ class AIEngine: elif 'keywords' in data: data_count = len(data['keywords']) else: - data_count = data.get('count', 1) + data_count = data.get('count', input_count) else: - data_count = 1 + data_count = input_count - self.console_tracker.prep(f"Building prompt from {data_count} items") + prep_message = self._get_prep_message(function_name, data_count, data) + self.console_tracker.prep(prep_message) prompt = fn.build_prompt(data, self.account) self.console_tracker.prep(f"Prompt built: {len(prompt)} characters") - self.step_tracker.add_request_step("PREP", "success", f"Loaded {data_count} items, built prompt ({len(prompt)} chars)") - self.tracker.update("PREP", 25, f"Data prepared: {data_count} items", meta=self.step_tracker.get_meta()) + self.step_tracker.add_request_step("PREP", "success", prep_message) + self.tracker.update("PREP", 25, prep_message, meta=self.step_tracker.get_meta()) # Phase 3: AI_CALL - Provider API Call (25-70%) ai_core = AICore(account=self.account) @@ -111,19 +177,7 @@ class AIEngine: exc_info=True, ) - # Track configured model information so it shows in the progress modal - self.step_tracker.add_request_step( - "PREP", - "success", - f"AI model in settings: {model_from_integration or 'Not set'}" - ) - self.step_tracker.add_request_step( - "PREP", - "success", - f"AI model selected for request: {model or 'default'}" - ) - - # Debug logging: Show model configuration + # Debug logging: Show model configuration (console only, not in step tracker) logger.info(f"[AIEngine] Model Configuration for {function_name}:") logger.info(f" - Model from get_model_config: {model}") logger.info(f" - Full model_config: {model_config}") @@ -132,13 +186,10 @@ class AIEngine: self.console_tracker.ai_call(f"Calling {model or 'default'} model with {len(prompt)} char prompt") self.console_tracker.ai_call(f"Function ID: {function_id}") - # Track AI call start - self.step_tracker.add_response_step( - "AI_CALL", - "success", - f"Calling {model or 'default'} model..." - ) - self.tracker.update("AI_CALL", 30, f"Sending to {model or 'default'}...", meta=self.step_tracker.get_meta()) + # Track AI call start with user-friendly message + ai_call_message = self._get_ai_call_message(function_name, data_count) + self.step_tracker.add_response_step("AI_CALL", "success", ai_call_message) + self.tracker.update("AI_CALL", 50, ai_call_message, meta=self.step_tracker.get_meta()) try: # Use centralized run_ai_request() with console logging (Stage 2 & 3 requirement) @@ -186,7 +237,8 @@ class AIEngine: # Phase 4: PARSE - Response Parsing (70-85%) try: - self.console_tracker.parse("Parsing AI response") + parse_message = self._get_parse_message(function_name) + self.console_tracker.parse(parse_message) response_content = raw_response.get('content', '') parsed = fn.parse_response(response_content, self.step_tracker) @@ -202,8 +254,8 @@ class AIEngine: parsed_count = 1 self.console_tracker.parse(f"Successfully parsed {parsed_count} items from response") - self.step_tracker.add_response_step("PARSE", "success", f"Parsed {parsed_count} items from AI response") - self.tracker.update("PARSE", 85, f"Parsed {parsed_count} items", meta=self.step_tracker.get_meta()) + self.step_tracker.add_response_step("PARSE", "success", parse_message) + self.tracker.update("PARSE", 85, parse_message, meta=self.step_tracker.get_meta()) except Exception as parse_error: error_msg = f"Failed to parse AI response: {str(parse_error)}" logger.error(f"AIEngine: {error_msg}", exc_info=True) @@ -211,20 +263,19 @@ class AIEngine: return self._handle_error(error_msg, fn) # Phase 5: SAVE - Database Operations (85-98%) - self.console_tracker.save("Saving results to database") # Pass step_tracker to save_output so it can add validation steps save_result = fn.save_output(parsed, data, self.account, self.tracker, step_tracker=self.step_tracker) clusters_created = save_result.get('clusters_created', 0) keywords_updated = save_result.get('keywords_updated', 0) count = save_result.get('count', 0) - # Build success message based on function type + # Use user-friendly save message based on function type if clusters_created: - save_msg = f"Created {clusters_created} clusters, updated {keywords_updated} keywords" + save_msg = f"Saving {clusters_created} cluster{'s' if clusters_created != 1 else ''}" elif count: - save_msg = f"Saved {count} items" + save_msg = self._get_save_message(function_name, count) else: - save_msg = "Results saved successfully" + save_msg = self._get_save_message(function_name, data_count) self.console_tracker.save(save_msg) self.step_tracker.add_request_step("SAVE", "success", save_msg) diff --git a/frontend/src/components/common/ProgressModal.tsx b/frontend/src/components/common/ProgressModal.tsx index b142c03e..178aa2b7 100644 --- a/frontend/src/components/common/ProgressModal.tsx +++ b/frontend/src/components/common/ProgressModal.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useRef } from 'react'; import { Modal } from '../ui/modal'; -import { ProgressBar } from '../ui/progress'; import Button from '../ui/button/Button'; export interface ProgressModalProps { @@ -29,11 +28,113 @@ export interface ProgressModalProps { }>; // Step logs for debugging } -// Generate modal instance ID (increments per modal instance) -let modalInstanceCounter = 0; -const getModalInstanceId = () => { - modalInstanceCounter++; - return `modal-${String(modalInstanceCounter).padStart(2, '0')}`; +// Success messages per function +const getSuccessMessage = (functionId?: string, title?: string): string => { + const funcName = functionId?.toLowerCase() || title?.toLowerCase() || ''; + + if (funcName.includes('cluster')) { + return 'Clustering complete — keywords grouped into meaningful clusters.'; + } + if (funcName.includes('idea')) { + return 'Content ideas and outlines created successfully.'; + } + if (funcName.includes('content')) { + return 'Article drafted successfully.'; + } + if (funcName.includes('image')) { + return 'Images created and saved successfully.'; + } + return 'Task completed successfully.'; +}; + +// Get step definitions per function +const getStepsForFunction = (functionId?: string, title?: string): Array<{phase: string, label: string}> => { + const funcName = functionId?.toLowerCase() || title?.toLowerCase() || ''; + + if (funcName.includes('cluster')) { + return [ + { phase: 'INIT', label: 'Validating keywords' }, + { phase: 'PREP', label: 'Loading keyword data' }, + { phase: 'AI_CALL', label: 'Generating clusters' }, + { phase: 'PARSE', label: 'Organizing clusters' }, + { phase: 'SAVE', label: 'Saving clusters' }, + ]; + } + + if (funcName.includes('idea')) { + return [ + { phase: 'INIT', label: 'Validating clusters' }, + { phase: 'PREP', label: 'Loading cluster data' }, + { phase: 'AI_CALL', label: 'Generating blog ideas' }, + { phase: 'PARSE', label: 'Structuring outlines' }, + { phase: 'SAVE', label: 'Saving ideas' }, + ]; + } + + if (funcName.includes('content')) { + return [ + { phase: 'INIT', label: 'Validating task' }, + { phase: 'PREP', label: 'Preparing content idea' }, + { phase: 'AI_CALL', label: 'Writing article with AI' }, + { phase: 'PARSE', label: 'Formatting content' }, + { phase: 'SAVE', label: 'Saving article' }, + ]; + } + + if (funcName.includes('image')) { + return [ + { phase: 'INIT', label: 'Validating task' }, + { phase: 'PREP', label: 'Extracting image prompts' }, + { phase: 'AI_CALL', label: 'Creating images with AI' }, + { phase: 'PARSE', label: 'Processing images' }, + { phase: 'SAVE', label: 'Saving images' }, + ]; + } + + // Default fallback + return [ + { phase: 'INIT', label: 'Initializing...' }, + { phase: 'PREP', label: 'Preparing...' }, + { phase: 'AI_CALL', label: 'Processing with AI...' }, + { phase: 'PARSE', label: 'Processing results...' }, + { phase: 'SAVE', label: 'Saving results...' }, + ]; +}; + +// Get current phase from step logs or percentage +const getCurrentPhase = (stepLogs: any[], percentage: number): string => { + if (stepLogs.length > 0) { + const lastStep = stepLogs[stepLogs.length - 1]; + return lastStep.stepName || ''; + } + + // Fallback to percentage + if (percentage < 10) return 'INIT'; + if (percentage < 25) return 'PREP'; + if (percentage < 70) return 'AI_CALL'; + if (percentage < 85) return 'PARSE'; + if (percentage < 100) return 'SAVE'; + return 'DONE'; +}; + +// Check if step is completed +const isStepCompleted = (stepPhase: string, currentPhase: string, stepLogs: any[]): boolean => { + const phaseOrder = ['INIT', 'PREP', 'AI_CALL', 'PARSE', 'SAVE', 'DONE']; + const stepIndex = phaseOrder.indexOf(stepPhase); + const currentIndex = phaseOrder.indexOf(currentPhase); + + // Step is completed if we've moved past it + if (currentIndex > stepIndex) return true; + + // Or if we have a log entry for it with success status + return stepLogs.some(log => + log.stepName === stepPhase && log.status === 'success' + ); +}; + +// Check if step is in progress +const isStepInProgress = (stepPhase: string, currentPhase: string): boolean => { + return stepPhase === currentPhase; }; export default function ProgressModal({ @@ -42,110 +143,52 @@ export default function ProgressModal({ percentage, status, message, - details, onClose, onCancel, taskId, functionId, stepLogs = [], }: ProgressModalProps) { - // Generate modal instance ID on first render - const modalInstanceIdRef = React.useRef(null); - React.useEffect(() => { - if (!modalInstanceIdRef.current) { - modalInstanceIdRef.current = getModalInstanceId(); - } - }, []); + const hasAutoClosedRef = useRef(false); - const modalInstanceId = modalInstanceIdRef.current || 'modal-01'; - - // Build full function ID with modal instance - const fullFunctionId = functionId ? `${functionId}-${modalInstanceId}` : null; - // Auto-close on completion after 2 seconds - // Don't auto-close on error - let user manually close to see error details - const hasAutoClosedRef = React.useRef(false); + // Auto-close on completion after 3 seconds useEffect(() => { if (status === 'completed' && onClose && !hasAutoClosedRef.current) { hasAutoClosedRef.current = true; const timer = setTimeout(() => { onClose(); - }, 2000); + }, 3000); return () => clearTimeout(timer); } - // Reset when status changes away from completed if (status !== 'completed') { hasAutoClosedRef.current = false; } - // Don't auto-close on error - user should manually dismiss }, [status, onClose]); - // Determine color based on status - const getProgressColor = (): 'primary' | 'success' | 'error' | 'warning' => { - if (status === 'error') return 'error'; - if (status === 'completed') return 'success'; - if (status === 'processing') return 'primary'; - return 'primary'; - }; + // Get steps for this function + const steps = getStepsForFunction(functionId, title); + const currentPhase = getCurrentPhase(stepLogs, percentage); + + // Build checklist items + const checklistItems = steps.map((step) => { + const completed = isStepCompleted(step.phase, currentPhase, stepLogs); + const inProgress = isStepInProgress(step.phase, currentPhase); + + // Get user-friendly message from step logs if available + const stepLog = stepLogs.find(log => log.stepName === step.phase); + const stepMessage = stepLog?.message || step.label; + + return { + label: stepMessage, + phase: step.phase, + completed, + inProgress, + }; + }); - // Get status icon - const getStatusIcon = () => { - if (status === 'completed') { - return ( - - - - ); - } - if (status === 'error') { - return ( - - - - ); - } - // Processing/Pending - spinner - return ( - - - - - ); - }; + // Show success alert when completed + const showSuccess = status === 'completed'; + const successMessage = getSuccessMessage(functionId, title); return ( {/* Header */}
-
{getStatusIcon()}
+ {!showSuccess && ( +
+ {status === 'error' ? ( + + + + ) : ( + + + + + )} +
+ )}

{title}

-

{message}

+ {!showSuccess && ( +

{message}

+ )}
- {/* Progress Bar */} -
- -
- - {/* Function ID and Task ID (for debugging) */} - {(fullFunctionId || taskId) && ( -
- {fullFunctionId && ( -
Function ID: {fullFunctionId}
- )} - {taskId && ( -
Task ID: {taskId}
- )} + {/* Success Alert (shown when completed) */} + {showSuccess && ( +
+
+ + + +

+ {successMessage} +

+
)} - {/* Step Logs / Debug Logs */} - {stepLogs.length > 0 && ( -
-
-

- Step Logs -

- - {stepLogs.length} step{stepLogs.length !== 1 ? 's' : ''} - -
-
- {stepLogs.map((step, index) => ( -
+ {checklistItems.map((item, index) => ( +
+ {/* Icon */} +
+ {item.completed ? ( + + + + ) : item.inProgress ? ( + + + + + ) : ( +
+ )} +
+ + {/* Step Text */} + -
- - [{step.stepNumber}] - - {step.stepName}: - {step.message} -
-
- ))} -
+ {item.label} + +
+ ))}
)} @@ -247,4 +307,3 @@ export default function ProgressModal({ ); } -