diff --git a/modules/services/serviceGeneration/renderers/rendererExcel.py b/modules/services/serviceGeneration/renderers/rendererExcel.py index 142892eb..6ea4ff32 100644 --- a/modules/services/serviceGeneration/renderers/rendererExcel.py +++ b/modules/services/serviceGeneration/renderers/rendererExcel.py @@ -262,22 +262,110 @@ class RendererExcel(BaseRenderer): } style_template = self._create_ai_style_template("xlsx", user_prompt, style_schema) - styles = await self._get_ai_styles(ai_service, style_template, self._get_default_excel_styles()) + # Use our own _get_ai_styles_with_excel_colors method to ensure proper color conversion + styles = await self._get_ai_styles_with_excel_colors(ai_service, style_template, self._get_default_excel_styles()) - # Convert colors to aRGB format and validate - styles = self._convert_colors_format(styles) + # Validate and fix contrast issues return self._validate_excel_styles_contrast(styles) + async def _get_ai_styles_with_excel_colors(self, ai_service, style_template: str, default_styles: Dict[str, Any]) -> Dict[str, Any]: + """Get AI styles with proper Excel color conversion.""" + if not ai_service: + return default_styles + + try: + from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationType + + request_options = AiCallOptions() + request_options.operationType = OperationType.GENERAL + + request = AiCallRequest(prompt=style_template, context="", options=request_options) + response = await ai_service.aiObjects.call(request) + + import json + import re + + # Debug output + print(f"🔍 AI STYLING RESPONSE TYPE: {type(response)}") + print(f"🔍 AI STYLING RESPONSE LENGTH: {len(response.content) if response and hasattr(response, 'content') and response.content else 0}") + + # Clean and parse JSON + result = response.content.strip() if response and response.content else "" + + # Check if result is empty + if not result: + self.logger.warning("AI styling returned empty response, using defaults") + return default_styles + + # Extract JSON from markdown if present + json_match = re.search(r'```json\s*\n(.*?)\n```', result, re.DOTALL) + if json_match: + result = json_match.group(1).strip() + print(f"🔍 EXTRACTED JSON FROM MARKDOWN: {result[:100]}...") + elif result.startswith('```json'): + result = re.sub(r'^```json\s*', '', result) + result = re.sub(r'\s*```$', '', result) + print(f"🔍 CLEANED JSON FROM MARKDOWN: {result[:100]}...") + elif result.startswith('```'): + result = re.sub(r'^```\s*', '', result) + result = re.sub(r'\s*```$', '', result) + print(f"🔍 CLEANED JSON FROM GENERIC MARKDOWN: {result[:100]}...") + + # Try to parse JSON + try: + styles = json.loads(result) + print(f"🔍 AI STYLING PARSED KEYS: {list(styles.keys()) if isinstance(styles, dict) else 'Not a dict'}") + except json.JSONDecodeError as json_error: + print(f"🔍 AI STYLING JSON ERROR: {json_error}") + print(f"🔍 AI STYLING RAW RESULT: {result[:200]}...") + self.logger.warning(f"AI styling returned invalid JSON: {json_error}, using defaults") + return default_styles + + # Convert colors to Excel aRGB format + styles = self._convert_colors_format(styles) + + return styles + + except Exception as e: + self.logger.warning(f"AI styling failed: {str(e)}, using defaults") + return default_styles + + def _get_safe_color(self, color_value: str, default: str = "FF000000") -> str: + """Get a safe aRGB color value for Excel (without # prefix).""" + if not isinstance(color_value, str): + return default + + # Remove # prefix if present + if color_value.startswith('#'): + color_value = color_value[1:] + + if len(color_value) == 6: + # Convert RRGGBB to AARRGGBB + return f"FF{color_value}" + elif len(color_value) == 8: + # Already aRGB format + return color_value + else: + # Unexpected format, return default + return default + def _convert_colors_format(self, styles: Dict[str, Any]) -> Dict[str, Any]: """Convert hex colors to aRGB format for Excel compatibility.""" try: + print(f"🔍 CONVERTING COLORS IN STYLES: {styles}") for style_name, style_config in styles.items(): if isinstance(style_config, dict): for prop, value in style_config.items(): if isinstance(value, str) and value.startswith('#') and len(value) == 7: # Convert #RRGGBB to #AARRGGBB (add FF alpha channel) + old_value = value styles[style_name][prop] = f"FF{value[1:]}" - print(f"🔍 CONVERTED COLOR: {value} → {styles[style_name][prop]}") + print(f"🔍 CONVERTED COLOR: {old_value} → {styles[style_name][prop]}") + elif isinstance(value, str) and value.startswith('#') and len(value) == 9: + print(f"🔍 COLOR ALREADY aRGB: {value}") + elif isinstance(value, str) and value.startswith('#'): + print(f"🔍 UNEXPECTED COLOR FORMAT: {value} (length: {len(value)})") + print(f"🔍 FINAL CONVERTED STYLES: {styles}") return styles except Exception as e: print(f"🔍 COLOR CONVERSION ERROR: {e}") @@ -361,23 +449,30 @@ class RendererExcel(BaseRenderer): if not sections: return ["Content"] - # Generate sheet names based on content types + # Generate sheet names based on content structure sheet_names = [] - # Always start with a main content sheet - document_title = json_content.get("metadata", {}).get("title", "Document") - sheet_names.append(document_title[:31]) # Excel sheet name limit + # Check if we have multiple table sections + table_sections = [s for s in sections if s.get("type") == "table"] - # Add sheets based on content types found - content_types = set() - for section in sections: - content_type = section.get("content_type", "paragraph") - content_types.add(content_type) - - # Create sheets for different content types if we have multiple types - if len(content_types) > 1: - if "table" in content_types: - sheet_names.append("Tables") + if len(table_sections) > 1: + # Create separate sheets for each table + for i, section in enumerate(table_sections, 1): + section_title = section.get("title", f"Table {i}") + sheet_names.append(section_title[:31]) # Excel sheet name limit + else: + # Single table or mixed content - create main sheet + document_title = json_content.get("metadata", {}).get("title", "Document") + sheet_names.append(document_title[:31]) # Excel sheet name limit + + # Add additional sheets for other content types + content_types = set() + for section in sections: + content_type = section.get("type", "paragraph") + content_types.add(content_type) + + if "table" in content_types and len(table_sections) == 1: + sheet_names.append("Table Data") if "list" in content_types: sheet_names.append("Lists") if "paragraph" in content_types or "heading" in content_types: @@ -395,17 +490,69 @@ class RendererExcel(BaseRenderer): if not sheet_names: return - # Populate the first sheet with all content - first_sheet_name = sheet_names[0] - self._populate_main_sheet(sheets[first_sheet_name], json_content, styles) + sections = json_content.get("sections", []) + table_sections = [s for s in sections if s.get("type") == "table"] - # If we have multiple sheets, distribute content by type - if len(sheet_names) > 1: - self._populate_content_type_sheets(sheets, json_content, styles, sheet_names[1:]) + if len(table_sections) > 1: + # Multiple tables - populate each sheet with its corresponding table + for i, section in enumerate(table_sections): + if i < len(sheet_names): + sheet_name = sheet_names[i] + sheet = sheets[sheet_name] + self._populate_table_sheet(sheet, section, styles, f"Table {i+1}") + else: + # Single table or mixed content - use original logic + first_sheet_name = sheet_names[0] + self._populate_main_sheet(sheets[first_sheet_name], json_content, styles) + + # If we have multiple sheets, distribute content by type + if len(sheet_names) > 1: + self._populate_content_type_sheets(sheets, json_content, styles, sheet_names[1:]) except Exception as e: self.logger.warning(f"Could not populate Excel sheets: {str(e)}") + def _populate_table_sheet(self, sheet, section: Dict[str, Any], styles: Dict[str, Any], sheet_title: str): + """Populate a sheet with a single table section.""" + try: + # Sheet title + sheet['A1'] = sheet_title + sheet['A1'].font = Font(size=16, bold=True, color=self._get_safe_color(styles.get("title", {}).get("color", "FF1F4E79"))) + sheet['A1'].alignment = Alignment(horizontal="center") + + # Get table data + table_data = section.get("data", {}) + headers = table_data.get("headers", []) + rows = table_data.get("rows", []) + + if not headers and not rows: + sheet['A3'] = "No table data available" + return + + # Add headers + header_style = styles.get("table_header", {}) + for col, header in enumerate(headers, 1): + cell = sheet.cell(row=3, column=col, value=header) + if header_style.get("bold"): + cell.font = Font(bold=True, color=self._get_safe_color(header_style.get("text_color", "FF000000"))) + if header_style.get("background"): + cell.fill = PatternFill(start_color=self._get_safe_color(header_style["background"]), end_color=self._get_safe_color(header_style["background"]), fill_type="solid") + + # Add rows + cell_style = styles.get("table_cell", {}) + for row_idx, row_data in enumerate(rows, 4): + for col_idx, cell_value in enumerate(row_data, 1): + cell = sheet.cell(row=row_idx, column=col_idx, value=cell_value) + if cell_style.get("text_color"): + cell.font = Font(color=self._get_safe_color(cell_style["text_color"])) + + # Auto-adjust column widths + for col in range(1, len(headers) + 1): + sheet.column_dimensions[get_column_letter(col)].width = 20 + + except Exception as e: + self.logger.warning(f"Could not populate table sheet: {str(e)}") + def _populate_main_sheet(self, sheet, json_content: Dict[str, Any], styles: Dict[str, Any]): """Populate the main sheet with document overview and all content.""" try: @@ -416,8 +563,17 @@ class RendererExcel(BaseRenderer): # Safety check for title style title_style = styles.get("title", {"font_size": 16, "bold": True, "color": "#FF1F4E79", "align": "center"}) print(f"🔍 EXCEL TITLE STYLE: {title_style}") - sheet['A1'].font = Font(size=title_style["font_size"], bold=title_style["bold"], color=title_style["color"]) - sheet['A1'].alignment = Alignment(horizontal=title_style["align"]) + print(f"🔍 EXCEL TITLE COLOR: {title_style['color']} (type: {type(title_style['color'])}, length: {len(title_style['color']) if isinstance(title_style['color'], str) else 'not string'})") + try: + safe_color = self._get_safe_color(title_style["color"]) + sheet['A1'].font = Font(size=title_style["font_size"], bold=title_style["bold"], color=safe_color) + sheet['A1'].alignment = Alignment(horizontal=title_style["align"]) + print(f"🔍 EXCEL TITLE FONT CREATED SUCCESSFULLY with color: {safe_color}") + except Exception as font_error: + print(f"🔍 EXCEL TITLE FONT ERROR: {font_error}") + # Try with a safe color + sheet['A1'].font = Font(size=title_style["font_size"], bold=title_style["bold"], color="FF000000") + sheet['A1'].alignment = Alignment(horizontal=title_style["align"]) # Generation info sheet['A3'] = "Generated:" @@ -516,21 +672,26 @@ class RendererExcel(BaseRenderer): sheet[f'A{start_row}'].font = Font(bold=True) start_row += 1 - # Process section elements - elements = section.get("elements", []) - content_type = section.get("content_type", "paragraph") + # Process section based on type + section_type = section.get("type", "paragraph") - for element in elements: - if content_type == "table": - start_row = self._add_table_to_excel(sheet, element, styles, start_row) - elif content_type == "list": - start_row = self._add_list_to_excel(sheet, element, styles, start_row) - elif content_type == "paragraph": - start_row = self._add_paragraph_to_excel(sheet, element, styles, start_row) - elif content_type == "heading": - start_row = self._add_heading_to_excel(sheet, element, styles, start_row) - else: - start_row = self._add_paragraph_to_excel(sheet, element, styles, start_row) + if section_type == "table": + # Handle table section directly + table_data = section.get("data", {}) + if table_data: + start_row = self._add_table_to_excel(sheet, table_data, styles, start_row) + else: + # Handle other section types + elements = section.get("elements", []) + for element in elements: + if section_type == "list": + start_row = self._add_list_to_excel(sheet, element, styles, start_row) + elif section_type == "paragraph": + start_row = self._add_paragraph_to_excel(sheet, element, styles, start_row) + elif section_type == "heading": + start_row = self._add_heading_to_excel(sheet, element, styles, start_row) + else: + start_row = self._add_paragraph_to_excel(sheet, element, styles, start_row) return start_row @@ -553,7 +714,7 @@ class RendererExcel(BaseRenderer): for col, header in enumerate(headers, 1): cell = sheet.cell(row=start_row, column=col, value=header) if header_style.get("bold"): - cell.font = Font(bold=True, color=header_style.get("text_color", "#FF000000")) + cell.font = Font(bold=True, color=self._get_safe_color(header_style.get("text_color", "FF000000"))) if header_style.get("background"): cell.fill = PatternFill(start_color=header_style["background"], end_color=header_style["background"], fill_type="solid") @@ -565,7 +726,7 @@ class RendererExcel(BaseRenderer): for col, cell_value in enumerate(row_data, 1): cell = sheet.cell(row=start_row, column=col, value=cell_value) if cell_style.get("text_color"): - cell.font = Font(color=cell_style["text_color"]) + cell.font = Font(color=self._get_safe_color(cell_style["text_color"])) start_row += 1 return start_row @@ -583,7 +744,7 @@ class RendererExcel(BaseRenderer): for item in list_items: sheet.cell(row=start_row, column=1, value=f"• {item}") if list_style.get("color"): - sheet.cell(row=start_row, column=1).font = Font(color=list_style["color"]) + sheet.cell(row=start_row, column=1).font = Font(color=self._get_safe_color(list_style["color"])) start_row += 1 return start_row @@ -601,7 +762,7 @@ class RendererExcel(BaseRenderer): paragraph_style = styles.get("paragraph", {}) if paragraph_style.get("color"): - sheet.cell(row=start_row, column=1).font = Font(color=paragraph_style["color"]) + sheet.cell(row=start_row, column=1).font = Font(color=self._get_safe_color(paragraph_style["color"])) start_row += 1 @@ -628,7 +789,7 @@ class RendererExcel(BaseRenderer): sheet.cell(row=start_row, column=1).font = Font( size=font_size, bold=True, - color=heading_style.get("color", "#FF000000") + color=self._get_safe_color(heading_style.get("color", "FF000000")) ) start_row += 1